{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dial-moire",
  "title": "Dial Moire",
  "description": "Weighted rotary knob tuned like a radio: counter-rotating line gratings shimmer with moire interference, and a hidden word only resolves at the detent.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/dial-moire/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// MoireDial — a weighted rotary knob whose feedback channel is optical\n// interference. Two fine-line gratings counter-rotate as you drag; grating A\n// is fixed but carries a hidden message as a locally phase-inverted region\n// (glyphs rasterized offscreen to an alpha mask, half-pitch phase offset\n// inside), so the word only gains contrast when the overlay grating aligns\n// and the dial clicks into its detent. Canvas 2D, DPR clamp 2, direct-DOM\n// rAF is the sole writer, loop sleeps when settled. Zero deps.\n\nconst DEG = Math.PI / 180;\nconst DETENT_BAND = 4 * DEG; // rad — snap capture zone\nconst DETENT_K = 200; // s^-2\nconst DETENT_C = 2 * 0.9 * Math.sqrt(DETENT_K); // zeta = 0.9 → one tiny overshoot\nconst NEEDLE_K = 120; // s^-2\nconst NEEDLE_C = 2 * 0.75 * Math.sqrt(NEEDLE_K); // zeta = 0.75 → weighted lag\nconst SLEEP_OMEGA = 0.02; // rad/s\nconst SLEEP_NEEDLE = 0.05 * DEG; // rad\nconst SNAP_EPS = 0.0002; // rad — close enough to hard-snap the detent\nconst TICKS = Array.from({ length: 24 }, (_, i) => i * 15);\n\nfunction wrapPi(a: number) {\n  a = (a + Math.PI) % (2 * Math.PI);\n  if (a < 0) a += 2 * Math.PI;\n  return a - Math.PI;\n}\n\nexport function MoireDial({\n  message = \"SIGNAL\",\n  pitch = 6,\n  gear = 0.15,\n  initialAngle = -132,\n  friction = 3.5,\n  fieldHeight = 224,\n  className = \"\",\n  \"aria-label\": ariaLabel = \"Moire tuning dial\",\n}: {\n  /** hidden word that resolves out of the interference at alignment */\n  message?: string;\n  /** grating pitch in px — one 1px line every `pitch` px */\n  pitch?: number;\n  /** grating radians per dial radian — radio-vernier ratio */\n  gear?: number;\n  /** starting dial angle in degrees (reduced motion forces 0 = aligned) */\n  initialAngle?: number;\n  /** angular velocity decay in s^-1 while coasting after a flick */\n  friction?: number;\n  /** interference field height in px */\n  fieldHeight?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n  /** accessible name for the dial. Default \"Moire tuning dial\". */\n  \"aria-label\"?: string;\n}) {\n  const fieldRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const knobRef = useRef<HTMLDivElement>(null);\n  const ringRef = useRef<HTMLDivElement>(null);\n  const needleRef = useRef<HTMLDivElement>(null);\n  const readoutRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    const field = fieldRef.current;\n    const canvas = canvasRef.current;\n    const knob = knobRef.current;\n    const ring = ringRef.current;\n    const needle = needleRef.current;\n    const readout = readoutRef.current;\n    const input = inputRef.current;\n    if (!field || !canvas || !knob || !ring || !needle || !readout || !input)\n      return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const reduced = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n\n    // hot-path state — locals only, never React state\n    let angle = reduced ? 0 : initialAngle * DEG; // ring angle, continuous rad\n    let omega = 0; // rad/s\n    let needleAngle = angle;\n    let needleOmega = 0;\n    let dragging = false;\n    let raf = 0;\n    let last = 0;\n    let lastPA = 0; // pointer angle at last move\n    let lastT = 0;\n    let detentGraceUntil = 0; // keyboard steps suppress the detent briefly\n    let disposed = false;\n\n    let w = 0;\n    let h = 0;\n    let dpr = 1;\n    let diag = 0;\n    let offA: HTMLCanvasElement | null = null; // fixed grating + inverted word\n    let offB: HTMLCanvasElement | null = null; // rotating overlay grating\n\n    // grating ink derives from the --foreground token so the field is\n    // legible in both themes — re-read whenever the theme class flips.\n    let gratingFill = \"rgba(237,237,237,0.5)\";\n    const deriveGratingFill = () => {\n      const fg = getComputedStyle(document.documentElement)\n        .getPropertyValue(\"--foreground\")\n        .trim();\n      if (!fg) return;\n      const probe = document.createElement(\"canvas\").getContext(\"2d\");\n      if (!probe) return;\n      probe.fillStyle = fg;\n      const [r, g, b] = probe.fillStyle.match(/\\d+/g)?.map(Number) ?? [];\n      if (r === undefined) return;\n      gratingFill = `rgba(${r},${g},${b},0.5)`;\n    };\n    deriveGratingFill();\n\n    const build = () => {\n      const rect = field.getBoundingClientRect();\n      w = Math.round(rect.width);\n      h = Math.round(rect.height);\n      if (w < 2 || h < 2) return;\n      dpr = Math.min(2, window.devicePixelRatio || 1);\n      canvas.width = w * dpr;\n      canvas.height = h * dpr;\n\n      // message glyphs → offscreen alpha mask (hero-gravity-well pattern)\n      const mask = document.createElement(\"canvas\");\n      mask.width = w * dpr;\n      mask.height = h * dpr;\n      const mctx = mask.getContext(\"2d\");\n      if (!mctx) return;\n      mctx.scale(dpr, dpr);\n      const size = Math.min(h * 0.58, (w * 1.5) / Math.max(1, message.length));\n      mctx.font = `600 ${size}px GeistSans, \"GeistSans Fallback\", ui-sans-serif, system-ui, sans-serif`;\n      mctx.textAlign = \"center\";\n      mctx.textBaseline = \"middle\";\n      mctx.fillStyle = \"#ffffff\";\n      mctx.fillText(message, w / 2, h / 2);\n\n      // grating A: fixed 1px lines at `pitch`, phase-inverted by half a pitch\n      // inside the glyph mask — invisible until the overlay aligns\n      offA = document.createElement(\"canvas\");\n      offA.width = w * dpr;\n      offA.height = h * dpr;\n      const actx = offA.getContext(\"2d\");\n      if (!actx) return;\n      actx.scale(dpr, dpr);\n      actx.fillStyle = gratingFill;\n      for (let y = 0; y <= h; y += pitch) actx.fillRect(0, y, w, 1);\n      actx.globalCompositeOperation = \"destination-out\";\n      actx.drawImage(mask, 0, 0, w, h);\n      const inv = document.createElement(\"canvas\");\n      inv.width = w * dpr;\n      inv.height = h * dpr;\n      const ictx = inv.getContext(\"2d\");\n      if (!ictx) return;\n      ictx.scale(dpr, dpr);\n      ictx.fillStyle = gratingFill;\n      for (let y = pitch / 2; y <= h; y += pitch) ictx.fillRect(0, y, w, 1);\n      ictx.globalCompositeOperation = \"destination-in\";\n      ictx.drawImage(mask, 0, 0, w, h);\n      actx.globalCompositeOperation = \"source-over\";\n      actx.drawImage(inv, 0, 0, w, h);\n\n      // grating B: oversized square so rotation never exposes an edge; phase\n      // chosen so its lines coincide with A's background lines at zero rotation\n      diag = Math.ceil(Math.hypot(w, h)) + pitch * 2;\n      offB = document.createElement(\"canvas\");\n      offB.width = diag * dpr;\n      offB.height = diag * dpr;\n      const bctx = offB.getContext(\"2d\");\n      if (!bctx) return;\n      bctx.scale(dpr, dpr);\n      bctx.fillStyle = gratingFill;\n      const phase = ((diag / 2 - h / 2) % pitch + pitch) % pitch;\n      for (let y = phase; y <= diag; y += pitch) bctx.fillRect(0, y, diag, 1);\n    };\n\n    const render = () => {\n      const wr = wrapPi(angle);\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.clearRect(0, 0, w, h);\n      if (offA) ctx.drawImage(offA, 0, 0, w, h);\n      if (offB) {\n        ctx.save();\n        ctx.translate(w / 2, h / 2);\n        // |wr| keeps the fringe field continuous across the ±180° wrap\n        // (a line grating is 2-fold symmetric, so sign carries no extra info)\n        ctx.rotate(gear * Math.abs(wr));\n        ctx.drawImage(offB, -diag / 2, -diag / 2, diag, diag);\n        ctx.restore();\n      }\n      ring.style.transform = `rotate(${angle / DEG}deg)`;\n      needle.style.transform = `rotate(${needleAngle / DEG}deg)`;\n      const pct =\n        Math.abs(wr) < SNAP_EPS\n          ? 100\n          : Math.min(99, Math.floor(100 * (1 - Math.abs(wr) / Math.PI)));\n      readout.textContent = `ALIGNMENT ${String(pct).padStart(3, \"0\")}%`;\n      const deg = Math.max(\n        -180,\n        Math.min(180, Math.round(wr / DEG / 2) * 2)\n      );\n      if (input.valueAsNumber !== deg) input.value = String(deg);\n      input.setAttribute(\"aria-valuetext\", `${pct}% aligned`);\n    };\n\n    const loop = (now: number) => {\n      const dt = last === 0 ? 1 / 60 : Math.min(0.05, (now - last) / 1000);\n      last = now;\n\n      if (!dragging) {\n        const wr = wrapPi(angle);\n        if (Math.abs(wr) < DETENT_BAND && now > detentGraceUntil) {\n          // detent snap spring: k = 200 s^-2, zeta = 0.9 — one tiny overshoot\n          omega += (-DETENT_K * wr - DETENT_C * omega) * dt;\n          angle += omega * dt;\n          const nwr = wrapPi(angle);\n          if (Math.abs(nwr) < SNAP_EPS && Math.abs(omega) < SLEEP_OMEGA) {\n            angle -= nwr; // exact zero\n            omega = 0;\n          }\n        } else {\n          // coast: friction decay 3.5 s^-1 — flick spins, coasts, settles\n          omega *= Math.exp(-friction * dt);\n          angle += omega * dt;\n          if (Math.abs(omega) < SLEEP_OMEGA) omega = 0;\n        }\n      }\n\n      // needle lags the ring through a second-order spring (weighted feel)\n      needleOmega += (NEEDLE_K * (angle - needleAngle) - NEEDLE_C * needleOmega) * dt;\n      needleAngle += needleOmega * dt;\n\n      render();\n\n      const wrNow = wrapPi(angle);\n      const settled =\n        !dragging &&\n        Math.abs(omega) < SLEEP_OMEGA &&\n        Math.abs(angle - needleAngle) < SLEEP_NEEDLE &&\n        Math.abs(needleOmega) < 0.02 &&\n        (Math.abs(wrNow) >= DETENT_BAND || Math.abs(wrNow) < SNAP_EPS);\n      if (settled) {\n        needleAngle = angle;\n        needleOmega = 0;\n        omega = 0;\n        render();\n        raf = 0;\n        last = 0;\n      } else {\n        raf = requestAnimationFrame(loop);\n      }\n    };\n    const wake = () => {\n      if (!raf && !reduced) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n\n    const pointerAngle = (e: PointerEvent) => {\n      const r = knob.getBoundingClientRect();\n      return Math.atan2(\n        e.clientY - (r.top + r.height / 2),\n        e.clientX - (r.left + r.width / 2)\n      );\n    };\n    const onDown = (e: PointerEvent) => {\n      if (e.pointerType === \"mouse\" && e.button !== 0) return;\n      try {\n        knob.setPointerCapture(e.pointerId);\n      } catch {\n        // synthetic events may carry an inactive pointerId — drag still works\n      }\n      dragging = true;\n      omega = 0;\n      lastPA = pointerAngle(e);\n      lastT = performance.now();\n      wake();\n    };\n    const onMove = (e: PointerEvent) => {\n      if (!dragging) return;\n      const pa = pointerAngle(e);\n      const d = wrapPi(pa - lastPA);\n      const t = performance.now();\n      const mdt = Math.max(1e-3, (t - lastT) / 1000);\n      angle += d;\n      // smoothed, clamped flick velocity\n      omega = Math.max(-24, Math.min(24, 0.7 * omega + 0.3 * (d / mdt)));\n      lastPA = pa;\n      lastT = t;\n      if (reduced) {\n        needleAngle = angle;\n        render();\n      } else {\n        wake();\n      }\n    };\n    const onUp = () => {\n      if (!dragging) return;\n      dragging = false;\n      if (reduced) {\n        // step instantly: snap into the detent if inside the band\n        omega = 0;\n        const wr = wrapPi(angle);\n        if (Math.abs(wr) < DETENT_BAND) angle -= wr;\n        needleAngle = angle;\n        render();\n      } else {\n        wake(); // omega carries → flick coasts under friction\n      }\n    };\n\n    const onInput = () => {\n      const deg = input.valueAsNumber;\n      if (Number.isNaN(deg)) return;\n      // shortest path from the current wrapped position to the slider value\n      angle += wrapPi(deg * DEG - wrapPi(angle));\n      omega = 0;\n      // let arrow keys step through the detent band instead of being recaptured\n      detentGraceUntil = performance.now() + 400;\n      if (reduced) {\n        needleAngle = angle;\n        render();\n      } else {\n        wake();\n      }\n    };\n\n    build();\n    render();\n    // re-rasterize once Geist is loaded — a fallback-font mask is wrong\n    document.fonts.ready.then(() => {\n      if (disposed) return;\n      build();\n      render();\n    });\n\n    const ro = new ResizeObserver(() => {\n      build();\n      render();\n    });\n    ro.observe(field);\n\n    // theme toggle flips the `dark` class on <html> — re-derive the grating\n    // ink from --foreground and re-rasterize, or light mode ships blank\n    const mo = new MutationObserver(() => {\n      deriveGratingFill();\n      build();\n      render();\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    knob.addEventListener(\"pointerdown\", onDown);\n    knob.addEventListener(\"pointermove\", onMove);\n    knob.addEventListener(\"pointerup\", onUp);\n    knob.addEventListener(\"pointercancel\", onUp);\n    input.addEventListener(\"input\", onInput);\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      raf = 0;\n      ro.disconnect();\n      mo.disconnect();\n      knob.removeEventListener(\"pointerdown\", onDown);\n      knob.removeEventListener(\"pointermove\", onMove);\n      knob.removeEventListener(\"pointerup\", onUp);\n      knob.removeEventListener(\"pointercancel\", onUp);\n      input.removeEventListener(\"input\", onInput);\n    };\n  }, [message, pitch, gear, initialAngle, friction]);\n\n  return (\n    <div className={`flex w-full flex-col items-center gap-6 ${className}`}>\n      <div\n        ref={fieldRef}\n        role=\"img\"\n        aria-label={`Interference field concealing the word ${message}`}\n        className=\"relative w-full overflow-hidden rounded-md border border-border\"\n        style={{ height: fieldHeight }}\n      >\n        <canvas\n          ref={canvasRef}\n          aria-hidden\n          className=\"absolute inset-0 h-full w-full\"\n        />\n      </div>\n\n      <div className=\"relative\">\n        {/* fixed target index the ring's zero tick lines up with */}\n        <span\n          aria-hidden\n          className=\"absolute -top-3 left-1/2 h-2 w-px -translate-x-1/2 bg-ns-muted\"\n        />\n        <div\n          ref={knobRef}\n          className=\"relative h-40 w-40 cursor-grab touch-none select-none rounded-full border border-border bg-surface active:cursor-grabbing focus-within:outline focus-within:outline-2 focus-within:outline-offset-4 focus-within:outline-ns-accent\"\n        >\n          <input\n            ref={inputRef}\n            type=\"range\"\n            min={-180}\n            max={180}\n            step={2}\n            defaultValue={Math.max(\n              -180,\n              Math.min(180, Math.round(wrapPi(initialAngle * DEG) / DEG / 2) * 2)\n            )}\n            aria-label={ariaLabel}\n            className=\"sr-only\"\n          />\n          {/* tick ring rotates rigidly with the dial angle */}\n          <div ref={ringRef} aria-hidden className=\"absolute inset-0 will-change-transform\">\n            {TICKS.map((deg) => (\n              <span\n                key={deg}\n                className={`absolute left-1/2 top-1/2 w-px ${\n                  deg === 0 ? \"h-3 bg-foreground\" : \"h-1.5 bg-border\"\n                }`}\n                style={{\n                  transform: `translate(-50%, -50%) rotate(${deg}deg) translateY(-66px)`,\n                }}\n              />\n            ))}\n          </div>\n          {/* needle lags behind through the second-order spring */}\n          <div\n            ref={needleRef}\n            aria-hidden\n            className=\"absolute inset-0 will-change-transform\"\n          >\n            <span className=\"absolute left-1/2 top-3.5 h-11 w-0.5 -translate-x-1/2 rounded-full bg-foreground\" />\n          </div>\n          <span\n            aria-hidden\n            className=\"absolute left-1/2 top-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border border-border bg-background\"\n          />\n        </div>\n      </div>\n\n      <div\n        ref={readoutRef}\n        aria-hidden\n        className=\"font-mono text-xs tracking-[0.2em] text-ns-muted tabular-nums\"\n      >\n        ALIGNMENT 000%\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/dial-moire.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-surface": "var(--surface)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "surface": "#fafafa"
    },
    "dark": {
      "ns-muted": "#8f8f8f",
      "surface": "#171717"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "canvas",
      "moire",
      "dial",
      "knob",
      "physics",
      "interference",
      "hidden-message",
      "detent"
    ],
    "instruction": "A weighted rotary knob whose feedback channel is optical interference, built on a DPR-clamped (max 2) Canvas 2D field. Two fine-line gratings (1px strokes at ~0.5 alpha derived live from the --foreground token, 6px pitch, re-derived via a MutationObserver on theme-class changes so both themes stay legible): grating A is fixed and carries a hidden message as a locally phase-inverted region — the message glyphs are rasterized offscreen to an alpha mask (hero-gravity-well sampling pattern) and inside the mask A's line phase is offset by half a pitch, so the word only gains contrast when the overlay aligns; grating B is an oversized pre-rendered square that rotates with the dial angle through a radio-vernier gear ratio (~0.15), its phase chosen so its lines coincide with A's background lines at zero rotation. Both gratings are pre-rendered to offscreen canvases and composited per frame. Knob: 160px disc, bg-surface with border-border ring, a 24-tick ring layer rotating rigidly with the dial, a needle that lags the ring through a second-order spring (k=120 s^-2, zeta=0.75, weighted Inertial-Dial feel), and a font-mono ALIGNMENT percent readout below. Interaction: pointer-capture drag maps pointer angle about the knob center to ring angle with smoothed angular velocity; on release omega integrates under friction decay 3.5 s^-1 (flick spins, coasts, settles). Detent: within +-4 deg of alignment a snap spring (k=200 s^-2, zeta=0.9) pulls to exact zero with one tiny overshoot and the readout ticks to 100%. A single direct-DOM rAF loop is the sole writer (canvas draw, ring/needle transforms, readout text, slider value — no React state on the hot path) and sleeps when |omega| < 0.02 rad/s and needle error < 0.05 deg. Keyboard a11y via a visually hidden range input (arrows step 2 deg, aria-valuetext reports percent aligned, short detent grace so steps can escape the band). Under prefers-reduced-motion the dial renders the aligned state statically with the word legible and steps instantly with no loop. Zero dependencies."
  },
  "type": "registry:ui"
}