{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "strobe-words",
  "title": "Strobe Words",
  "description": "Big words strobe in at a set rhythm over a dark horizon glow, ending on the logo.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/strobe-words/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\nimport { loadFont as loadDisplay } from \"@remotion/google-fonts/Sora\";\nimport type React from \"react\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst manrope = loadManrope(\"normal\", {\n  weights: [\"500\", \"600\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: display } = loadDisplay(\"normal\", {\n  weights: [\"500\"],\n  subsets: [\"latin\"],\n});\n\n// gradients.sh \"radial-glow\" palette (bg #020204): #f3f1ff, #9db4ff, #5a46cb, #1d1240.\nconst GLOW = [\"#f3f1ff\", \"#9db4ff\", \"#5a46cb\", \"#1d1240\", \"#020204\"] as const;\n\n// Sora 500 advances (1/1000 em), ASCII 32–126, measured with HarfBuzz.\nconst SORA_500 = [\n  222, 303, 452, 702, 652, 864, 702, 264, 382, 382, 568, 592, 266, 506, 266,\n  365, 750, 423, 623, 620, 652, 630, 668, 585, 647, 668, 266, 266, 592, 590,\n  592, 556, 1124, 765, 684, 797, 788, 598, 560, 832, 803, 320, 641, 703, 549,\n  937, 858, 865, 649, 865, 710, 682, 602, 785, 719, 1052, 705, 654, 652, 382,\n  365, 382, 587, 581, 300, 581, 694, 608, 694, 616, 380, 679, 644, 317, 324,\n  595, 296, 973, 644, 678, 694, 694, 414, 543, 429, 634, 572, 873, 574, 555,\n  493, 382, 375, 382, 511,\n];\nconst TRACK = -0.045;\n\nconst REMOCN_R_PATH =\n  \"M 0.01 0.81 C 0.01 1.73, 0.36 2.79, 1.09 4.13 C 4.91 11.04, 13.45 17.16, 21.7 18.9 C 22.94 19.16, 23.18 19.16, 51.39 19.27 C 76.07 19.36, 80.02 19.4, 81.32 19.57 C 89.89 20.69, 96.2 24.68, 99.38 31.01 C 103.19 38.56, 102.53 50.1, 97.91 57.07 C 94.66 61.96, 89.68 64.99, 83.26 66 C 82.81 66.07, 70.18 66.83, 55.2 67.69 C 24.82 69.43, 27.03 69.23, 24.18 70.4 C 19.9 72.15, 14.84 75.7, 10.65 79.89 C 4.86 85.68, 1.3 91.91, 0.25 98.13 C 0.12 98.85, 0.08 103.13, 0.04 116.66 L 0 134.26 9.5 134.26 L 19 134.26 19.05 119.41 C 19.1 103.27, 19.08 103.82, 19.82 101.04 C 21.79 93.65, 27.86 88.75, 36.45 87.63 C 37.23 87.53, 39.41 87.5, 43.57 87.53 C 50.12 87.59, 50.75 87.65, 53.22 88.45 C 56.61 89.56, 59.67 91.86, 62.02 95.07 C 62.52 95.76, 69.35 103.85, 77.2 113.07 C 85.04 122.28, 91.63 130.04, 91.85 130.32 C 92.07 130.59, 92.5 131.34, 92.82 131.97 C 93.52 133.35, 94.11 133.93, 95.06 134.13 C 95.5 134.23, 98.97 134.26, 106.36 134.23 L 117.01 134.19 100.82 113.07 C 91.91 101.45, 84.52 91.78, 84.39 91.57 C 83.36 89.89, 83.66 87.53, 85.09 86 C 85.79 85.25, 86.36 84.94, 88.07 84.38 C 96.18 81.72, 104.15 76.62, 109.97 70.36 C 120.59 58.93, 124.06 44.32, 119.44 30.43 C 114.59 15.81, 101.93 4.02, 87.64 0.83 C 83.93 0.01, 88.09 0.08, 41.85 0.04 L 0.01 0 0.01 0.81 Z\";\n\n/** Full logo as on remocn.dev: R mark (0.76em tall, baseline-aligned) + \"emocn\" in Manrope 600, tracking-tight. */\nfunction RemocnLogo({ fontSize, color }: { fontSize: number; color: string }) {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"baseline\",\n        color,\n        fontFamily: manrope.fontFamily,\n        fontWeight: 600,\n        fontSize,\n        letterSpacing: \"-0.025em\",\n        lineHeight: 1,\n      }}\n    >\n      <svg\n        viewBox=\"0 0 124.06 134.26\"\n        style={{ height: \"0.76em\", width: \"auto\" }}\n        aria-hidden\n      >\n        <path fill=\"currentColor\" d={REMOCN_R_PATH} />\n      </svg>\n      <span style={{ marginLeft: \"0.04em\" }}>emocn</span>\n    </div>\n  );\n}\n\nexport interface StrobeWordsProps {\n  words?: string[];\n  rhythm?: number[];\n  logo?: React.ReactNode;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\nconst CAP = 0.73;\nconst BASE = 0.84;\nconst INK = \"#0A0A0B\";\nconst PAPER = \"#F5F4F0\";\nconst PAPER_INK = \"#121214\";\n\ntype Field = \"accent\" | \"ink\" | \"paper\";\nconst CYCLE: Field[] = [\"accent\", \"ink\", \"paper\"];\n\nfunction widthEm(text: string) {\n  let w = 0;\n  for (const ch of text) {\n    const code = ch.charCodeAt(0);\n    const adv = code >= 32 && code < 127 ? SORA_500[code - 32] : 600;\n    w += adv / 1000 + TRACK;\n  }\n  return Math.max(w, 0.5);\n}\n\nfunction defaultRhythm(count: number) {\n  let n = count - 1;\n  while (n < 13) n += count;\n  return Array.from({ length: n }, (_, i) =>\n    Math.round(6 - (3 * i) / Math.max(1, n - 1)),\n  );\n}\n\nfunction Grain({ opacity }: { opacity: number }) {\n  const id = `g${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      style={{ position: \"absolute\", inset: 0, opacity, pointerEvents: \"none\" }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.9\"\n          numOctaves={2}\n          seed={3}\n          stitchTiles=\"stitch\"\n        />\n        <feColorMatrix values=\"0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0.9 0\" />\n      </filter>\n      <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n    </svg>\n  );\n}\n\nfunction Word({\n  text,\n  size,\n  color,\n  stop,\n  fill,\n}: {\n  text: string;\n  size: number;\n  color: string;\n  stop: string;\n  fill?: string;\n}) {\n  const { height: H } = useVideoConfig();\n  const baseline = H / 2 + (CAP * size) / 2;\n  const tail = text.match(/[.!?]$/) ? text.slice(-1) : \"\";\n  const body = tail ? text.slice(0, -1) : text;\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: 0,\n        right: 0,\n        top: baseline - BASE * size,\n        display: \"flex\",\n        justifyContent: \"center\",\n      }}\n    >\n      <div\n        style={{\n          fontFamily: display,\n          fontWeight: 500,\n          fontSize: size,\n          lineHeight: 1,\n          letterSpacing: `${TRACK}em`,\n          paddingLeft: `${-TRACK}em`,\n          whiteSpace: \"nowrap\",\n          color,\n        }}\n      >\n        {fill ? (\n          <span\n            style={{\n              color: \"transparent\",\n              backgroundImage: fill,\n              WebkitBackgroundClip: \"text\",\n              backgroundClip: \"text\",\n              paddingRight: \"0.08em\",\n              marginRight: \"-0.08em\",\n            }}\n          >\n            {body}\n          </span>\n        ) : (\n          body\n        )}\n        {tail ? <span style={{ color: stop }}>{tail}</span> : null}\n      </div>\n    </div>\n  );\n}\n\nexport function StrobeWords({\n  words = [\"Plan.\", \"Write.\", \"Render.\", \"Ship.\"],\n  rhythm,\n  logo,\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: StrobeWordsProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const u = H / 720;\n  const list = words.length > 0 ? words : [\"Ship.\"];\n  const beats =\n    rhythm && rhythm.length > 0 ? rhythm : defaultRhythm(list.length);\n  const poster: Field = theme === \"light\" ? \"paper\" : \"ink\";\n\n  const offset = (((CYCLE.indexOf(\"accent\") - (beats.length - 1)) % 3) + 3) % 3;\n\n  let index = beats.length;\n  let at = 0;\n  for (let i = 0; i < beats.length; i++) {\n    if (t < at + beats[i]) {\n      index = i;\n      break;\n    }\n    at += beats[i];\n  }\n  const isPoster = index >= beats.length;\n  const field: Field = isPoster ? poster : CYCLE[(index + offset) % 3];\n  const text = isPoster\n    ? list[list.length - 1]\n    : list[\n        (((index - beats.length + list.length - 1) % list.length) +\n          list.length) %\n          list.length\n      ];\n\n  // Each strobe beat on the accent field is a radial-glow ground; the disc\n  // sits in a slightly different place per beat so the cuts read as light.\n  const k = isPoster ? 0 : index;\n  const bx = 50 + ((k * 37) % 5) * 5 - 10;\n  const glowField = [\n    `radial-gradient(52% 64% at ${bx}% 104%, ${GLOW[0]} 0%, color-mix(in oklab, ${GLOW[1]} 85%, transparent) 30%, transparent 72%)`,\n    `radial-gradient(90% 90% at ${100 - bx}% 0%, color-mix(in oklab, ${GLOW[3]} 80%, transparent) 0%, transparent 70%)`,\n    `linear-gradient(180deg, ${GLOW[3]} 0%, ${GLOW[2]} 62%, #7e6fe0 100%)`,\n  ].join(\", \");\n  const rise = interpolate(t - at, [0, 60], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.bezier(0.22, 1, 0.36, 1),\n  });\n  // Poster: the glow's ring reads as a horizon arc low in frame, rising a\n  // few percent over the hold, under the word rather than around it.\n  const posterGlow = [\n    `radial-gradient(78% 62% at 50% ${142 - 6 * rise}%, color-mix(in oklab, ${GLOW[3]} 90%, transparent) 60%, color-mix(in oklab, ${GLOW[0]} 80%, transparent) 63%, color-mix(in oklab, ${GLOW[1]} 40%, transparent) 67%, transparent 80%)`,\n    `radial-gradient(90% 60% at 50% 120%, color-mix(in oklab, ${GLOW[2]} 45%, transparent) 0%, transparent 70%)`,\n    GLOW[4],\n  ].join(\", \");\n  const bg =\n    field === \"accent\"\n      ? glowField\n      : field === \"ink\"\n        ? isPoster\n          ? posterGlow\n          : INK\n        : PAPER;\n  const fg = field === \"paper\" ? PAPER_INK : \"#F7F7F5\";\n  const stop = field === \"accent\" ? GLOW[0] : accent;\n  const posterFill =\n    isPoster && field === \"paper\"\n      ? `linear-gradient(180deg, ${GLOW[2]} 10%, ${GLOW[3]} 92%)`\n      : undefined;\n\n  const longest = list.reduce((m, w) => Math.max(m, widthEm(w)), 0.5);\n  const strobeSize = Math.min(290 * u, (0.74 * W) / longest);\n  const final = list[list.length - 1];\n  const posterSize = Math.min(340 * u, (0.8 * W) / widthEm(final));\n\n  return (\n    <AbsoluteFill style={{ background: bg, overflow: \"hidden\" }}>\n      <Word\n        text={text}\n        size={isPoster ? posterSize : strobeSize}\n        color={fg}\n        stop={stop}\n        fill={posterFill}\n      />\n      {isPoster ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            right: 0,\n            bottom: 0.085 * H,\n            display: \"flex\",\n            justifyContent: \"center\",\n            alignItems: \"center\",\n            gap: 10 * u,\n            color: fg,\n          }}\n        >\n          {logo ?? <RemocnLogo fontSize={28 * u} color={fg} />}\n        </div>\n      ) : null}\n      <Grain opacity={field === \"paper\" ? 0.04 : 0.05} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/strobe-words.tsx"
    }
  ],
  "type": "registry:component"
}
