{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "caption-active-pop",
  "title": "Caption Active Pop",
  "description": "The word being spoken turns to the accent colour and springs up in scale, then settles back as the voice moves on.",
  "dependencies": ["remotion", "@remotion/captions"],
  "registryDependencies": ["@remocn/caption-core"],
  "files": [
    {
      "path": "registry/remocn/caption-active-pop/index.tsx",
      "content": "\"use client\";\n\nimport { Fragment } from \"react\";\nimport {\n  Easing,\n  interpolate,\n  interpolateColors,\n  spring,\n  useVideoConfig,\n} 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 CaptionActivePopProps extends CaptionBaseProps {\n  activeColor?: string;\n  popScale?: number;\n  shadow?: boolean;\n}\n\nconst ENTER_MS = 160;\nconst EXIT_MS = 220;\nconst RISE_EM = 0.14;\nconst LEGIBILITY_SHADOW =\n  \"0 2px 3px rgba(0, 0, 0, 0.5), 0 4px 16px rgba(0, 0, 0, 0.35)\";\n\nconst POP_SPRING = { damping: 12, stiffness: 220, mass: 0.6 };\nconst RELEASE_SPRING = { damping: 20, stiffness: 200, mass: 0.6 };\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 CaptionActivePop({\n  captions,\n  combineTokensWithinMilliseconds = 1200,\n  holdMs = DEFAULT_HOLD_MS,\n  fontSize = 64,\n  fontWeight = 800,\n  color = \"#ffffff\",\n  activeColor = \"#facc15\",\n  popScale = 1.12,\n  shadow = true,\n  className,\n}: CaptionActivePopProps) {\n  const { fps } = useVideoConfig();\n  const state = useCaptions({\n    captions,\n    combineTokensWithinMilliseconds,\n    holdMs,\n  });\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 words = groupCaptionWords(state.tokens);\n  const activeWord = words.findIndex((word) =>\n    word.some(({ index }) => index === state.activeIndex),\n  );\n  const toFrames = (ms: number) => (ms / 1000) * fps;\n\n  const emphasis = (w: number): number => {\n    if (activeWord < 0 || w > activeWord) return 0;\n    const startMs = words[w][0].token.fromMs;\n    if (w === activeWord) {\n      return spring({\n        frame: toFrames(state.timeMs - startMs),\n        fps,\n        config: POP_SPRING,\n      });\n    }\n    if (w < activeWord - 1) return 0;\n    const releaseMs = words[w + 1][0].token.fromMs;\n    const peak = spring({\n      frame: toFrames(releaseMs - startMs),\n      fps,\n      config: POP_SPRING,\n    });\n    const release = spring({\n      frame: toFrames(state.timeMs - releaseMs),\n      fps,\n      config: RELEASE_SPRING,\n    });\n    return peak * (1 - release);\n  };\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: Math.min(enter, exit),\n        transform: `translateY(${(1 - enter) * RISE_EM}em)`,\n      }}\n    >\n      {words.map((word, w) => {\n        const amount = emphasis(w);\n        return (\n          <Fragment key={`${state.pageIndex}-${word[0].index}`}>\n            {w > 0 && \" \"}\n            <span\n              style={{\n                position: \"relative\",\n                zIndex: w === activeWord ? 1 : 0,\n                display: \"inline-block\",\n                whiteSpace: \"nowrap\",\n                transform: `scale(${1 + (popScale - 1) * amount})`,\n                transformOrigin: \"50% 60%\",\n                color: interpolateColors(\n                  Math.min(1, Math.max(0, amount)),\n                  [0, 1],\n                  [color, activeColor],\n                ),\n                textShadow: shadow ? LEGIBILITY_SHADOW : undefined,\n              }}\n            >\n              {word.map(({ index, token }) => (\n                <span key={index}>{token.text}</span>\n              ))}\n            </span>\n          </Fragment>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/caption-active-pop.tsx"
    }
  ],
  "type": "registry:component"
}
