{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ring-align",
  "title": "Ring Align",
  "description": "Concentric dotted rings rotate until their slots align and reveal a word in the channel.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/ring-align/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadDisplay } from \"@remotion/google-fonts/Geist\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: display } = loadDisplay(\"normal\", {\n  weights: [\"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface RingAlignProps {\n  /** Word revealed in the channel once every slot lines up. */\n  word?: string;\n  /** Number of concentric rings. */\n  rings?: number;\n  accent?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst FIRST_LOCK = 30;\nconst LOCK_STEP = 8;\nconst BEAM_GAP = 4;\nconst BEAM = 14;\nconst TAIL_DELAY = 8;\nconst TAIL = 16;\nconst HOLD = 32;\n\nexport const ringAlignLength = (rings = 6) =>\n  FIRST_LOCK + (rings - 1) * LOCK_STEP + BEAM_GAP + TAIL_DELAY + TAIL + HOLD;\n\nconst LOCK = Easing.bezier(0.3, 0.55, 0.32, 1.05);\nconst WIPE = Easing.bezier(0.22, 1, 0.36, 1);\nconst TAIL_EASE = Easing.bezier(0.55, 0, 0.25, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nfunction hash(n: number): number {\n  const s = Math.sin(n * 127.1 + 311.7) * 43758.5453;\n  return s - Math.floor(s);\n}\n\nfunction circle(x: number, y: number, r: number) {\n  const rs = r.toFixed(2);\n  const d = (2 * r).toFixed(2);\n  return `M${(x - r).toFixed(1)} ${y.toFixed(1)}a${rs} ${rs} 0 1 0 ${d} 0a${rs} ${rs} 0 1 0 -${d} 0`;\n}\n\nfunction wrap(a: number) {\n  let v = a % (Math.PI * 2);\n  if (v > Math.PI) v -= Math.PI * 2;\n  if (v < -Math.PI) v += Math.PI * 2;\n  return v;\n}\n\n/* gradients.sh \"radial-glow\" palette: the beam that runs the channel, and\n   the fill the word keeps once the beam has passed. */\nconst GLOW = [\"#f3f1ff\", \"#9db4ff\", \"#5a46cb\", \"#1d1240\"] as const;\n\nfunction Grain({ opacity, light }: { opacity: number; light: boolean }) {\n  const uid = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      style={{ position: \"absolute\", inset: 0, pointerEvents: \"none\" }}\n      aria-hidden\n    >\n      <filter id={`${uid}-grain`}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency={0.85}\n          numOctaves={2}\n          seed={3}\n        />\n        <feColorMatrix\n          type=\"matrix\"\n          values=\"0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 1 0\"\n        />\n      </filter>\n      <rect\n        width=\"100%\"\n        height=\"100%\"\n        filter={`url(#${uid}-grain)`}\n        opacity={opacity}\n        style={{ mixBlendMode: light ? \"multiply\" : \"screen\" }}\n      />\n    </svg>\n  );\n}\n\nexport function RingAlign({\n  word = \"Aligned.\",\n  rings = 6,\n  accent = \"#3B5BFD\",\n  speed = 1,\n  theme = \"dark\",\n}: RingAlignProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const isLight = theme === \"light\";\n  const bg = isLight ? \"#F5F4F0\" : \"#0A0A0B\";\n  const ink = isLight ? \"#141416\" : \"#EEEEF0\";\n  const u = Math.min(W, H) / 720;\n  const n = Math.max(3, Math.min(9, Math.round(rings)));\n  const cx = W / 2;\n  const cy = H / 2;\n\n  const band = H * 0.175;\n  const pad = 9 * u;\n  const rIn = H * 0.165;\n  const rOut = H * 0.44;\n  const dotR = 3.1 * u;\n  const pitch = 17 * u;\n\n  const lastLock = FIRST_LOCK + (n - 1) * LOCK_STEP;\n  const beamStart = lastLock + BEAM_GAP;\n\n  const ringPaths: { d: string; opacity: number }[] = [];\n  for (let k = 0; k < n; k++) {\n    // Outer ring locks first, the innermost last.\n    const r = rOut - ((rOut - rIn) * k) / (n - 1);\n    const count = Math.round((Math.PI * 2 * r) / pitch);\n    const gap = 2 * Math.asin(Math.min(0.98, (band / 2 + pad) / r));\n    const lockAt = FIRST_LOCK + k * LOCK_STEP;\n    const dir = k % 2 === 0 ? 1 : -1;\n    const offset = dir * Math.PI * (1.35 + hash(k * 4.1) * 0.5);\n    const x = Math.min(1, Math.max(0, t / lockAt));\n    let rot = offset * (1 - LOCK(x));\n    const since = t - lockAt;\n    if (since > 0) {\n      rot +=\n        -dir * 0.012 * Math.sin((since / 7) * Math.PI) * Math.exp(-since / 4);\n    }\n    const lit = interpolate(since, [-2, 6], [0.42, 1], CLAMP);\n    const segs: string[] = [];\n    for (let j = 0; j < count; j++) {\n      const a = (j / count) * Math.PI * 2 + rot;\n      const local = wrap((j / count) * Math.PI * 2);\n      const toSlot = Math.min(Math.abs(local), Math.abs(wrap(local - Math.PI)));\n      if (toSlot < gap / 2) continue;\n      segs.push(circle(cx + Math.cos(a) * r, cy + Math.sin(a) * r, dotR));\n    }\n    ringPaths.push({ d: segs.join(\"\"), opacity: lit });\n  }\n\n  const head =\n    W * WIPE(interpolate(t, [beamStart, beamStart + BEAM], [0, 1], CLAMP));\n  const tail =\n    W *\n    TAIL_EASE(\n      interpolate(\n        t,\n        [beamStart + TAIL_DELAY, beamStart + TAIL_DELAY + TAIL],\n        [0, 1],\n        CLAMP,\n      ),\n    );\n  const fontSize = band * 0.72;\n  const dot = word.endsWith(\".\");\n  const body = dot ? word.slice(0, -1) : word;\n  const [g0, g1, g2, g3] = GLOW;\n  const drift = interpolate(t, [beamStart, ringAlignLength(n)], [0, 1], CLAMP);\n  const wordFill = isLight\n    ? `linear-gradient(90deg, ${g2} ${(-10 + 20 * drift).toFixed(1)}%, ${g3} ${(45 + 10 * drift).toFixed(1)}%, ${g2} ${(110 + 10 * drift).toFixed(1)}%)`\n    : `linear-gradient(90deg, ${g1} ${(-10 + 20 * drift).toFixed(1)}%, ${g0} ${(45 + 10 * drift).toFixed(1)}%, ${g1} ${(110 + 10 * drift).toFixed(1)}%)`;\n  const beamFill = `linear-gradient(90deg, ${g3} 0%, ${g2} 45%, color-mix(in oklab, ${g2} 55%, ${g1}) 88%, ${g1} 98%, ${g0} 100%)`;\n  const wordLayer = (fill: string, l: number, r: number) => (\n    <div\n      style={{\n        position: \"absolute\",\n        left: 0,\n        top: cy - band / 2,\n        width: W,\n        height: band,\n        clipPath: `inset(0 ${(W - r).toFixed(1)}px 0 ${l.toFixed(1)}px)`,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      <div\n        style={{\n          fontFamily: display,\n          fontWeight: 500,\n          fontSize,\n          letterSpacing: \"-0.035em\",\n          lineHeight: 1.1,\n          whiteSpace: \"nowrap\",\n          paddingBottom: fontSize * 0.04,\n          paddingRight: fontSize * 0.04,\n          backgroundImage: fill,\n          WebkitBackgroundClip: \"text\",\n          backgroundClip: \"text\",\n          color: \"transparent\",\n        }}\n      >\n        {body}\n        {dot ? (\n          <span\n            style={{\n              WebkitTextFillColor: fill.includes(\"#FFFFFF\")\n                ? \"#FFFFFF\"\n                : accent,\n            }}\n          >\n            .\n          </span>\n        ) : null}\n      </div>\n    </div>\n  );\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: bg,\n        backgroundImage: isLight\n          ? `radial-gradient(34% 58% at 50% 50%, color-mix(in oklab, ${GLOW[1]} 16%, transparent) 0%, transparent 100%), radial-gradient(70% 70% at 50% 50%, #FBFAF7 0%, ${bg} 72%)`\n          : `radial-gradient(34% 58% at 50% 50%, color-mix(in oklab, ${GLOW[2]} 22%, transparent) 0%, transparent 100%), radial-gradient(70% 70% at 50% 50%, #121215 0%, ${bg} 72%)`,\n        overflow: \"hidden\",\n      }}\n    >\n      <svg\n        width={W}\n        height={H}\n        viewBox={`0 0 ${W} ${H}`}\n        style={{ position: \"absolute\", inset: 0 }}\n        aria-hidden\n      >\n        {ringPaths.map((p, k) => (\n          <path\n            key={`ring-${k.toString()}`}\n            d={p.d}\n            fill={ink}\n            opacity={p.opacity}\n          />\n        ))}\n      </svg>\n      {tail > 0 ? wordLayer(wordFill, 0, tail) : null}\n      {head > tail + 0.5 ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: tail,\n            top: cy - band / 2,\n            width: head - tail,\n            height: band,\n            backgroundImage: beamFill,\n          }}\n        />\n      ) : null}\n      {head > tail + 0.5\n        ? wordLayer(\"linear-gradient(#FFFFFF, #FFFFFF)\", tail, head)\n        : null}\n      <Grain opacity={isLight ? 0.045 : 0.06} light={isLight} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/ring-align.tsx"
    }
  ],
  "type": "registry:component"
}
