{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "morphing-modal", "title": "Morphing Modal", "description": "A bento card lifts off the grid and blooms into a full-screen modal driven by a single heavy spring.", "dependencies": [ "remotion" ], "files": [ { "path": "registry/remocn/morphing-modal/index.tsx", "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport interface MorphingModalRect {\n top: number;\n left: number;\n width: number;\n height: number;\n}\n\nexport interface MorphingModalProps {\n from?: MorphingModalRect;\n to?: MorphingModalRect;\n borderRadiusFrom?: number;\n borderRadiusTo?: number;\n morphAt?: number;\n background?: string;\n cardColor?: string;\n textColor?: string;\n mutedColor?: string;\n sourceTitle?: string;\n sourceBody?: string;\n modalTitle?: string;\n modalBody?: string;\n source?: ReactNode;\n modal?: ReactNode;\n speed?: number;\n className?: string;\n}\n\nconst FONT_FAMILY =\n \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\";\n\nconst VIEWPORT_W = 1280;\nconst VIEWPORT_H = 720;\n\nconst DEFAULT_FROM: MorphingModalRect = {\n left: 460,\n top: 260,\n width: 360,\n height: 200,\n};\n\nconst DEFAULT_TO: MorphingModalRect = {\n left: 80,\n top: 60,\n width: VIEWPORT_W - 160,\n height: VIEWPORT_H - 120,\n};\n\nexport function MorphingModal({\n from = DEFAULT_FROM,\n to = DEFAULT_TO,\n borderRadiusFrom = 24,\n borderRadiusTo = 0,\n morphAt = 30,\n background = \"#050505\",\n cardColor = \"#0a0a0a\",\n textColor = \"#fafafa\",\n mutedColor = \"#71717a\",\n sourceTitle = \"Compose video\",\n sourceBody = \"Click to start a new project\",\n modalTitle = \"New project\",\n modalBody = \"Pick a template, drop your assets, and ship a 30s video in under five minutes. Everything renders deterministically — no flicker, no flaky frames.\",\n source,\n modal,\n speed = 1,\n className,\n}: MorphingModalProps) {\n const frame = useCurrentFrame() * speed;\n const { fps } = useVideoConfig();\n\n // ONE heavy spring drives all 5 morph properties at once. High mass +\n // low stiffness so the modal \"blooms\" instead of jumping.\n const morph = spring({\n frame: frame - morphAt,\n fps,\n config: { mass: 1.5, damping: 22, stiffness: 110 },\n });\n\n const left = interpolate(morph, [0, 1], [from.left, to.left]);\n const top = interpolate(morph, [0, 1], [from.top, to.top]);\n const width = interpolate(morph, [0, 1], [from.width, to.width]);\n const height = interpolate(morph, [0, 1], [from.height, to.height]);\n const radius = interpolate(morph, [0, 1], [borderRadiusFrom, borderRadiusTo]);\n\n // Backdrop fades in alongside the morph spring.\n const backdropAlpha = interpolate(morph, [0, 1], [0, 0.8]);\n\n // Source content vanishes in the FIRST third of the morph.\n const sourceOpacity = interpolate(morph, [0, 0.33], [1, 0], {\n extrapolateLeft: \"clamp\",\n extrapolateRight: \"clamp\",\n });\n\n // Modal content arrives in the LAST third with a translateY offset.\n const modalOpacity = interpolate(morph, [0.66, 1], [0, 1], {\n extrapolateLeft: \"clamp\",\n extrapolateRight: \"clamp\",\n });\n const modalTy = interpolate(morph, [0.66, 1], [20, 0], {\n extrapolateLeft: \"clamp\",\n extrapolateRight: \"clamp\",\n });\n\n return (\n \n {/* Backdrop dim layer */}\n \n\n {/* Suggestion of a bento grid behind so the morph reads as\n \"card lifting off\". Very low contrast. */}\n \n\n {/* The morphing card */}\n \n {/* Source layer (fades out first) */}\n \n {source ?? (\n <>\n \n {sourceTitle}\n \n
\n {sourceBody}\n
\n \n )}\n \n\n {/* Modal layer (arrives last with translateY) */}\n \n {modal ?? (\n <>\n \n {modalTitle}\n \n \n {modalBody}\n \n \n Get started\n \n \n )}\n \n \n \n );\n}\n\nfunction BackgroundGrid() {\n // Decorative bento background — 3x2 cards, very low contrast.\n const cells = [\n { x: 80, y: 60, w: 360, h: 200 },\n { x: 460, y: 60, w: 360, h: 200 },\n { x: 840, y: 60, w: 360, h: 200 },\n { x: 80, y: 460, w: 360, h: 200 },\n { x: 840, y: 460, w: 360, h: 200 },\n ];\n return (\n <>\n {cells.map((c, i) => (\n \n ))}\n \n );\n}\n", "type": "registry:component", "target": "components/remocn/morphing-modal.tsx" } ], "type": "registry:component" }