{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "captions",
  "title": "Captions",
  "description": "Kinetic subtitle pill from aligned words.",
  "dependencies": [
    "remotion"
  ],
  "registryDependencies": [
    "@jbm/tokens",
    "@jbm/motion-hooks"
  ],
  "files": [
    {
      "path": "registry/jbm/motion/captions.tsx",
      "content": "import * as React from \"react\";\nimport { interpolate } from \"remotion\";\nimport { color, font, type Orientation } from \"../lib/tokens\";\nimport { useSec } from \"./hooks\";\n\nexport type Word = { w: string; s: number; e: number; emph?: boolean };\n\n/**\n * Kinetic subtitle pill. Groups words on punctuation, gaps > 0.55 s, or max words (9 landscape / 6 vertical);\n * unspoken words sit at 0.28 opacity; emphasis words pop in the soft colour. Pass the video's aligned words.\n */\nexport function Captions({ words, orientation = \"landscape\", offset = 0 }: { words: Word[]; orientation?: Orientation; offset?: number }) {\n  const t = useSec() - offset;\n  const maxW = orientation === \"vertical\" ? 6 : 9;\n  const groups = React.useMemo(() => {\n    const out: { words: Word[]; s: number; e: number }[] = [];\n    let cur: Word[] = [];\n    for (let i = 0; i < words.length; i++) {\n      const w = words[i]; cur.push(w);\n      const next = words[i + 1];\n      const ends = /[.!?…,:;—]$/.test(w.w);\n      const gap = next ? next.s - w.e : 99;\n      if (!next || ends || gap > 0.55 || cur.length >= maxW) { out.push({ words: cur, s: cur[0].s, e: next ? Math.min(w.e + 0.6, next.s) : w.e + 1 }); cur = []; }\n    }\n    return out;\n  }, [words, maxW]);\n  const g = groups.find((x) => t >= x.s && t < x.e);\n  if (!g) return null;\n  const fd = Math.min(0.12, Math.max(0.01, (g.e - g.s) / 2 - 0.005));\n  const fade = interpolate(t, [g.s, g.s + fd, g.e - fd, g.e], [0, 1, 1, 0], { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" });\n  const v = orientation === \"vertical\";\n  return (\n    <div style={{ position: \"absolute\", left: 0, right: 0, bottom: v ? 270 : 84, display: \"flex\", justifyContent: \"center\", opacity: fade, pointerEvents: \"none\" }}>\n      <div style={{ fontFamily: font.sans, fontSize: v ? 54 : 44, fontWeight: 600, color: color.bg, background: color.ink, padding: v ? \"18px 34px\" : \"14px 28px\", borderRadius: 20, maxWidth: v ? 940 : 1500, textAlign: \"center\", lineHeight: 1.25 }}>\n        {g.words.map((w, i) => {\n          const on = t >= w.s;\n          const pop = w.emph ? interpolate(t, [w.s, w.s + 0.2, w.s + 0.4], [1, 1.22, 1.08], { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" }) : 1;\n          return (\n            <span key={i} style={{ opacity: on ? 1 : 0.28, color: w.emph && on ? color.soft : color.bg, fontWeight: w.emph ? 800 : 600, display: \"inline-block\", transform: `translateY(${(1 - pop) * 6}px)`, marginRight: 12 }}>{w.w}</span>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "src/jbm/motion/captions.tsx"
    }
  ],
  "type": "registry:component"
}