{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rep-stack",
  "title": "Rep Stack",
  "description": "One caps word is repeated in a stack of outlines and fills, with a blue accent period.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/rep-stack/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/ArchivoNarrow\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: display } = loadFont(\"normal\", {\n  weights: [\"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface RepStackProps {\n  /** One short word, set in caps. Trailing punctuation takes the accent. */\n  word?: string;\n  /** How many rows run through the gate before the last one locks. */\n  reps?: number;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\n// Archivo Narrow 500, per 1000 em, chars 32–126 (harfbuzz): advance, left and right side bearing.\nconst ADV = [\n  228, 242, 322, 456, 456, 729, 561, 169, 273, 273, 319, 479, 228, 273, 228,\n  228, 456, 456, 456, 456, 456, 456, 456, 456, 456, 456, 242, 242, 479, 479,\n  479, 470, 822, 561, 561, 592, 592, 547, 501, 638, 592, 228, 425, 561, 470,\n  683, 592, 638, 547, 638, 592, 547, 501, 592, 547, 774, 547, 547, 501, 242,\n  228, 242, 415, 456, 0, 456, 470, 425, 470, 456, 242, 470, 470, 228, 197, 425,\n  197, 698, 470, 470, 470, 470, 288, 425, 242, 470, 425, 607, 425, 425, 410,\n  288, 218, 288, 479,\n];\nconst LSB = [\n  0, 60, 40, 8, 47, 42, 39, 36, 37, 15, 20, 40, 58, 12, 58, 1, 37, 46, 37, 32,\n  16, 37, 37, 30, 37, 37, 61, 61, 41, 40, 41, 44, 39, -1, 63, 39, 67, 67, 67,\n  44, 67, 64, 12, 67, 67, 59, 67, 44, 67, 44, 67, 38, 18, 67, 5, 7, 3, 5, 19,\n  49, 1, 22, 13, 0, 7, 28, 58, 38, 40, 39, 9, 14, 58, 65, -40, 58, 51, 58, 58,\n  40, 58, 40, 58, 33, 12, 58, 8, 1, 6, 8, 23, 21, 68, 20, 32,\n];\nconst RSB = [\n  228, 56, 40, 11, 50, 35, 11, 39, 15, 37, 21, 41, 58, 13, 58, 0, 37, 8, 40, 37,\n  16, 30, 37, 30, 37, 37, 69, 69, 40, 41, 40, 47, 39, -1, 30, 38, 44, 47, 36,\n  61, 67, 64, 64, 9, 19, 59, 67, 44, 27, 44, 23, 41, 19, 67, 5, 7, 3, 5, 22, 21,\n  0, 49, 10, 1, -193, 12, 40, 34, 58, 40, 0, 9, 58, 66, 51, -1, 52, 58, 58, 40,\n  40, 58, 6, 39, 15, 58, 8, 2, 6, 8, 24, 20, 68, 21, 32,\n];\nconst KERN = new Map(\n  [\n    \"11-55 AT-60 AV-60 AW-35 AY-65 Av-19 Aw-14 Ay-19 F,-91 F.-91 FA-45 F_-91\",\n    \"Fa-40 Fc-40 Fe-40 Fo-40 Kg-20 LT-60 LV-60 LW-55 LY-65 Ly-30 P,-105\",\n    \"P.-105 PA-60 P_-105 RT-10 RV-14 RW-14 RY-19 T,-91 T--45 T.-91 T:-91\",\n    \"T;-91 TA-60 TO-14 T_-91 Ta-70 Tc-81 Te-81 Ti-17 To-81 Tr-35 Ts-67 Tu-67\",\n    \"Tw-50 Tx-40 Ty-50 Tz-40 V,-75 V--45 V.-75 V:-35 V;-35 VA-60 V_-75 Va-55\",\n    \"Vc-50 Ve-50 Vi-14 Vo-50 Vr-35 Vu-30 Vy-30 W,-45 W--15 W.-45 W:-14 W;-14\",\n    \"WA-35 W_-45 Wa-30 Wc-14 We-14 Wo-14 Wr-14 Wu-14 Wy-9 Xa-7 Xc-23 Xe-23\",\n    \"Xo-23 Y,-101 Y--66 Y.-101 Y:-50 Y;-55 YA-65 Y_-101 Ya-55 Yc-63 Ye-63\",\n    \"Yg-49 Yi-30 Yo-63 Yp-55 Yq-70 Ys-63 Yt-20 Yu-45 Yv-45 r,-45 r.-45 r_-45\",\n    \"v,-60 v.-60 v_-60 w,-40 w.-40 w_-40 y,-60 y.-60 y_-60\",\n  ]\n    .join(\" \")\n    .split(\" \")\n    .map((p) => [p.slice(0, 2), Number(p.slice(2)) / 1000] as const),\n);\nconst CAP = 0.686;\nconst TRACK = -0.006;\n\n// Beat sheet (30fps, speed 1):\n//  0–42    a reel of outlined copies of the word runs upward, braking; a\n//          cap-height gate across the middle of the frame is a band of dusk\n//          light, and letters crossing it are punched out of the band\n//  42–50   the last copy overshoots the gate by a hair and settles into it:\n//          the word stands cut out of the dusk, its punctuation in the accent\n//  48–60   the outlined echoes above and below dim back\n//  60–     poster hold\nconst RUN_LEN = 42;\nconst SETTLE_LEN = 8;\nconst OVERSHOOT = 0.03;\nconst DIM_AT = 48;\nconst DIM_LEN = 12;\n\nconst RUN = Easing.bezier(0.2, 0.52, 0.3, 1);\nconst SETTLE = Easing.bezier(0.45, 0, 0.55, 1);\nconst DIM = Easing.bezier(0.33, 0, 0.2, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst THEMES = {\n  dark: { stage: \"#0A0A0B\", ink: \"#F3F2EF\", cut: \"#0A0A0B\", echo: 0.36 },\n  light: { stage: \"#F5F4F0\", ink: \"#121214\", cut: \"#121214\", echo: 0.3 },\n} as const;\n\n// gradients.sh \"horizon\" (bg #000000): a dusk band, cool sky over a warm\n// horizon. It fills the gate, the letters crossing it read as cutouts. The\n// band holds still; only its warm horizon glow drifts a few percent sideways.\nconst HORIZON = [\"#dfe8ff\", \"#6f8fb0\", \"#c98f8a\", \"#b4553a\"] as const;\n\nfunction code(ch: string) {\n  const c = ch.charCodeAt(0);\n  if (c >= 32 && c < 127) return c - 32;\n  if (ch === \"’\" || ch === \"‘\") return 7;\n  return 46;\n}\n\n/** Advance and ink box of a word, in em, with tracking. */\nfunction measure(text: string) {\n  let adv = 0;\n  for (let i = 0; i < text.length; i++) {\n    adv += ADV[code(text[i])] / 1000;\n    if (i > 0) adv += (KERN.get(text[i - 1] + text[i]) ?? 0) + TRACK;\n  }\n  const lsb = LSB[code(text[0] ?? \" \")] / 1000;\n  const rsb = RSB[code(text[text.length - 1] ?? \" \")] / 1000;\n  return { lsb, ink: Math.max(0.2, adv - lsb - rsb) };\n}\n\nfunction splitTail(word: string) {\n  const m = word.match(/^(.*?)([.!?…]+)$/);\n  return m?.[1] ? [m[1], m[2]] : [word, \"\"];\n}\n\nexport function RepStack({\n  word = \"AGAIN.\",\n  reps = 6,\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: RepStackProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n  const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n  const gateId = `rs${uid}`;\n  const duskId = `rsd${uid}`;\n  const glowId = `rsg${uid}`;\n\n  const text = word.trim().toUpperCase() || \"AGAIN.\";\n  const [body, tail] = splitTail(text);\n  const m = measure(text);\n  const fs = Math.min((0.86 * W) / m.ink, (0.46 * H) / CAP);\n  const cap = CAP * fs;\n  const pitch = cap * 1.1;\n  const x = (W - m.ink * fs) / 2 - m.lsb * fs;\n  const baseline = (H + cap) / 2;\n  const n = Math.max(2, Math.round(reps));\n\n  const run = RUN(interpolate(t, [0, RUN_LEN], [0, 1], CLAMP));\n  const settle = SETTLE(\n    interpolate(t, [RUN_LEN, RUN_LEN + SETTLE_LEN], [0, 1], CLAMP),\n  );\n  // Rows still to travel, in pitches. It starts between two rows, so the\n  // first frame is mid-pass, ends a hair past the gate, then settles.\n  const left = (n + 0.45) * (1 - run) - OVERSHOOT * run + OVERSHOOT * settle;\n  const dim = DIM(interpolate(t, [DIM_AT, DIM_AT + DIM_LEN], [0, 1], CLAMP));\n  const echoOpacity = 1 - (1 - c.echo) * dim;\n\n  const pad = 0.03 * fs;\n  const gateTop = baseline - cap - pad;\n  const gateH = cap + 2 * pad;\n  const glowX = interpolate(t, [0, 90], [0.38, 0.46], CLAMP);\n  const stroke = Math.max(1.5, fs * 0.0055);\n  const rows: number[] = [];\n  for (let r = -n - 2; r <= 2; r++) {\n    const y = baseline + (r + left) * pitch;\n    if (y > -0.2 * pitch && y - cap < H + 0.2 * pitch) rows.push(r);\n  }\n\n  const typeProps = {\n    x,\n    fontFamily: display,\n    fontWeight: 500,\n    fontSize: fs,\n    style: { letterSpacing: `${TRACK}em`, fontKerning: \"normal\" as const },\n  };\n\n  return (\n    <AbsoluteFill style={{ background: c.stage }}>\n      <svg\n        width={W}\n        height={H}\n        viewBox={`0 0 ${W} ${H}`}\n        style={{ position: \"absolute\", inset: 0 }}\n        aria-label={text}\n        role=\"img\"\n      >\n        <defs>\n          <clipPath id={gateId}>\n            <rect x={0} y={gateTop} width={W} height={gateH} />\n          </clipPath>\n          <linearGradient\n            id={duskId}\n            gradientUnits=\"userSpaceOnUse\"\n            x1={0}\n            y1={gateTop}\n            x2={0}\n            y2={gateTop + gateH}\n          >\n            <stop offset=\"0\" stopColor={HORIZON[1]} />\n            <stop offset=\"0.55\" stopColor={HORIZON[2]} />\n            <stop offset=\"0.92\" stopColor={HORIZON[3]} />\n          </linearGradient>\n          <radialGradient\n            id={glowId}\n            gradientUnits=\"userSpaceOnUse\"\n            cx={glowX * W}\n            cy={gateTop + gateH * 0.78}\n            r={W * 0.42}\n            gradientTransform={`translate(${glowX * W} ${gateTop + gateH * 0.78}) scale(1 0.32) translate(${-glowX * W} ${-(gateTop + gateH * 0.78)})`}\n          >\n            <stop offset=\"0\" stopColor={HORIZON[0]} stopOpacity={0.4} />\n            <stop offset=\"0.45\" stopColor={HORIZON[2]} stopOpacity={0.25} />\n            <stop offset=\"1\" stopColor={HORIZON[2]} stopOpacity={0} />\n          </radialGradient>\n        </defs>\n        <g opacity={echoOpacity}>\n          {rows.map((r) => (\n            <text\n              key={r}\n              {...typeProps}\n              y={baseline + (r + left) * pitch}\n              fill=\"none\"\n              stroke={c.ink}\n              strokeWidth={stroke}\n              strokeLinejoin=\"round\"\n            >\n              {text}\n            </text>\n          ))}\n        </g>\n        <rect\n          x={0}\n          y={gateTop}\n          width={W}\n          height={gateH}\n          fill={`url(#${duskId})`}\n        />\n        <rect\n          x={0}\n          y={gateTop}\n          width={W}\n          height={gateH}\n          fill={`url(#${glowId})`}\n        />\n        <g clipPath={`url(#${gateId})`}>\n          {rows.map((r) => (\n            <text\n              key={r}\n              {...typeProps}\n              y={baseline + (r + left) * pitch}\n              fill={c.cut}\n              stroke={c.cut}\n              strokeWidth={stroke}\n              strokeLinejoin=\"round\"\n            >\n              {body}\n              {tail ? (\n                <tspan fill={accent} stroke={accent}>\n                  {tail}\n                </tspan>\n              ) : null}\n            </text>\n          ))}\n        </g>\n      </svg>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/rep-stack.tsx"
    }
  ],
  "type": "registry:component"
}
