{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "caption-prosody",
  "title": "Caption Prosody",
  "description": "Word size follows the delivery, so drawn-out words land large and rushed words stay small, and the rhythm of the voice is visible with the sound off.",
  "dependencies": ["remotion", "@remotion/captions"],
  "registryDependencies": ["@remocn/caption-core"],
  "files": [
    {
      "path": "registry/remocn/caption-prosody/index.tsx",
      "content": "\"use client\";\n\nimport { Fragment } from \"react\";\nimport { Easing, interpolate } from \"remotion\";\nimport {\n  type CaptionBaseProps,\n  type CaptionTokenState,\n  captionWordText,\n  DEFAULT_HOLD_MS,\n  getExitWindowMs,\n  groupCaptionWords,\n  useCaptions,\n} from \"@/lib/remocn/caption-core\";\n\nexport interface CaptionProsodyProps extends CaptionBaseProps {\n  minScale?: number;\n  maxScale?: number;\n  activeColor?: string;\n  shadow?: boolean;\n}\n\nconst EXIT_MS = 220;\nconst WORD_IN_MS = 180;\nconst RISE_EM = 0.2;\nconst LEGIBILITY_SHADOW =\n  \"0 2px 3px rgba(0, 0, 0, 0.5), 0 4px 16px rgba(0, 0, 0, 0.35)\";\n\nconst appearEasing = Easing.bezier(0.2, 0.8, 0.2, 1);\n\nconst clamp = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nfunction letterCount(text: string): number {\n  return Math.max(1, text.match(/[\\p{L}\\p{N}]/gu)?.length ?? 0);\n}\n\nfunction median(values: number[]): number {\n  const sorted = [...values].sort((a, b) => a - b);\n  const mid = sorted.length >> 1;\n  return sorted.length % 2 === 1\n    ? sorted[mid]\n    : (sorted[mid - 1] + sorted[mid]) / 2;\n}\n\nexport function getProsodyScales(\n  words: { token: CaptionTokenState }[][],\n  minScale: number,\n  maxScale: number,\n): number[] {\n  const paces = words.map((word) => {\n    const fromMs = word[0].token.fromMs;\n    const toMs = word[word.length - 1].token.toMs;\n    return Math.max(0, toMs - fromMs) / letterCount(captionWordText(word));\n  });\n  const typical = median(paces);\n  if (!(typical > 0)) return words.map(() => 1);\n  return paces.map((pace) => {\n    const t = Math.max(-1, Math.min(1, Math.log2(Math.max(pace, 1) / typical)));\n    return t >= 0 ? 1 + t * (maxScale - 1) : 1 + t * (1 - minScale);\n  });\n}\n\nexport function CaptionProsody({\n  captions,\n  combineTokensWithinMilliseconds = 1200,\n  holdMs = DEFAULT_HOLD_MS,\n  fontSize = 64,\n  fontWeight = 800,\n  color = \"#ffffff\",\n  minScale = 0.75,\n  maxScale = 1.6,\n  activeColor = \"#facc15\",\n  shadow = true,\n  className,\n}: CaptionProsodyProps) {\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        clamp,\n      )\n    : 1;\n  const words = groupCaptionWords(state.tokens);\n  const scales = getProsodyScales(words, minScale, maxScale);\n\n  return (\n    <div\n      className={className}\n      style={{\n        fontSize,\n        fontWeight,\n        color,\n        lineHeight: 1.1,\n        textAlign: \"center\",\n        opacity: exit,\n      }}\n    >\n      {words.map((word, w) => {\n        const appear = interpolate(\n          state.timeMs - word[0].token.fromMs,\n          [0, WORD_IN_MS],\n          [0, 1],\n          { ...clamp, easing: appearEasing },\n        );\n        const active = word.some(({ index }) => index === state.activeIndex);\n        return (\n          <Fragment key={`${state.pageIndex}-${word[0].index}`}>\n            {w > 0 && \" \"}\n            <span\n              style={{\n                display: \"inline-block\",\n                whiteSpace: \"nowrap\",\n                fontSize: `${scales[w]}em`,\n                color: active ? activeColor : color,\n                opacity: appear,\n                transform: `translateY(${(1 - appear) * RISE_EM}em)`,\n                textShadow: shadow ? LEGIBILITY_SHADOW : undefined,\n              }}\n            >\n              {captionWordText(word)}\n            </span>\n          </Fragment>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/caption-prosody.tsx"
    }
  ],
  "type": "registry:component"
}
