{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "caption-speaker",
  "title": "Caption Speaker",
  "description": "Dialogue captions where every phrase takes its speaker's colour, so a conversation reads like a script.",
  "dependencies": ["remotion", "@remotion/captions"],
  "registryDependencies": ["@remocn/caption-core"],
  "files": [
    {
      "path": "registry/remocn/caption-speaker/index.tsx",
      "content": "\"use client\";\n\nimport { Fragment, useMemo } from \"react\";\nimport { Easing, interpolate } from \"remotion\";\nimport {\n  type CaptionBaseProps,\n  DEFAULT_HOLD_MS,\n  getExitWindowMs,\n  groupCaptionWords,\n  type SpeakerCaption,\n  useCaptions,\n} from \"@/lib/remocn/caption-core\";\n\nexport interface CaptionSpeakerProps extends CaptionBaseProps {\n  captions: SpeakerCaption[];\n  speakerColors?: Record<string, string>;\n  upcomingOpacity?: number;\n  shadow?: boolean;\n}\n\nconst SPEAKER_PALETTE = [\"#facc15\", \"#38bdf8\", \"#f472b6\", \"#4ade80\"];\n\nconst ENTER_MS = 160;\nconst EXIT_MS = 220;\nconst RISE_EM = 0.14;\nconst REVEAL_MS = 100;\nconst LEGIBILITY_SHADOW =\n  \"0 2px 3px rgba(0, 0, 0, 0.5), 0 4px 16px rgba(0, 0, 0, 0.35)\";\n\nconst enterEasing = Easing.bezier(0.2, 0.8, 0.2, 1);\n\nconst clamp = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nexport function CaptionSpeaker({\n  captions,\n  combineTokensWithinMilliseconds = 1200,\n  holdMs = DEFAULT_HOLD_MS,\n  fontSize = 64,\n  fontWeight = 800,\n  color = \"#ffffff\",\n  speakerColors,\n  upcomingOpacity = 0.4,\n  shadow = true,\n  className,\n}: CaptionSpeakerProps) {\n  const state = useCaptions({\n    captions,\n    combineTokensWithinMilliseconds,\n    holdMs,\n  });\n  const autoColors = useMemo(() => {\n    const pinned = new Set(Object.values(speakerColors ?? {}));\n    const free = SPEAKER_PALETTE.filter((c) => !pinned.has(c));\n    const palette = free.length > 0 ? free : SPEAKER_PALETTE;\n    const assigned = new Map<string, string>();\n    for (const caption of captions) {\n      const { speaker } = caption;\n      if (\n        speaker === undefined ||\n        speakerColors?.[speaker] !== undefined ||\n        assigned.has(speaker)\n      ) {\n        continue;\n      }\n      assigned.set(speaker, palette[assigned.size % palette.length]);\n    }\n    return assigned;\n  }, [captions, speakerColors]);\n  if (!state) return null;\n\n  const enter = interpolate(\n    state.previous ? ENTER_MS : state.sinceStartMs,\n    [0, ENTER_MS],\n    [0, 1],\n    {\n      ...clamp,\n      easing: enterEasing,\n    },\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\n  const { speaker } = state;\n  const speakerColor =\n    speaker === undefined\n      ? color\n      : (speakerColors?.[speaker] ?? autoColors.get(speaker) ?? color);\n\n  return (\n    <div\n      className={className}\n      style={{\n        fontSize,\n        fontWeight,\n        color: speakerColor,\n        lineHeight: 1.2,\n        textAlign: \"center\",\n        opacity: Math.min(enter, exit),\n        transform: `translateY(${(1 - enter) * RISE_EM}em)`,\n      }}\n    >\n      {groupCaptionWords(state.tokens).map((word, w) => (\n        <Fragment key={`${state.pageIndex}-${word[0].index}`}>\n          {w > 0 && \" \"}\n          <span\n            style={{\n              display: \"inline-block\",\n              whiteSpace: \"nowrap\",\n              textShadow: shadow ? LEGIBILITY_SHADOW : undefined,\n            }}\n          >\n            {word.map(({ index, token }) => (\n              <span\n                key={index}\n                style={{\n                  opacity: interpolate(\n                    state.timeMs - token.fromMs,\n                    [0, REVEAL_MS],\n                    [upcomingOpacity, 1],\n                    clamp,\n                  ),\n                }}\n              >\n                {token.text}\n              </span>\n            ))}\n          </span>\n        </Fragment>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/caption-speaker.tsx"
    }
  ],
  "type": "registry:component"
}
