{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "word-stream",
  "title": "Word Stream",
  "description": "Voiceover-paced phrases that build word by word — the next word looms as a blurred ghost before it resolves, then the phrase cuts away.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/word-stream/index.tsx",
      "content": "\"use client\";\n\nimport { Easing, interpolate, useCurrentFrame } from \"remotion\";\n\nexport interface WordStreamProps {\n  text: string;\n  fontSize?: number;\n  color?: string;\n  fontWeight?: number;\n  wordGap?: number;\n  hold?: number;\n  drift?: number;\n  speed?: number;\n  className?: string;\n}\n\ninterface PhraseSchedule {\n  words: string[];\n  start: number;\n  exitStart: number;\n  last: boolean;\n}\n\nconst REVEAL_FRAMES = 3;\nconst ENTRY_FRAMES = 8;\nconst RUN_OUT_FRAMES = 5;\nconst SWAP_OVERLAP = 0;\n\nexport function wordStreamLength(text: string, wordGap = 6, hold = 18): number {\n  const schedule = schedulePhrases(text, wordGap, hold);\n  const finalPhrase = schedule[schedule.length - 1];\n  if (!finalPhrase) return 0;\n  return (\n    finalPhrase.start + (finalPhrase.words.length - 1) * wordGap + REVEAL_FRAMES\n  );\n}\n\nfunction schedulePhrases(\n  text: string,\n  wordGap: number,\n  hold: number,\n): PhraseSchedule[] {\n  const phrases = text\n    .split(\"|\")\n    .map((phrase) => phrase.trim())\n    .filter(Boolean);\n  let cursor = 0;\n  return phrases.map((phrase, i) => {\n    const words = phrase.split(/\\s+/);\n    const start = cursor;\n    const complete = start + (words.length - 1) * wordGap + REVEAL_FRAMES;\n    const exitStart = complete + hold;\n    cursor = exitStart + SWAP_OVERLAP;\n    return { words, start, exitStart, last: i === phrases.length - 1 };\n  });\n}\n\nconst revealEasing = Easing.bezier(0.2, 0.8, 0.2, 1);\nconst entryEasing = Easing.bezier(0.2, 0.8, 0.3, 1);\nconst runOutEasing = Easing.bezier(0.7, 0, 0.9, 0.4);\n\nexport function WordStream({\n  text,\n  fontSize = 72,\n  color = \"#171717\",\n  fontWeight = 400,\n  wordGap = 6,\n  hold = 18,\n  drift = 2,\n  speed = 1,\n  className,\n}: WordStreamProps) {\n  const frame = useCurrentFrame() * speed;\n\n  const schedule = schedulePhrases(text, wordGap, hold);\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        background: \"transparent\",\n      }}\n    >\n      {schedule.map((phrase, i) => {\n        if (frame < phrase.start) return null;\n        if (!phrase.last && frame >= phrase.exitStart) return null;\n\n        const scale = fontSize / 72;\n        const crawl = scale * drift;\n        const life = phrase.exitStart - phrase.start;\n\n        const entryX = interpolate(\n          frame,\n          [phrase.start, phrase.start + ENTRY_FRAMES],\n          [scale * 30, 0],\n          {\n            extrapolateLeft: \"clamp\",\n            extrapolateRight: \"clamp\",\n            easing: entryEasing,\n          },\n        );\n        const crawlX =\n          crawl *\n          (life / 2 - (Math.min(frame, phrase.exitStart) - phrase.start));\n        const runOutX = phrase.last\n          ? 0\n          : interpolate(\n              frame,\n              [phrase.exitStart - RUN_OUT_FRAMES, phrase.exitStart],\n              [0, -scale * 80],\n              {\n                extrapolateLeft: \"clamp\",\n                extrapolateRight: \"clamp\",\n                easing: runOutEasing,\n              },\n            );\n        const x = entryX + crawlX + runOutX;\n\n        return (\n          <div\n            key={i}\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              whiteSpace: \"nowrap\",\n              translate: `${x}px 0`,\n            }}\n          >\n            <span\n              className={className}\n              style={{\n                fontSize,\n                fontWeight,\n                color,\n                letterSpacing: \"-0.02em\",\n                fontFamily:\n                  \"var(--font-geist-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif)\",\n              }}\n            >\n              {phrase.words.map((word, j) => {\n                const revealAt = phrase.start + j * wordGap;\n\n                const opacity = interpolate(\n                  frame,\n                  [revealAt, revealAt + REVEAL_FRAMES],\n                  [0, 1],\n                  {\n                    extrapolateLeft: \"clamp\",\n                    extrapolateRight: \"clamp\",\n                    easing: revealEasing,\n                  },\n                );\n\n                return (\n                  <span\n                    key={j}\n                    style={{\n                      display: \"inline-block\",\n                      whiteSpace: \"pre\",\n                      marginRight:\n                        j < phrase.words.length - 1 ? \"0.28em\" : undefined,\n                      opacity,\n                    }}\n                  >\n                    {word}\n                  </span>\n                );\n              })}\n            </span>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/word-stream.tsx"
    }
  ],
  "type": "registry:component"
}
