{
  "name": "marker-highlight",
  "type": "registry:ui",
  "description": "Highlighter stroke swept word by word across a phrase, with marker, knockout, underline and box variants",
  "dependencies": [
    "remotion"
  ],
  "registryDependencies": [
    "layout",
    "motion-tokens",
    "text-emphasis"
  ],
  "files": [
    {
      "path": "registry/bases/default/primitives/marker-highlight.tsx",
      "type": "registry:ui",
      "content": "import { interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { scaleFont } from \"@/remotion/lib/layout\";\nimport { EASING } from \"@/remotion/lib/motion-tokens\";\nimport { markEmphasis, markerEdges } from \"@/remotion/lib/text-emphasis\";\n\nexport type MarkerVariant = \"marker\" | \"knockout\" | \"underline\" | \"box\";\n\nexport type MarkerHighlightProps = {\n  text: string;\n  /** Phrase to sweep. Matched case-insensitively, may span several words. */\n  phrase?: string;\n  /** Single-word form of `phrase`. */\n  highlightWord?: string;\n  /** How the emphasis is drawn. */\n  variant?: MarkerVariant;\n  /** Length of the sweep across one word. */\n  durationInFrames?: number;\n  delayInFrames?: number;\n  /** Frames between one word being struck and the next. */\n  staggerInFrames?: number;\n  /** Tilt of the stroke in degrees — a hand does not draw level. */\n  tilt?: number;\n  color?: string;\n  markerColor?: string;\n  /** Ink over a covered word. Defaults on for `knockout`. */\n  invertOnHighlight?: boolean;\n  inkColor?: string;\n  fontSize?: number;\n  fontWeight?: number;\n  fontFamily?: string;\n  textAlign?: \"left\" | \"center\" | \"right\";\n  style?: React.CSSProperties;\n};\n\nconst WORD_GAP = \"0.28em\";\n\n/** Band opacity per variant — a real marker is translucent, a knockout is not. */\nconst BAND_OPACITY: Record<MarkerVariant, number> = {\n  marker: 0.42,\n  knockout: 1,\n  underline: 1,\n  box: 1,\n};\n\nconst clamp = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\n/**\n * Strikes emphasis across a phrase, word by word.\n *\n * Word by word matters: one band appearing behind the whole phrase reads as a\n * background, while a stroke that crosses each word in turn reads as a hand\n * moving. The ink flips under the leading edge of the stroke rather than at a\n * threshold, so the covered text is never briefly unreadable.\n */\nexport const MarkerHighlight: React.FC<MarkerHighlightProps> = ({\n  text,\n  phrase,\n  highlightWord,\n  variant = \"marker\",\n  durationInFrames = 12,\n  delayInFrames = 0,\n  staggerInFrames = 4,\n  tilt = -0.7,\n  color = \"#f8fafc\",\n  markerColor = \"#fbbf24\",\n  invertOnHighlight,\n  inkColor = \"#080810\",\n  fontSize: fontSizeProp,\n  fontWeight = 600,\n  fontFamily,\n  textAlign = \"left\",\n  style,\n}) => {\n  const frame = useCurrentFrame();\n  const { width } = useVideoConfig();\n  const fontSize = fontSizeProp ?? scaleFont(84, width);\n  const words = markEmphasis(text, phrase ?? highlightWord);\n  const inverts = invertOnHighlight ?? variant === \"knockout\";\n  const overhang = fontSize * 0.07;\n\n  return (\n    <span\n      style={{\n        display: \"flex\",\n        flexWrap: \"wrap\",\n        gap: `0.14em ${WORD_GAP}`,\n        justifyContent:\n          textAlign === \"center\"\n            ? \"center\"\n            : textAlign === \"right\"\n              ? \"flex-end\"\n              : \"flex-start\",\n        color,\n        fontSize,\n        fontWeight,\n        lineHeight: 1.3,\n        ...(fontFamily !== undefined ? { fontFamily } : {}),\n        ...style,\n      }}\n    >\n      {words.map((word, index) => {\n        if (!word.marked) {\n          return <span key={`w-${index}`}>{word.text}</span>;\n        }\n\n        const start = delayInFrames + word.order * staggerInFrames;\n        const sweep = interpolate(\n          frame,\n          [start, start + durationInFrames],\n          [0, 1],\n          { easing: EASING.editorial, ...clamp },\n        );\n        const edges = markerEdges(words, index, WORD_GAP, overhang, overhang);\n        const band = bandBox(variant);\n\n        return (\n          <span\n            key={`w-${index}`}\n            style={{ position: \"relative\", whiteSpace: \"nowrap\" }}\n          >\n            <span\n              style={{\n                position: \"absolute\",\n                left: edges.left,\n                right: edges.right,\n                ...band,\n                background: markerColor,\n                opacity: BAND_OPACITY[variant],\n                ...(variant === \"box\"\n                  ? {\n                      background: \"transparent\",\n                      border: `${Math.max(2, fontSize * 0.035)}px solid ${markerColor}`,\n                    }\n                  : {}),\n                borderTopLeftRadius: edges.borderTopLeftRadius,\n                borderBottomLeftRadius: edges.borderBottomLeftRadius,\n                borderTopRightRadius: edges.borderTopRightRadius,\n                borderBottomRightRadius: edges.borderBottomRightRadius,\n                transformOrigin: \"left center\",\n                transform: `rotate(${tilt}deg) scaleX(${sweep})`,\n              }}\n            />\n            <span style={{ position: \"relative\" }}>{word.text}</span>\n            {inverts ? (\n              /* Same glyphs in the inverted ink, revealed exactly under the\n               * leading edge of the stroke. */\n              <span\n                style={{\n                  position: \"absolute\",\n                  left: 0,\n                  top: 0,\n                  color: inkColor,\n                  clipPath: `inset(0 ${(1 - sweep) * 100}% 0 0)`,\n                }}\n              >\n                {word.text}\n              </span>\n            ) : null}\n          </span>\n        );\n      })}\n    </span>\n  );\n};\n\n/** Vertical extent of the stroke for each variant. */\nfunction bandBox(variant: MarkerVariant): React.CSSProperties {\n  if (variant === \"underline\") {\n    return { bottom: \"-0.02em\", height: \"0.12em\" };\n  }\n  if (variant === \"box\") {\n    return { top: \"-0.06em\", bottom: \"-0.02em\" };\n  }\n  return { top: \"0.08em\", bottom: \"0.04em\" };\n}\n"
    }
  ],
  "atlas": {
    "lane": "atoms",
    "drive": "time",
    "tier": "core",
    "tags": [
      "text"
    ]
  }
}