{ "$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