{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "caption-drop",
  "title": "Caption Drop",
  "description": "Each word falls into place from above and bounces as it lands the moment it is spoken, and the whole phrase drops out of frame when the speaker pauses.",
  "dependencies": ["remotion", "@remotion/captions"],
  "registryDependencies": ["@remocn/caption-core"],
  "files": [
    {
      "path": "registry/remocn/caption-drop/index.tsx",
      "content": "\"use client\";\n\nimport { Fragment } from \"react\";\nimport { interpolate, random, spring, useVideoConfig } 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 CaptionDropProps extends CaptionBaseProps {\n  bounce?: number;\n  shadow?: boolean;\n}\n\nconst EXIT_MS = 360;\nconst EXIT_STAGGER_MS = 120;\nconst FADE_IN_MS = 80;\nconst FALL_EM = 1.4;\nconst EXIT_FALL_EM = 1.8;\nconst MAX_TILT_DEG = 6;\nconst LEGIBILITY_SHADOW =\n  \"0 2px 3px rgba(0, 0, 0, 0.5), 0 4px 16px rgba(0, 0, 0, 0.35)\";\n\nconst clamp = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nexport function CaptionDrop({\n  captions,\n  combineTokensWithinMilliseconds = 1200,\n  holdMs = DEFAULT_HOLD_MS,\n  fontSize = 64,\n  fontWeight = 800,\n  color = \"#ffffff\",\n  bounce = 0.6,\n  shadow = true,\n  className,\n}: CaptionDropProps) {\n  const { fps } = useVideoConfig();\n  const state = useCaptions({\n    captions,\n    combineTokensWithinMilliseconds,\n    holdMs,\n  });\n  if (!state) return null;\n\n  const damping = interpolate(bounce, [0, 1], [20, 6], clamp);\n  const words = groupCaptionWords(state.tokens);\n  const exitWindow = getExitWindowMs(state, EXIT_MS);\n  const exitElapsed = state.endsInSilence\n    ? Math.max(0, exitWindow - state.untilEndMs)\n    : 0;\n  const stagger = Math.min(EXIT_STAGGER_MS, exitWindow * 0.4);\n  const fallSpan = Math.max(1, exitWindow - stagger);\n\n  return (\n    <div\n      className={className}\n      style={{\n        fontSize,\n        fontWeight,\n        color,\n        lineHeight: 1.2,\n        textAlign: \"center\",\n      }}\n    >\n      {words.map((word, w) => {\n        const first = word[0];\n        const sinceMs = state.timeMs - first.token.fromMs;\n        const arrived = sinceMs >= 0;\n        const landed = arrived\n          ? spring({\n              frame: (sinceMs / 1000) * fps,\n              fps,\n              config: { damping, stiffness: 180, mass: 0.7 },\n            })\n          : 0;\n        const fadeIn = arrived\n          ? interpolate(sinceMs, [0, FADE_IN_MS], [0, 1], clamp)\n          : 0;\n        const tilt =\n          (random(`${state.pageIndex}-${first.index}-tilt`) * 2 - 1) *\n          MAX_TILT_DEG;\n        const delay = words.length > 1 ? (w / (words.length - 1)) * stagger : 0;\n        const out = state.endsInSilence\n          ? Math.min(1, Math.max(0, (exitElapsed - delay) / fallSpan))\n          : 0;\n        const gravity = out * out;\n        const y = -(1 - landed) * FALL_EM + gravity * EXIT_FALL_EM;\n        const rotate = tilt * (1 - landed) - tilt * 0.6 * gravity;\n        return (\n          <Fragment key={`${state.pageIndex}-${first.index}`}>\n            {w > 0 && \" \"}\n            <span\n              style={{\n                display: \"inline-block\",\n                whiteSpace: \"nowrap\",\n                opacity: fadeIn * (1 - out ** 1.5),\n                transform: `translateY(${y}em) rotate(${rotate}deg)`,\n                transformOrigin: \"50% 100%\",\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-drop.tsx"
    }
  ],
  "type": "registry:component"
}
