{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "caption-typewriter",
  "title": "Caption Typewriter",
  "description": "Each word types out character by character at the pace it is spoken, with a caret that blinks while the speaker pauses.",
  "dependencies": ["remotion", "@remotion/captions"],
  "registryDependencies": ["@remocn/caption-core"],
  "files": [
    {
      "path": "registry/remocn/caption-typewriter/index.tsx",
      "content": "\"use client\";\n\nimport { Fragment } from \"react\";\nimport { interpolate } from \"remotion\";\nimport {\n  type CaptionBaseProps,\n  DEFAULT_HOLD_MS,\n  getExitWindowMs,\n  groupCaptionWords,\n  useCaptions,\n} from \"@/lib/remocn/caption-core\";\n\nexport interface CaptionTypewriterProps extends CaptionBaseProps {\n  caret?: boolean;\n  shadow?: boolean;\n}\n\nconst EXIT_MS = 220;\nconst BLINK_MS = 500;\nconst LEGIBILITY_SHADOW =\n  \"0 2px 3px rgba(0, 0, 0, 0.5), 0 4px 16px rgba(0, 0, 0, 0.35)\";\nconst CARET_SHADOW = \"0 2px 6px rgba(0, 0, 0, 0.45)\";\n\nfunction typedCount(text: string, progress: number): number {\n  const length = Array.from(text).length;\n  return Math.min(length, Math.ceil(progress * length));\n}\n\nexport function CaptionTypewriter({\n  captions,\n  combineTokensWithinMilliseconds = 1200,\n  holdMs = DEFAULT_HOLD_MS,\n  fontSize = 64,\n  fontWeight = 800,\n  color = \"#ffffff\",\n  caret = true,\n  shadow = true,\n  className,\n}: CaptionTypewriterProps) {\n  const state = useCaptions({\n    captions,\n    combineTokensWithinMilliseconds,\n    holdMs,\n  });\n  if (!state) return null;\n\n  const exit = state.endsInSilence\n    ? interpolate(\n        state.untilEndMs,\n        [0, getExitWindowMs(state, EXIT_MS)],\n        [0, 1],\n        {\n          extrapolateLeft: \"clamp\",\n          extrapolateRight: \"clamp\",\n        },\n      )\n    : 1;\n\n  const active = state.tokens[state.activeIndex];\n  const typing = active !== undefined && active.progress < 1;\n  const idleMs = active ? state.timeMs - active.toMs : 0;\n  const caretOn = typing || Math.floor(idleMs / BLINK_MS) % 2 === 0;\n\n  return (\n    <div\n      className={className}\n      style={{\n        fontSize,\n        fontWeight,\n        color,\n        lineHeight: 1.2,\n        textAlign: \"center\",\n        opacity: exit,\n      }}\n    >\n      {groupCaptionWords(state.tokens).map((word, w) => (\n        <Fragment key={`${state.pageIndex}-${word[0].index}`}>\n          {w > 0 && \" \"}\n          <span style={{ display: \"inline-block\", whiteSpace: \"nowrap\" }}>\n            {word.map(({ index, token }) => {\n              const chars = Array.from(token.text);\n              const typed = typedCount(token.text, token.progress);\n              return (\n                <Fragment key={index}>\n                  <span\n                    style={{\n                      textShadow: shadow ? LEGIBILITY_SHADOW : undefined,\n                    }}\n                  >\n                    {chars.slice(0, typed).join(\"\")}\n                  </span>\n                  {caret && index === state.activeIndex ? (\n                    <span style={{ position: \"relative\" }}>\n                      <span\n                        style={{\n                          position: \"absolute\",\n                          left: \"0.03em\",\n                          top: \"0.1em\",\n                          bottom: \"0.1em\",\n                          width: \"0.07em\",\n                          backgroundColor: color,\n                          boxShadow: shadow ? CARET_SHADOW : undefined,\n                          opacity: caretOn ? 1 : 0,\n                        }}\n                      />\n                    </span>\n                  ) : null}\n                  <span aria-hidden style={{ color: \"transparent\" }}>\n                    {chars.slice(typed).join(\"\")}\n                  </span>\n                </Fragment>\n              );\n            })}\n          </span>\n        </Fragment>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/caption-typewriter.tsx"
    }
  ],
  "type": "registry:component"
}
