{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "caption-stamp",
  "title": "Caption Stamp",
  "description": "Plain captions where the words that matter slam down as a rubber stamp the moment they are spoken.",
  "dependencies": ["remotion", "@remotion/captions"],
  "registryDependencies": ["@remocn/caption-core"],
  "files": [
    {
      "path": "registry/remocn/caption-stamp/index.tsx",
      "content": "\"use client\";\n\nimport { Fragment, useId } from \"react\";\nimport { Easing, interpolate, random } from \"remotion\";\nimport {\n  type CaptionBaseProps,\n  captionWordText,\n  createKeywordMatcher,\n  DEFAULT_HOLD_MS,\n  getExitWindowMs,\n  groupCaptionWords,\n  normalizeCaptionWord,\n  useCaptions,\n} from \"@/lib/remocn/caption-core\";\n\nexport interface CaptionStampProps extends CaptionBaseProps {\n  keywords?: string[];\n  stampColor?: string;\n  texture?: boolean;\n  shadow?: boolean;\n}\n\nconst ENTER_MS = 160;\nconst EXIT_MS = 220;\nconst RISE_EM = 0.14;\nconst SLAM_MS = 90;\nconst SETTLE_MS = 170;\nconst INK_MS = 50;\nconst PAD_EM = 0.16;\nconst BORDER_EM = 0.06;\nconst RADIUS_EM = 0.08;\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);\nconst slamEasing = Easing.in(Easing.quad);\nconst settleEasing = Easing.out(Easing.quad);\n\nconst clamp = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nfunction stampScale(sinceMs: number): number {\n  if (sinceMs <= SLAM_MS) {\n    return interpolate(sinceMs, [0, SLAM_MS], [1.8, 0.96], {\n      ...clamp,\n      easing: slamEasing,\n    });\n  }\n  return interpolate(sinceMs, [SLAM_MS, SETTLE_MS], [0.96, 1], {\n    ...clamp,\n    easing: settleEasing,\n  });\n}\n\nexport function CaptionStamp({\n  captions,\n  combineTokensWithinMilliseconds = 1200,\n  holdMs = DEFAULT_HOLD_MS,\n  fontSize = 64,\n  fontWeight = 800,\n  color = \"#ffffff\",\n  keywords,\n  stampColor = \"#ef4444\",\n  texture = true,\n  shadow = true,\n  className,\n}: CaptionStampProps) {\n  const filterId = `caption-stamp-${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\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    { ...clamp, easing: enterEasing },\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 isKeyword = createKeywordMatcher(keywords);\n  const byKeyword = keywords !== undefined && keywords.length > 0;\n  let longest = -1;\n  let longestLength = 0;\n  if (!byKeyword) {\n    words.forEach((word, w) => {\n      const length = normalizeCaptionWord(captionWordText(word)).length;\n      if (length > longestLength) {\n        longest = w;\n        longestLength = length;\n      }\n    });\n  }\n  const textShadow = shadow ? LEGIBILITY_SHADOW : undefined;\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"relative\",\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      {texture && (\n        <svg\n          aria-hidden\n          width={0}\n          height={0}\n          style={{ position: \"absolute\", overflow: \"hidden\" }}\n        >\n          <filter id={filterId} x=\"-10%\" y=\"-20%\" width=\"120%\" height=\"140%\">\n            <feTurbulence\n              type=\"fractalNoise\"\n              baseFrequency=\"0.45\"\n              numOctaves={2}\n              seed={4}\n              result=\"warp\"\n            />\n            <feDisplacementMap\n              in=\"SourceGraphic\"\n              in2=\"warp\"\n              scale={fontSize * 0.035}\n              xChannelSelector=\"R\"\n              yChannelSelector=\"G\"\n              result=\"rough\"\n            />\n            <feTurbulence\n              type=\"fractalNoise\"\n              baseFrequency=\"0.8\"\n              numOctaves={1}\n              seed={9}\n              result=\"grain\"\n            />\n            <feColorMatrix\n              in=\"grain\"\n              type=\"matrix\"\n              values=\"0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -10 6.8\"\n              result=\"holes\"\n            />\n            <feComposite in=\"rough\" in2=\"holes\" operator=\"in\" />\n          </filter>\n        </svg>\n      )}\n      {words.map((word, w) => {\n        const key = `${state.pageIndex}-${word[0].index}`;\n        const text = captionWordText(word);\n        const stamped = byKeyword ? isKeyword(text) : w === longest;\n        if (!stamped) {\n          return (\n            <Fragment key={key}>\n              {w > 0 && \" \"}\n              <span\n                style={{\n                  display: \"inline-block\",\n                  whiteSpace: \"nowrap\",\n                  textShadow,\n                }}\n              >\n                {text}\n              </span>\n            </Fragment>\n          );\n        }\n        const sinceMs = state.timeMs - word[0].token.fromMs;\n        const rotation =\n          -8 + random(`caption-stamp-${state.pageIndex}-${word[0].index}`) * 5;\n        return (\n          <Fragment key={key}>\n            {w > 0 && \" \"}\n            <span\n              style={{\n                position: \"relative\",\n                display: \"inline-block\",\n                whiteSpace: \"nowrap\",\n              }}\n            >\n              <span\n                style={{\n                  opacity: interpolate(\n                    sinceMs,\n                    [SLAM_MS * 0.6, SLAM_MS],\n                    [1, 0],\n                    clamp,\n                  ),\n                  textShadow,\n                }}\n              >\n                {text}\n              </span>\n              {sinceMs >= 0 && (\n                <span\n                  aria-hidden\n                  style={{\n                    position: \"absolute\",\n                    left: `-${PAD_EM}em`,\n                    right: `-${PAD_EM}em`,\n                    top: \"8%\",\n                    bottom: \"4%\",\n                    display: \"flex\",\n                    alignItems: \"center\",\n                    justifyContent: \"center\",\n                    lineHeight: 1,\n                    color: stampColor,\n                    border: `${BORDER_EM}em solid ${stampColor}`,\n                    borderRadius: `${RADIUS_EM}em`,\n                    opacity: interpolate(sinceMs, [0, INK_MS], [0, 1], clamp),\n                    transform: `rotate(${rotation}deg) scale(${stampScale(sinceMs)})`,\n                    filter: texture ? `url(#${filterId})` : undefined,\n                  }}\n                >\n                  {text}\n                </span>\n              )}\n            </span>\n          </Fragment>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/caption-stamp.tsx"
    }
  ],
  "type": "registry:component"
}
