{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "caption-scramble",
  "title": "Caption Scramble",
  "description": "Each word arrives as random glyphs at the moment it is spoken and decodes left to right into the real letters.",
  "dependencies": ["remotion", "@remotion/captions"],
  "registryDependencies": ["@remocn/caption-core"],
  "files": [
    {
      "path": "registry/remocn/caption-scramble/index.tsx",
      "content": "\"use client\";\n\nimport { Fragment } from \"react\";\nimport { Easing, interpolate, random, 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 CaptionScrambleProps extends CaptionBaseProps {\n  glyphColor?: string;\n  shadow?: boolean;\n}\n\nconst ENTER_MS = 160;\nconst EXIT_MS = 220;\nconst RISE_EM = 0.14;\nconst MAX_RESOLVE_MS = 300;\nconst FRAMES_PER_ROLL = 2;\nconst GLYPHS = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&*/+=\";\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\nfunction resolveProgress(sinceMs: number, fromMs: number, toMs: number) {\n  const duration = Math.min(toMs - fromMs, MAX_RESOLVE_MS);\n  if (duration <= 0) return 1;\n  return Math.min(1, sinceMs / duration);\n}\n\nexport function CaptionScramble({\n  captions,\n  combineTokensWithinMilliseconds = 1200,\n  holdMs = DEFAULT_HOLD_MS,\n  fontSize = 64,\n  fontWeight = 800,\n  color = \"#ffffff\",\n  glyphColor = \"#a3a3a3\",\n  shadow = true,\n  className,\n}: CaptionScrambleProps) {\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  const step = Math.floor(((state.timeMs / 1000) * fps) / FRAMES_PER_ROLL);\n  const textShadow = shadow ? LEGIBILITY_SHADOW : undefined;\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      {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              if (sinceMs < 0) {\n                return (\n                  <span key={index} style={{ opacity: 0 }}>\n                    {token.text}\n                  </span>\n                );\n              }\n              const progress = resolveProgress(\n                sinceMs,\n                token.fromMs,\n                token.toMs,\n              );\n              if (progress >= 1) {\n                return (\n                  <span key={index} style={{ textShadow }}>\n                    {token.text}\n                  </span>\n                );\n              }\n              const chars = Array.from(token.text);\n              const resolved = Math.floor(progress * chars.length);\n              return (\n                <span\n                  key={index}\n                  style={{ position: \"relative\", display: \"inline-block\" }}\n                >\n                  <span style={{ color: \"transparent\" }}>{token.text}</span>\n                  <span\n                    aria-hidden\n                    style={{\n                      position: \"absolute\",\n                      left: 0,\n                      right: 0,\n                      top: 0,\n                      whiteSpace: \"nowrap\",\n                      clipPath: \"inset(-50% 0 -50% 0)\",\n                      textShadow,\n                    }}\n                  >\n                    <span>{chars.slice(0, resolved).join(\"\")}</span>\n                    <span style={{ color: glyphColor }}>\n                      {chars\n                        .slice(resolved)\n                        .map((char, c) =>\n                          /\\s/.test(char)\n                            ? char\n                            : GLYPHS[\n                                Math.floor(\n                                  random(\n                                    `${state.pageIndex}-${index}-${resolved + c}-${step}`,\n                                  ) * GLYPHS.length,\n                                )\n                              ],\n                        )\n                        .join(\"\")}\n                    </span>\n                  </span>\n                </span>\n              );\n            })}\n          </span>\n        </Fragment>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/caption-scramble.tsx"
    }
  ],
  "type": "registry:component"
}
