{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "caption-marker",
  "title": "Caption Marker",
  "description": "Plain captions where the words that matter get a highlighter swipe the moment they are spoken.",
  "dependencies": ["remotion", "@remotion/captions"],
  "registryDependencies": ["@remocn/caption-core"],
  "files": [
    {
      "path": "registry/remocn/caption-marker/index.tsx",
      "content": "\"use client\";\n\nimport { Fragment } from \"react\";\nimport { Easing, interpolate } from \"remotion\";\nimport {\n  type CaptionBaseProps,\n  captionWordText,\n  createKeywordMatcher,\n  DEFAULT_HOLD_MS,\n  getExitWindowMs,\n  groupCaptionWords,\n  useCaptions,\n} from \"@/lib/remocn/caption-core\";\n\nexport interface CaptionMarkerProps extends CaptionBaseProps {\n  keywords?: string[];\n  markerColor?: string;\n  markerTextColor?: string;\n  shadow?: boolean;\n}\n\nconst ENTER_MS = 160;\nconst EXIT_MS = 220;\nconst RISE_EM = 0.14;\nconst MIN_DRAW_MS = 220;\nconst PAD_EM = 0.12;\nconst RADIUS_EM = 0.18;\nconst TILT_DEG = -1;\nconst BAND_TOP = \"12%\";\nconst BAND_BOTTOM = \"2%\";\n\nconst enterEasing = Easing.bezier(0.2, 0.8, 0.2, 1);\nconst drawEasing = Easing.bezier(0.25, 1, 0.5, 1);\n\nconst clamp = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nfunction legibilityShadow(strength: number): string | undefined {\n  if (strength <= 0) return undefined;\n  return `0 2px 3px rgba(0, 0, 0, ${0.5 * strength}), 0 4px 16px rgba(0, 0, 0, ${0.35 * strength})`;\n}\n\nexport function CaptionMarker({\n  captions,\n  combineTokensWithinMilliseconds = 1200,\n  holdMs = DEFAULT_HOLD_MS,\n  fontSize = 64,\n  fontWeight = 800,\n  color = \"#ffffff\",\n  keywords,\n  markerColor = \"#facc15\",\n  markerTextColor = \"#0a0a0a\",\n  shadow = true,\n  className,\n}: CaptionMarkerProps) {\n  const state = useCaptions({\n    captions,\n    combineTokensWithinMilliseconds,\n    holdMs,\n  });\n  if (!state) return null;\n\n  const isKeyword = createKeywordMatcher(keywords);\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 baseShadow = shadow ? 1 : 0;\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        const key = `${state.pageIndex}-${word[0].index}`;\n        const text = word.map(({ index, token }) => (\n          <span key={index}>{token.text}</span>\n        ));\n        if (!isKeyword(captionWordText(word))) {\n          return (\n            <Fragment key={key}>\n              {w > 0 && \" \"}\n              <span\n                style={{\n                  display: \"inline-block\",\n                  whiteSpace: \"nowrap\",\n                  textShadow: legibilityShadow(baseShadow),\n                }}\n              >\n                {text}\n              </span>\n            </Fragment>\n          );\n        }\n        const fromMs = word[0].token.fromMs;\n        const toMs = word[word.length - 1].token.toMs;\n        const drawMs = Math.max(toMs - fromMs, MIN_DRAW_MS);\n        const drawn = interpolate(state.timeMs - fromMs, [0, drawMs], [0, 1], {\n          ...clamp,\n          easing: drawEasing,\n        });\n        const hidden = `${(1 - drawn) * 100}%`;\n        return (\n          <Fragment key={key}>\n            {w > 0 && \" \"}\n            <span\n              style={{\n                position: \"relative\",\n                display: \"inline-block\",\n                whiteSpace: \"nowrap\",\n                padding: `0 ${PAD_EM}em`,\n                margin: `0 -${PAD_EM}em`,\n              }}\n            >\n              <span\n                aria-hidden\n                style={{\n                  position: \"absolute\",\n                  left: 0,\n                  right: 0,\n                  top: BAND_TOP,\n                  bottom: BAND_BOTTOM,\n                  background: markerColor,\n                  borderRadius: `${RADIUS_EM}em`,\n                  transform: `rotate(${TILT_DEG}deg)`,\n                  clipPath: `inset(0 ${hidden} 0 0 round ${RADIUS_EM}em)`,\n                }}\n              />\n              <span\n                style={{\n                  position: \"relative\",\n                  textShadow: legibilityShadow(baseShadow * (1 - drawn)),\n                }}\n              >\n                {text}\n              </span>\n              <span\n                aria-hidden\n                style={{\n                  position: \"absolute\",\n                  inset: 0,\n                  padding: `0 ${PAD_EM}em`,\n                  color: markerTextColor,\n                  clipPath: `inset(${BAND_TOP} ${hidden} ${BAND_BOTTOM} 0)`,\n                }}\n              >\n                {text}\n              </span>\n            </span>\n          </Fragment>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/caption-marker.tsx"
    }
  ],
  "type": "registry:component"
}
