{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slider-chladni-tune",
  "title": "Slider Chladni Tune",
  "description": "Precision-tuning slider whose readout is a plate of a few thousand sand grains: off target they churn in a formless haze, on target they lock into a crisp symmetric Chladni figure and hold still. Distance-to-correct read as pattern coherence, not a number.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/slider-chladni-tune/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\n\n// ---------------------------------------------------------------------------\n// ChladniTune — a precision-tuning slider whose readout is a plate of sand.\n// A few thousand grains random-walk across a square Canvas 2D plate; how far\n// the slider sits from an unstated `target` sets both how hard they jitter\n// and how strongly they're pulled onto the nodal lines of a Chladni figure\n// sin(n*pi*x)*sin(m*pi*y) - sin(m*pi*x)*sin(n*pi*y) = 0 for a mode (n, m)\n// picked deterministically from `target`. Off target the grains churn in a\n// formless haze; on target they lock into the crisp symmetric figure and\n// hold still. Distance-to-correct is encoded as PATTERN COHERENCE — a\n// continuous, directionless \"how close\" signal, the way you'd tune an\n// instrument by ear rather than by reading a number.\n//\n// MECHANISM: rather than integrating a force field (expensive, and force\n// magnitude near a saddle of |f| doesn't actually read as \"coherence\"), each\n// grain proposes a small random step and the step is judged by whether it\n// makes |f(x, y)| bigger or smaller — a Metropolis-style accept/reject, the\n// standard trick real-time Chladni-sand demos use. A step that improves\n// (shrinks) |f| is always taken. A step that worsens it is taken anyway with\n// probability normalizedDetune^1.4 — that probability IS the \"attraction\n// strength = 1 - normalizedDetune\" the brief describes, just expressed as a\n// rejection rate instead of a force: at detune 0 worse moves are almost\n// never accepted, so grains that reach the zero set stay there (locked,\n// motionless); at detune 1 they're accepted almost always, so the walk is\n// unbiased and grains never accumulate anywhere (pure scatter). Jitter\n// magnitude also scales linearly with detune, so besides drifting or holding,\n// off-target grains visibly move harder. No dt-scaling: this is a discrete\n// per-frame Markov step, not a velocity integration, so running it once per\n// rAF tick (not scaled by elapsed time) is the correct thing to do, same as\n// any other frame-stepped cellular automaton.\n//\n// Grain positions live in a plain Float32Array pair, updated in place —\n// never React state. Canvas ink (plate = --background, grains = --foreground\n// at low alpha) is read via getComputedStyle at mount and re-derived on a\n// MutationObserver watching documentElement's class attribute, so a theme\n// flip repaints correctly instead of leaving stale colors baked into a\n// gradient. The rAF loop pauses via IntersectionObserver while offscreen and\n// on document.hidden; it never fully sleeps otherwise, because even a locked\n// plate still carries a small residual jitter (real sand vibrates, it\n// doesn't teleport to a stop) — that's deliberate, not a missed easing.\n//\n// A11Y: the slider is a real native <input type=\"range\"> (role=slider, full\n// arrow/Home/End/PageUp/PageDown keyboard support for free), aria-valuetext\n// reporting both the number and a categorical proximity phrase (\"42.1, very\n// close to target\"). The canvas is aria-hidden; a Geist Mono readout beneath\n// the slider duplicates the same value/coherence numbers in text, so the\n// sand is never the only channel carrying the signal. A dedicated\n// aria-live=polite region announces \"On target.\" exactly on the transition\n// into lock (empty otherwise, so it never spams on every drag tick).\n// REDUCED MOTION: the whole simulation is replaced by a static, non-canvas\n// SVG figure with three fixed point sets (scattered / forming / locked)\n// computed once per mode; the live detune/lock state just selects which of\n// the three to render — an instant swap, never an animation. Zero deps.\n// ---------------------------------------------------------------------------\n\ntype Vec3 = readonly [number, number, number];\n\nfunction parseColor(raw: string): Vec3 | null {\n  const s = raw.trim();\n  if (!s) return null;\n  if (s.startsWith(\"#\")) {\n    const hex = s.slice(1);\n    if (hex.length === 3) {\n      const r = parseInt(hex[0] + hex[0], 16);\n      const g = parseInt(hex[1] + hex[1], 16);\n      const b = parseInt(hex[2] + hex[2], 16);\n      return Number.isNaN(r + g + b) ? null : [r, g, b];\n    }\n    if (hex.length >= 6) {\n      const r = parseInt(hex.slice(0, 2), 16);\n      const g = parseInt(hex.slice(2, 4), 16);\n      const b = parseInt(hex.slice(4, 6), 16);\n      return Number.isNaN(r + g + b) ? null : [r, g, b];\n    }\n    return null;\n  }\n  const m = s.match(/rgba?\\(\\s*([\\d.]+)[,\\s]+([\\d.]+)[,\\s]+([\\d.]+)/);\n  return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;\n}\n\n// canonical asymmetric (n, m) pairs — n === m makes the Chladni expression\n// identically zero everywhere (a degenerate, non-figure \"target\"), so every\n// entry here deliberately keeps n !== m.\nconst CHLADNI_MODES: readonly (readonly [number, number])[] = [\n  [2, 1],\n  [3, 1],\n  [3, 2],\n  [4, 1],\n  [4, 3],\n  [5, 2],\n  [5, 3],\n  [5, 4],\n  [6, 1],\n  [6, 5],\n];\n\nfunction hash01(n: number): number {\n  const s = Math.sin(n * 12.9898) * 43758.5453;\n  return s - Math.floor(s);\n}\n\nfunction modeForTarget(target: number): readonly [number, number] {\n  const idx = Math.floor(hash01(target) * CHLADNI_MODES.length) % CHLADNI_MODES.length;\n  return CHLADNI_MODES[idx] ?? CHLADNI_MODES[0];\n}\n\nfunction chladni(n: number, m: number, x: number, y: number): number {\n  return (\n    Math.sin(n * Math.PI * x) * Math.sin(m * Math.PI * y) -\n    Math.sin(m * Math.PI * x) * Math.sin(n * Math.PI * y)\n  );\n}\n\nfunction clamp(v: number, lo: number, hi: number) {\n  return Math.min(hi, Math.max(lo, v));\n}\n\nfunction proximityWord(detune: number, locked: boolean): string {\n  if (locked) return \"on target\";\n  if (detune < 0.15) return \"very close to target\";\n  if (detune < 0.4) return \"close to target\";\n  if (detune < 0.75) return \"off target\";\n  return \"far from target\";\n}\n\nfunction stateBucket(detune: number, locked: boolean): \"locked\" | \"converging\" | \"scattered\" {\n  if (locked) return \"locked\";\n  return detune < 0.55 ? \"converging\" : \"scattered\";\n}\n\nfunction useReducedMotionPref(): boolean {\n  const [reduced, setReduced] = useState(false);\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduced(mq.matches);\n    const onChange = (e: MediaQueryListEvent) => setReduced(e.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n  return reduced;\n}\n\nconst REDUCED_GRID = 44; // sample grid resolution for the static figure\nconst REDUCED_COUNT = 220; // point count in every static bucket\n\nfunction computeLockedPoints(n: number, m: number): readonly (readonly [number, number])[] {\n  const candidates: { x: number; y: number; a: number }[] = [];\n  for (let i = 0; i <= REDUCED_GRID; i++) {\n    for (let j = 0; j <= REDUCED_GRID; j++) {\n      const x = i / REDUCED_GRID;\n      const y = j / REDUCED_GRID;\n      candidates.push({ x, y, a: Math.abs(chladni(n, m, x, y)) });\n    }\n  }\n  candidates.sort((p, q) => p.a - q.a);\n  return candidates.slice(0, REDUCED_COUNT).map((p) => [p.x, p.y] as const);\n}\n\nfunction computeScatteredPoints(): readonly (readonly [number, number])[] {\n  const pts: (readonly [number, number])[] = [];\n  for (let i = 0; i < REDUCED_COUNT; i++) {\n    pts.push([hash01(i * 12.9898 + 1.7), hash01(i * 78.233 + 4.1)] as const);\n  }\n  return pts;\n}\n\nfunction computeFormingPoints(\n  scattered: readonly (readonly [number, number])[],\n  locked: readonly (readonly [number, number])[]\n): readonly (readonly [number, number])[] {\n  return scattered.map(([sx, sy], i) => {\n    const [lx, ly] = locked[i % locked.length] ?? [sx, sy];\n    return [sx + (lx - sx) * 0.55, sy + (ly - sy) * 0.55] as const;\n  });\n}\n\nfunction ReducedFigure({\n  bucket,\n  scattered,\n  forming,\n  locked,\n}: {\n  bucket: \"locked\" | \"converging\" | \"scattered\";\n  scattered: readonly (readonly [number, number])[];\n  forming: readonly (readonly [number, number])[];\n  locked: readonly (readonly [number, number])[];\n}) {\n  const points = bucket === \"locked\" ? locked : bucket === \"converging\" ? forming : scattered;\n  return (\n    <svg\n      aria-hidden\n      viewBox=\"0 0 100 100\"\n      className=\"absolute inset-0 h-full w-full\"\n      preserveAspectRatio=\"none\"\n    >\n      {points.map(([x, y], i) => (\n        <circle\n          key={i}\n          cx={x * 100}\n          cy={y * 100}\n          r={0.9}\n          className=\"fill-foreground/45\"\n        />\n      ))}\n    </svg>\n  );\n}\n\nexport interface ChladniTuneProps {\n  /** controlled value; omit for uncontrolled */\n  value?: number;\n  /** uncontrolled initial value */\n  defaultValue?: number;\n  /** the value the plate resolves at — never rendered as a number in the UI */\n  target: number;\n  /** minimum allowed value */\n  min?: number;\n  /** maximum allowed value */\n  max?: number;\n  /** increment per keyboard/drag step */\n  step?: number;\n  /** value distance at which detune reaches 1 (full chaos). default: 25% of the range */\n  detuneSpan?: number;\n  /** value distance within which the plate is considered locked. default: max(step, 0.5% of the range) */\n  lockEpsilon?: number;\n  /** override the auto-derived Chladni mode (n, m); n === m is rejected as degenerate */\n  mode?: readonly [number, number];\n  /** sand grain count on the canvas plate. default 2200 */\n  grainCount?: number;\n  /** called with the new value on every drag/keyboard change */\n  onValueChange?: (value: number) => void;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n  /** accessible name for the slider */\n  \"aria-label\"?: string;\n}\n\nexport function ChladniTune({\n  value,\n  defaultValue = 50,\n  target,\n  min = 0,\n  max = 100,\n  step = 0.1,\n  detuneSpan,\n  lockEpsilon,\n  mode,\n  grainCount = 2200,\n  onValueChange,\n  className = \"\",\n  \"aria-label\": ariaLabel = \"Tuning value\",\n}: ChladniTuneProps) {\n  const isControlled = value !== undefined;\n  const [internal, setInternal] = useState(() => clamp(defaultValue, min, max));\n  const current = isControlled ? (value as number) : internal;\n  const currentRef = useRef(current);\n  currentRef.current = current;\n\n  const span = Math.max(1e-9, max - min);\n  const dSpan = detuneSpan ?? span * 0.25;\n  const lockEps = lockEpsilon ?? Math.max(step, span * 0.005);\n\n  const rawMode = mode ?? modeForTarget(target);\n  const resolvedMode: readonly [number, number] =\n    rawMode[0] === rawMode[1] ? modeForTarget(target + 1) : rawMode;\n  const [n, m] = resolvedMode;\n\n  const detune = clamp(Math.abs(current - target) / Math.max(1e-9, dSpan), 0, 1);\n  const locked = Math.abs(current - target) <= lockEps;\n  const coherencePct = Math.round((1 - detune) * 100);\n  const word = proximityWord(detune, locked);\n  const bucket = stateBucket(detune, locked);\n\n  const reduced = useReducedMotionPref();\n\n  const detuneRef = useRef(detune);\n  detuneRef.current = detune;\n  const modeRef = useRef(resolvedMode);\n  modeRef.current = resolvedMode;\n\n  const containerRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    if (reduced) return;\n    const container = containerRef.current;\n    const canvas = canvasRef.current;\n    if (!container || !canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    // -- token-derived ink: read at mount, re-derived on theme class flips --\n    let plateColor = \"#ffffff\";\n    let grainColor = \"rgba(23,23,23,0.5)\";\n    const deriveColors = () => {\n      const cs = getComputedStyle(document.documentElement);\n      const bg = parseColor(cs.getPropertyValue(\"--background\"));\n      const fg = parseColor(cs.getPropertyValue(\"--foreground\"));\n      if (bg) plateColor = `rgb(${bg[0]}, ${bg[1]}, ${bg[2]})`;\n      if (fg) grainColor = `rgba(${fg[0]}, ${fg[1]}, ${fg[2]}, 0.5)`;\n    };\n    deriveColors();\n\n    const count = Math.max(200, Math.floor(grainCount));\n    const xs = new Float32Array(count);\n    const ys = new Float32Array(count);\n    for (let i = 0; i < count; i++) {\n      xs[i] = Math.random();\n      ys[i] = Math.random();\n    }\n\n    let w = 0;\n    let h = 0;\n    let dpr = 1;\n    let sized = false;\n    let raf = 0;\n    let visible = true;\n\n    const resize = () => {\n      const rect = container.getBoundingClientRect();\n      if (rect.width < 4 || rect.height < 4) {\n        sized = false;\n        return;\n      }\n      w = rect.width;\n      h = rect.height;\n      dpr = Math.min(2, window.devicePixelRatio || 1);\n      canvas.width = Math.round(w * dpr);\n      canvas.height = Math.round(h * dpr);\n      canvas.style.width = `${w}px`;\n      canvas.style.height = `${h}px`;\n      sized = true;\n    };\n    resize();\n\n    const GRAIN_PX = 1.4;\n    const JITTER_MIN = 0.0016; // normalized-unit step at detune 0 — a held-still shimmer, not zero\n    const JITTER_MAX = 0.03; // normalized-unit step at detune 1 — a visibly churning haze\n\n    const stepGrains = () => {\n      const [cn, cm] = modeRef.current;\n      const d = detuneRef.current;\n      const jitter = JITTER_MIN + (JITTER_MAX - JITTER_MIN) * d;\n      const chaosAccept = Math.pow(d, 1.4); // probability a worse move is kept anyway\n      for (let i = 0; i < count; i++) {\n        const x = xs[i];\n        const y = ys[i];\n        const f0 = chladni(cn, cm, x, y);\n        let nx = x + (Math.random() - 0.5) * jitter;\n        let ny = y + (Math.random() - 0.5) * jitter;\n        if (nx < 0) nx = -nx;\n        else if (nx > 1) nx = 2 - nx;\n        if (ny < 0) ny = -ny;\n        else if (ny > 1) ny = 2 - ny;\n        const f1 = chladni(cn, cm, nx, ny);\n        if (Math.abs(f1) <= Math.abs(f0) || Math.random() < chaosAccept) {\n          xs[i] = nx;\n          ys[i] = ny;\n        }\n      }\n    };\n\n    const render = () => {\n      if (!sized) return;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.fillStyle = plateColor;\n      ctx.fillRect(0, 0, w, h);\n      ctx.fillStyle = grainColor;\n      const g = GRAIN_PX;\n      for (let i = 0; i < count; i++) {\n        ctx.fillRect(xs[i] * w - g / 2, ys[i] * h - g / 2, g, g);\n      }\n    };\n\n    const loop = () => {\n      stepGrains();\n      render();\n      raf = requestAnimationFrame(loop);\n    };\n    const wake = () => {\n      if (!raf && sized && visible && !document.hidden) raf = requestAnimationFrame(loop);\n    };\n    const sleep = () => {\n      cancelAnimationFrame(raf);\n      raf = 0;\n    };\n\n    render();\n    wake();\n\n    const ro = new ResizeObserver(() => {\n      resize();\n      render();\n    });\n    ro.observe(container);\n\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible) wake();\n      else sleep();\n    });\n    io.observe(container);\n\n    // theme toggle flips the `dark` class on <html> — re-derive plate/grain\n    // ink from the tokens, or the wrong theme's colors stay baked in\n    const mo = new MutationObserver(() => {\n      deriveColors();\n      render();\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    const onVis = () => {\n      if (document.hidden) sleep();\n      else wake();\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    return () => {\n      sleep();\n      ro.disconnect();\n      io.disconnect();\n      mo.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n    // grainCount and the resolved mode are the only things that require a\n    // fresh grain field / redraw cadence; detune/current flow through refs\n    // so ordinary slider movement never tears down and re-seeds the plate.\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [reduced, grainCount, n, m]);\n\n  const scattered = useMemo(() => computeScatteredPoints(), []);\n  const lockedPts = useMemo(() => computeLockedPoints(n, m), [n, m]);\n  const forming = useMemo(() => computeFormingPoints(scattered, lockedPts), [scattered, lockedPts]);\n\n  // -- custom track: a real native <input type=range> carries keyboard a11y\n  // (sr-only, not display:none, so Tab reaches it and arrow/Home/End/PageUp/\n  // PageDown come free from native semantics) while a plain DOM track+thumb\n  // pair carries pointer interaction and the visible \"core-styled\" look.\n  // This split (compare-crack-seam's / slider-range-shear's pattern in this registry)\n  // matters beyond taste: a synthetic pointerdown/pointermove dispatched at\n  // a native range's thumb is not guaranteed to move it — that native\n  // click-drag-to-set-value behavior is UA-internal, not something a script\n  // can reliably trigger with untrusted events — so autoplay's \"drag\" mode\n  // needs a plain element whose own onPointer* handlers do the mapping.\n  const trackRef = useRef<HTMLDivElement>(null);\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [focused, setFocused] = useState(false);\n  const draggingRef = useRef(false);\n\n  const commit = (clientX: number) => {\n    const track = trackRef.current;\n    if (!track) return;\n    const r = track.getBoundingClientRect();\n    const frac = clamp(r.width > 0 ? (clientX - r.left) / r.width : 0, 0, 1);\n    const raw = min + frac * span;\n    const snapped = Math.round((raw - min) / step) * step + min;\n    const v = clamp(Number(snapped.toFixed(6)), min, max);\n    if (!isControlled) setInternal(v);\n    onValueChange?.(v);\n  };\n\n  const onTrackPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (e.pointerType === \"mouse\" && e.button !== 0) return;\n    try {\n      e.currentTarget.setPointerCapture(e.pointerId);\n    } catch {\n      // synthetic (autoplay) pointerIds match no live pointer — drag still works\n    }\n    draggingRef.current = true;\n    inputRef.current?.focus();\n    commit(e.clientX);\n  };\n  const onTrackPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!draggingRef.current) return;\n    commit(e.clientX);\n  };\n  const onTrackPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {\n    draggingRef.current = false;\n    try {\n      e.currentTarget.releasePointerCapture(e.pointerId);\n    } catch {\n      // no-op — see onTrackPointerDown\n    }\n  };\n\n  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const v = clamp(Number(e.target.value), min, max);\n    if (!isControlled) setInternal(v);\n    onValueChange?.(v);\n  };\n\n  const pct = span > 0 ? ((current - min) / span) * 100 : 0;\n\n  return (\n    <div className={`flex w-full flex-col items-center gap-5 ${className}`}>\n      <div\n        ref={containerRef}\n        aria-hidden\n        className=\"relative aspect-square w-full max-w-xs overflow-hidden rounded-md border border-border bg-background\"\n      >\n        {!reduced && <canvas ref={canvasRef} aria-hidden className=\"absolute inset-0 h-full w-full\" />}\n        {reduced && (\n          <ReducedFigure bucket={bucket} scattered={scattered} forming={forming} locked={lockedPts} />\n        )}\n      </div>\n\n      <div className=\"flex w-full max-w-xs flex-col gap-2.5\">\n        <div\n          ref={trackRef}\n          onPointerDown={onTrackPointerDown}\n          onPointerMove={onTrackPointerMove}\n          onPointerUp={onTrackPointerUp}\n          onPointerCancel={onTrackPointerUp}\n          className=\"chladni-track relative flex h-4 w-full cursor-pointer touch-none select-none items-center\"\n        >\n          <div aria-hidden className=\"pointer-events-none h-1.5 w-full rounded-full bg-border\">\n            <div\n              aria-hidden\n              className=\"h-full rounded-full bg-foreground/70\"\n              style={{ width: `${pct}%` }}\n            />\n          </div>\n          <div\n            aria-hidden\n            className={`pointer-events-none absolute top-1/2 h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border border-border bg-foreground transition-shadow duration-150 ${\n              focused ? \"ring-2 ring-ns-accent ring-offset-2 ring-offset-background\" : \"\"\n            }`}\n            style={{ left: `${pct}%` }}\n          />\n          <input\n            ref={inputRef}\n            type=\"range\"\n            min={min}\n            max={max}\n            step={step}\n            value={current}\n            onChange={handleInputChange}\n            onFocus={() => setFocused(true)}\n            onBlur={() => setFocused(false)}\n            aria-label={ariaLabel}\n            aria-valuetext={`${current.toFixed(1)}, ${word}`}\n            className=\"sr-only\"\n          />\n        </div>\n\n        <div className=\"flex items-baseline justify-between font-mono text-xs text-ns-muted tabular-nums\">\n          <span>VALUE {current.toFixed(1)}</span>\n          <span>COHERENCE {String(Math.max(0, coherencePct)).padStart(3, \"0\")}%</span>\n        </div>\n        <p className=\"font-mono text-[11px] uppercase tracking-[0.16em] text-ns-muted\">\n          {bucket === \"locked\" ? \"locked\" : bucket === \"converging\" ? \"converging\" : \"scattered\"}\n        </p>\n\n        <p aria-live=\"polite\" className=\"sr-only\">\n          {locked ? \"On target.\" : \"\"}\n        </p>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/slider-chladni-tune.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "loud",
    "tags": [
      "slider",
      "canvas",
      "calibration",
      "physics",
      "particles",
      "tuning",
      "input",
      "form",
      "aria-live"
    ],
    "instruction": "A tuning slider paired with a square Canvas 2D sand plate above it. `target` (required) is the value the plate resolves at and is never rendered as a number anywhere in the UI — only proximity is communicated, like tuning by ear. MECHANISM: a few thousand grains (Float32Array x/y pairs, `grainCount` default 2200) each propose a small random step every frame; the step is judged by a Metropolis-style accept/reject against |f(x,y)| where f(x,y) = sin(n*pi*x)*sin(m*pi*y) - sin(m*pi*x)*sin(n*pi*y) is the Chladni mode function for a mode (n, m) — steps that shrink |f| are always taken, steps that grow it are taken anyway with probability normalizedDetune^1.4. That acceptance probability IS 'attraction strength = 1 - normalizedDetune' expressed as a rejection rate rather than a force: near detune 0 worse moves are almost never kept, so grains that land on the zero set (the nodal lines) stay there, locked and nearly motionless; near detune 1 worse moves are accepted almost always, so the walk is unbiased and no pattern ever accumulates. Jitter step size also scales linearly with detune (0.0016 to 0.03 in plate-normalized units), so off-target grains visibly churn harder as well as failing to organize. detune = clamp(|value - target| / detuneSpan, 0, 1) where detuneSpan defaults to 25% of the min/max range (override via `detuneSpan`); the plate is considered LOCKED when |value - target| <= lockEpsilon (default max(step, 0.5% of the range)). Mode (n, m) is picked deterministically from `target` via a cheap sine hash into a curated table of ten asymmetric pairs (n === m is rejected as degenerate, since sin(n*pi*x)*sin(n*pi*y) - sin(n*pi*x)*sin(n*pi*y) is identically zero) so every distinct target gets its own figure; overridable via `mode`. RENDER: the canvas is fully cleared and redrawn from the grain array every frame (fillRect plate fill + one fillRect per grain, no alpha accumulation, no destination-out) so a locked plate is provably a clean figure, never residue. Plate fill is --background and grain ink is --foreground at 0.5 alpha, both read via getComputedStyle at mount and re-derived on a MutationObserver watching documentElement's class attribute, so a theme flip repaints correctly instead of baking in stale colors. DPR clamped to 2, ResizeObserver keeps the backing store correct, IntersectionObserver plus a visibilitychange listener pause the rAF loop offscreen/hidden; the loop otherwise never fully sleeps because even a locked plate keeps a small residual jitter (real sand vibrates, it doesn't teleport to a stop). SLIDER: a plain core-styled track (bg-border rail, bg-foreground fill, a bg-foreground thumb whose position is a left percent) beneath the plate, driven by a pointerdown/pointermove handler on the track itself that maps clientX to value directly — deliberately not left to a native range input's own drag handling, since a synthetic pointerdown/pointermove dispatched at a native thumb is not guaranteed to move it (that click-drag-to-set-value behavior is UA-internal to trusted input), which would silently break the autoplay demo. A real native input type=range sits sr-only (not display:none) inside the same track for keyboard: Tab reaches it and arrow/Home/End/PageUp/PageDown come free from native semantics; its focus state is mirrored onto the visible thumb as an accent ring (ring utilities, never focus-visible:outline-* alongside a bare outline-none, which renders invisible in this Tailwind v4 setup). Controlled (`value`/`onValueChange`) or uncontrolled (`defaultValue`, default 50). ACCESSIBILITY: aria-valuetext reports both the number and a categorical proximity phrase derived from detune — 'on target' when locked, else 'very close to target' / 'close to target' / 'off target' / 'far from target' (e.g. '42.1, very close to target') — so a screen reader user gets the same convergence signal sighted users read off the sand. A Geist Mono readout beneath the slider (VALUE / COHERENCE percent, tabular-nums) duplicates that same information as visible text, plus a plain-language state word (scattered / converging / locked), so the canvas is never the only channel. A dedicated aria-live=polite region (sr-only) holds 'On target.' exactly while locked and is empty otherwise, so it announces once on the transition into lock rather than spamming every drag tick. REDUCED MOTION: prefers-reduced-motion (checked live via a matchMedia change listener) swaps the canvas for a static, non-canvas SVG of 220 points, computed once per mode as three fixed sets — 'scattered' (a deterministic pseudo-random spread), 'locked' (the 220 grid points, from a 44x44 sample, with the smallest |f|, i.e. genuinely the nodal lines), and 'forming' (each scattered point lerped 55% toward its paired locked point) — and the live detune/lock state just SELECTS which of the three renders; there is no interpolation or motion between them, an instant swap on every value change, so the same convergence signal stays legible with zero animation. Zero dependencies."
  },
  "type": "registry:ui"
}