{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "zoom-words",
  "title": "Zoom Words",
  "description": "A close-up word reveal: huge words fade into a long line while the camera rides the write head, the line streaming off-frame as the shot eases into focus.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/zoom-words/index.tsx",
      "content": "\"use client\";\n\nimport { Easing, interpolate, useCurrentFrame } from \"remotion\";\n\nexport interface ZoomWordsProps {\n  text: string;\n  fontSize?: number;\n  zoom?: number;\n  color?: string;\n  fontWeight?: number;\n  wordGap?: number;\n  anchor?: number;\n  blur?: number;\n  blurFrames?: number;\n  speed?: number;\n  className?: string;\n}\n\nconst CHAR_WIDTH_EM = 0.52;\nconst SPACE_WIDTH_EM = 0.3;\nconst WORD_BLUR_EM = 0.05;\nconst phraseEasing = Easing.bezier(0.5, 0, 0.7, 0.7);\nconst FADE_EARLY = 0.5;\nconst FADE_SLOTS = 2;\n\nfunction smoothstep(t: number): number {\n  const x = Math.min(Math.max(t, 0), 1);\n  return x * x * (3 - 2 * x);\n}\nconst WORD_SHIFT_X_EM = 0.16;\nconst WORD_SHIFT_Y_EM = 0.12;\n\nfunction wordWidthEm(word: string): number {\n  return Array.from(word).length * CHAR_WIDTH_EM + SPACE_WIDTH_EM;\n}\n\nexport function zoomWordsLength(text: string, wordGap = 12): number {\n  const words = text.trim().split(/\\s+/).filter(Boolean);\n  return words.length * wordGap;\n}\n\nexport function ZoomWords({\n  text,\n  fontSize = 64,\n  zoom = 2.2,\n  color = \"#cfc2ff\",\n  fontWeight = 400,\n  wordGap = 12,\n  anchor = 0.5,\n  blur = 0.04,\n  blurFrames = 60,\n  speed = 1,\n  className,\n}: ZoomWordsProps) {\n  const frame = useCurrentFrame() * speed;\n\n  const words = text.trim().split(/\\s+/).filter(Boolean);\n\n  const phraseFrames = words.length * wordGap;\n  const wordProgress =\n    words.length *\n    interpolate(frame, [0, phraseFrames], [0, 1], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: phraseEasing,\n    });\n\n  const totalEm = words.reduce((sum, w) => sum + wordWidthEm(w), 0);\n  const firstHalfEm = words.length > 0 ? wordWidthEm(words[0]) / 2 : 0;\n  const lastHalfEm =\n    words.length > 0 ? wordWidthEm(words[words.length - 1]) / 2 : 0;\n  let revealedEm = 0;\n  for (let i = 0; i < words.length; i++) {\n    revealedEm +=\n      wordWidthEm(words[i]) *\n      smoothstep((wordProgress - i + FADE_EARLY) / FADE_SLOTS);\n  }\n  const travelMax = Math.max(totalEm - lastHalfEm - firstHalfEm, 0.001);\n  const raw = Math.min(Math.max(revealedEm, 0), travelMax);\n  const cameraEm = firstHalfEm + travelMax * smoothstep(raw / travelMax);\n  const cameraPct = totalEm > 0 ? (cameraEm / totalEm) * 100 : 0;\n\n  const blurNow =\n    blur *\n    fontSize *\n    zoom *\n    interpolate(frame, [0, blurFrames], [1, 0], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n    });\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        overflow: \"hidden\",\n        background: \"transparent\",\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          scale: `${zoom}`,\n          filter: blurNow > 0.05 ? `blur(${blurNow}px)` : undefined,\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            top: \"50%\",\n            left: `${anchor * 100}%`,\n            transform: \"translateY(-50%)\",\n            whiteSpace: \"pre\",\n          }}\n        >\n          <span\n            className={className}\n            style={{\n              display: \"inline-flex\",\n              alignItems: \"baseline\",\n              transform: `translateX(-${cameraPct.toFixed(3)}%)`,\n              fontSize,\n              fontWeight,\n              color,\n              letterSpacing: \"-0.02em\",\n              lineHeight: 1.15,\n              whiteSpace: \"pre\",\n              fontFamily:\n                \"var(--font-geist-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif)\",\n            }}\n          >\n            {words.map((word, i) => {\n              const p = smoothstep(\n                (wordProgress - i + FADE_EARLY) / FADE_SLOTS,\n              );\n              const opacity = smoothstep(p * 1.2);\n              const wordBlur = WORD_BLUR_EM * fontSize * (1 - p);\n              const arrive = 1 - smoothstep(p * 1.6);\n              return (\n                <span\n                  key={i}\n                  style={{\n                    display: \"inline-block\",\n                    whiteSpace: \"pre\",\n                    opacity,\n                    filter: wordBlur > 0.05 ? `blur(${wordBlur}px)` : undefined,\n                    translate: `${(WORD_SHIFT_X_EM * fontSize * arrive).toFixed(1)}px ${(WORD_SHIFT_Y_EM * fontSize * arrive).toFixed(1)}px`,\n                  }}\n                >\n                  {word}\n                  {i < words.length - 1 && \" \"}\n                </span>\n              );\n            })}\n          </span>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/zoom-words.tsx"
    }
  ],
  "type": "registry:component"
}
