{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "caption-bounce-in",
  "title": "Caption Bounce In",
  "description": "Each word springs up into place with a little overshoot the moment it is spoken, and the phrase builds word by word.",
  "dependencies": ["remotion", "@remotion/captions"],
  "registryDependencies": ["@remocn/caption-core"],
  "files": [
    {
      "path": "registry/remocn/caption-bounce-in/index.tsx",
      "content": "\"use client\";\n\nimport { Fragment } from \"react\";\nimport { interpolate, 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 CaptionBounceInProps extends CaptionBaseProps {\n  bounce?: number;\n  shadow?: boolean;\n}\n\nconst EXIT_MS = 220;\nconst FADE_IN_MS = 90;\nconst DROP_EM = 0.35;\nconst START_SCALE = 0.7;\nconst LEGIBILITY_SHADOW =\n  \"0 2px 3px rgba(0, 0, 0, 0.5), 0 4px 16px rgba(0, 0, 0, 0.35)\";\n\nexport function CaptionBounceIn({\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}: CaptionBounceInProps) {\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], [22, 7], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const exit = state.endsInSilence\n    ? interpolate(\n        state.untilEndMs,\n        [0, getExitWindowMs(state, EXIT_MS)],\n        [0, 1],\n        {\n          extrapolateLeft: \"clamp\",\n          extrapolateRight: \"clamp\",\n        },\n      )\n    : 1;\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: exit,\n      }}\n    >\n      {groupCaptionWords(state.tokens).map((word, w) => (\n        <Fragment key={`${state.pageIndex}-${word[0].index}`}>\n          {w > 0 && \" \"}\n          <span style={{ display: \"inline-block\", whiteSpace: \"nowrap\" }}>\n            {word.map(({ index, token }) => {\n              const sinceMs = state.timeMs - token.fromMs;\n              const arrived = sinceMs >= 0;\n              const p = arrived\n                ? spring({\n                    frame: (sinceMs / 1000) * fps,\n                    fps,\n                    config: { damping, stiffness: 220, mass: 0.6 },\n                  })\n                : 0;\n              const opacity = arrived\n                ? interpolate(sinceMs, [0, FADE_IN_MS], [0, 1], {\n                    extrapolateLeft: \"clamp\",\n                    extrapolateRight: \"clamp\",\n                  })\n                : 0;\n              return (\n                <span\n                  key={index}\n                  style={{\n                    display: \"inline-block\",\n                    opacity,\n                    transform: `translateY(${(1 - p) * DROP_EM}em) scale(${\n                      START_SCALE + (1 - START_SCALE) * p\n                    })`,\n                    transformOrigin: \"50% 80%\",\n                    textShadow: shadow ? LEGIBILITY_SHADOW : undefined,\n                  }}\n                >\n                  {token.text}\n                </span>\n              );\n            })}\n          </span>\n        </Fragment>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/caption-bounce-in.tsx"
    }
  ],
  "type": "registry:component"
}
