{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sparkline-automaton",
  "title": "Sparkline Automaton",
  "description": "Inline KPI sparkline grown over an elementary cellular-automaton texture: the Wolfram rule is picked by the series' volatility, one generation column per data point.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/sparkline-automaton/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// RuleSparkline — inline KPI sparkline over an elementary cellular-automaton\n// texture. The Wolfram rule is selected by the series' volatility (coefficient\n// of variation): calm data grows a sparse automaton, volatile data a chaotic\n// one, one generation column per data point — the texture reads the data\n// instead of decorating it. CA is computed once per data update and cached to\n// an offscreen canvas; rAF exists only during the entrance sweep and active\n// scrub, cancelled otherwise.\n// ---------------------------------------------------------------------------\n\ntype Vec3 = [number, number, number];\n\n// cubic-bezier(0.22, 1, 0.36, 1) solved via Newton–Raphson\nfunction makeBezier(p1x: number, p1y: number, p2x: number, p2y: number) {\n  const cx = 3 * p1x;\n  const bx = 3 * (p2x - p1x) - cx;\n  const ax = 1 - cx - bx;\n  const cy = 3 * p1y;\n  const by = 3 * (p2y - p1y) - cy;\n  const ay = 1 - cy - by;\n  const sampleX = (t: number) => ((ax * t + bx) * t + cx) * t;\n  const sampleY = (t: number) => ((ay * t + by) * t + cy) * t;\n  const slopeX = (t: number) => (3 * ax * t + 2 * bx) * t + cx;\n  return (x: number) => {\n    if (x <= 0) return 0;\n    if (x >= 1) return 1;\n    let t = x;\n    for (let i = 0; i < 6; i++) {\n      const s = slopeX(t);\n      if (Math.abs(s) < 1e-6) break;\n      t -= (sampleX(t) - x) / s;\n    }\n    return sampleY(Math.min(1, Math.max(0, t)));\n  };\n}\nconst glideEase = makeBezier(0.22, 1, 0.36, 1);\n\nfunction parseColor(raw: string): Vec3 | null {\n  const s = raw.trim();\n  if (s.startsWith(\"#\")) {\n    const hex = s.slice(1);\n    if (hex.length === 3) {\n      const r = parseInt(hex.slice(0, 1) + hex.slice(0, 1), 16);\n      const g = parseInt(hex.slice(1, 2) + hex.slice(1, 2), 16);\n      const b = parseInt(hex.slice(2, 3) + hex.slice(2, 3), 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// deterministic per-cell hash → stable alpha texture across repaints\nfunction hash01(x: number, y: number) {\n  const n = Math.sin(x * 127.1 + y * 311.7) * 43758.5453123;\n  return n - Math.floor(n);\n}\n\n// volatility bucket → elementary rule. cv = stddev/mean, zero-mean guarded.\nfunction volatilityRule(data: number[]): { rule: number; cv: number } {\n  const n = data.length;\n  if (n < 2) return { rule: 4, cv: 0 };\n  let sum = 0;\n  for (let i = 0; i < n; i++) sum += data[i] ?? 0;\n  const mean = sum / n;\n  let sq = 0;\n  for (let i = 0; i < n; i++) {\n    const d = (data[i] ?? 0) - mean;\n    sq += d * d;\n  }\n  const sd = Math.sqrt(sq / n);\n  const denom = Math.abs(mean);\n  const cv = denom < 1e-9 ? (sd > 1e-9 ? 1 : 0) : sd / denom;\n  if (cv < 0.08) return { rule: 4, cv };\n  if (cv < 0.2) return { rule: 108, cv };\n  if (cv < 0.4) return { rule: 110, cv };\n  return { rule: 30, cv };\n}\n\n// seed row derived from the first value's IEEE-754 bits (mulberry32 walk) —\n// identical data reproduces identical texture\nfunction seedRow(rows: number, firstValue: number): Uint8Array {\n  const f = new Float32Array(1);\n  f[0] = firstValue;\n  let s = (new Uint32Array(f.buffer)[0] ?? 0) || 0x9e3779b9;\n  const out = new Uint8Array(rows);\n  for (let i = 0; i < rows; i++) {\n    s = (s + 0x6d2b79f5) | 0;\n    let t = Math.imul(s ^ (s >>> 15), 1 | s);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    out[i] = (((t ^ (t >>> 14)) >>> 0) >>> 8) & 1;\n  }\n  return out;\n}\n\nfunction nextGen(prev: Uint8Array, rule: number): Uint8Array {\n  const n = prev.length;\n  const out = new Uint8Array(n);\n  for (let i = 0; i < n; i++) {\n    const l = prev[(i - 1 + n) % n] ?? 0;\n    const c = prev[i] ?? 0;\n    const r = prev[(i + 1) % n] ?? 0;\n    out[i] = (rule >> ((l << 2) | (c << 1) | r)) & 1;\n  }\n  return out;\n}\n\nconst DEFAULT_DATA = [\n  42, 44, 43, 47, 45, 49, 46, 51, 48, 53, 50, 47, 52, 55, 51, 56, 53, 58, 55,\n  60, 57, 61, 59, 63,\n];\n\nfunction defaultFormat(v: number) {\n  return v.toLocaleString(\"en-US\", { maximumFractionDigits: 1 });\n}\n\nexport function RuleSparkline({\n  data = DEFAULT_DATA,\n  cellSize = 3,\n  entranceMs = 900,\n  strokeWidth = 1.5,\n  formatValue = defaultFormat,\n  className = \"h-14\",\n  \"aria-label\": ariaLabel = \"KPI sparkline\",\n}: {\n  /** series values, oldest → newest; one CA generation per point */\n  data?: number[];\n  /** CA cell height in px */\n  cellSize?: number;\n  /** entrance sweep duration in ms */\n  entranceMs?: number;\n  /** polyline stroke width in px */\n  strokeWidth?: number;\n  /** scrub readout formatter */\n  formatValue?: (value: number, index: number) => string;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n  /** accessible name for the sparkline. Default \"KPI sparkline\". */\n  \"aria-label\"?: string;\n}) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const readoutRef = useRef<HTMLDivElement>(null);\n  const dataRef = useRef(data);\n  const formatRef = useRef(formatValue);\n  formatRef.current = formatValue;\n  const ariaLabelRef = useRef(ariaLabel);\n  ariaLabelRef.current = ariaLabel;\n  const engineRef = useRef<{ setData: (d: number[]) => void } | null>(null);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const canvas = canvasRef.current;\n    const readout = readoutRef.current;\n    if (!root || !canvas || !readout) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n    const off = document.createElement(\"canvas\");\n    const octx = off.getContext(\"2d\");\n    if (!octx) return;\n\n    const reduced = window.matchMedia(\n      \"(prefers-reduced-motion: reduce)\"\n    ).matches;\n\n    // -- token-derived ink: read at mount, re-derived on theme class change --\n    let fg: Vec3 = [237, 237, 237];\n    let ac: Vec3 = [0, 107, 255];\n    let mu: Vec3 = [143, 143, 143];\n    const derive = () => {\n      const cs = getComputedStyle(document.documentElement);\n      fg = parseColor(cs.getPropertyValue(\"--foreground\")) ?? fg;\n      ac = parseColor(cs.getPropertyValue(\"--ns-accent\")) ?? ac;\n      mu = parseColor(cs.getPropertyValue(\"--ns-muted\")) ?? mu;\n    };\n    derive();\n\n    // -- hot-path state: locals only, never React state ---------------------\n    let w = 0;\n    let h = 0;\n    let dpr = 1;\n    let raf = 0;\n    let last = 0;\n    let cur: number[] = dataRef.current;\n    let gens: Uint8Array[] = [];\n    let rule = 4;\n    let rows = 0;\n    let xs = new Float32Array(0);\n    let ys = new Float32Array(0);\n    let pathLen = 0;\n    // sweep: -1 = settled (fully revealed)\n    let sweepStart = -1;\n    let sweepDur = entranceMs;\n    let sweepFromFrac = 0;\n    // scrub spring: k = 250 s^-2, zeta = 1.0 (critically damped)\n    const K = 250;\n    const C = 2 * Math.sqrt(K);\n    let hx = 0;\n    let hv = 0;\n    let tx = 0;\n    let alpha = 0;\n    let targetAlpha = 0;\n    let ai = -1;\n    let readoutAi = -1;\n    let readoutShown = false;\n\n    const computeCA = () => {\n      gens = [];\n      const picked = volatilityRule(cur);\n      rule = picked.rule;\n      rows = Math.max(0, Math.floor(h / cellSize));\n      if (cur.length === 0 || rows === 0) return;\n      let row = seedRow(rows, cur[0] ?? 0);\n      gens.push(row);\n      for (let i = 1; i < cur.length; i++) {\n        row = nextGen(row, rule);\n        gens.push(row);\n      }\n    };\n\n    const computeGeometry = () => {\n      const n = cur.length;\n      xs = new Float32Array(n);\n      ys = new Float32Array(n);\n      pathLen = 0;\n      if (n === 0 || w <= 0 || h <= 0) return;\n      let mn = Infinity;\n      let mx = -Infinity;\n      for (let i = 0; i < n; i++) {\n        const v = cur[i] ?? 0;\n        if (v < mn) mn = v;\n        if (v > mx) mx = v;\n      }\n      const span = mx - mn;\n      const padY = Math.max(3, strokeWidth + 2);\n      for (let i = 0; i < n; i++) {\n        xs[i] = n > 1 ? (i / (n - 1)) * w : w / 2;\n        ys[i] =\n          span > 1e-12\n            ? padY + (1 - ((cur[i] ?? 0) - mn) / span) * (h - padY * 2)\n            : h / 2;\n      }\n      for (let i = 1; i < n; i++) {\n        const dx = (xs[i] ?? 0) - (xs[i - 1] ?? 0);\n        const dy = (ys[i] ?? 0) - (ys[i - 1] ?? 0);\n        pathLen += Math.sqrt(dx * dx + dy * dy);\n      }\n    };\n\n    // CA texture painted ONCE per data/theme/size change — never per frame\n    const paintOffscreen = () => {\n      off.width = Math.max(1, Math.round(w * dpr));\n      off.height = Math.max(1, Math.round(h * dpr));\n      const n = gens.length;\n      if (w <= 0 || h <= 0 || n === 0) return;\n      octx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      octx.clearRect(0, 0, w, h);\n      const colW = w / n;\n      const gap = colW > 2.5 ? 1 : 0;\n      const cellGap = cellSize > 2 ? 1 : 0;\n      for (let i = 0; i < n; i++) {\n        const g = gens[i];\n        if (!g) continue;\n        const x = i * colW;\n        for (let r = 0; r < rows; r++) {\n          if (!g[r]) continue;\n          // foreground ink at 8–14% alpha, deterministic per cell\n          const a = 0.08 + 0.06 * hash01(i * 7.13 + 1.7, r * 3.71 + 9.2);\n          octx.fillStyle = `rgba(${fg[0]},${fg[1]},${fg[2]},${a.toFixed(3)})`;\n          octx.fillRect(\n            x,\n            r * cellSize,\n            Math.max(0.5, colW - gap),\n            cellSize - cellGap\n          );\n        }\n      }\n    };\n\n    const sweepX = (now: number) => {\n      if (sweepStart < 0) return w;\n      const p = (now - sweepStart) / sweepDur;\n      if (p >= 1) {\n        sweepStart = -1;\n        return w;\n      }\n      const from = sweepFromFrac * w;\n      return from + (w - from) * glideEase(p);\n    };\n\n    const draw = (now: number) => {\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.clearRect(0, 0, w, h);\n      if (w <= 0 || h <= 0) return;\n      const sx = Math.max(0, Math.min(w, sweepX(now)));\n      // CA backdrop revealed by clip-rect sweep (source-rect blit)\n      if (sx > 0.5 && off.width > 1) {\n        ctx.drawImage(off, 0, 0, sx * dpr, off.height, 0, 0, sx, h);\n      }\n      // polyline drawn via dash-offset technique\n      const n = cur.length;\n      if (n >= 2 && pathLen > 0) {\n        const frac = sx / w;\n        if (frac > 0.001) {\n          ctx.beginPath();\n          ctx.moveTo(xs[0] ?? 0, ys[0] ?? 0);\n          for (let i = 1; i < n; i++) ctx.lineTo(xs[i] ?? 0, ys[i] ?? 0);\n          ctx.strokeStyle = `rgb(${ac[0]},${ac[1]},${ac[2]})`;\n          ctx.lineWidth = strokeWidth;\n          ctx.lineJoin = \"round\";\n          ctx.lineCap = \"round\";\n          if (frac < 1) ctx.setLineDash([pathLen * frac, pathLen]);\n          ctx.stroke();\n          ctx.setLineDash([]);\n        }\n      }\n      // scrub hairline + snapped point marker\n      if (alpha > 0.01 && n > 0 && ai >= 0) {\n        const cx = Math.round(hx) + 0.5;\n        ctx.globalAlpha = alpha;\n        ctx.strokeStyle = `rgba(${mu[0]},${mu[1]},${mu[2]},0.7)`;\n        ctx.lineWidth = 1;\n        ctx.beginPath();\n        ctx.moveTo(cx, 0);\n        ctx.lineTo(cx, h);\n        ctx.stroke();\n        ctx.fillStyle = `rgb(${ac[0]},${ac[1]},${ac[2]})`;\n        ctx.beginPath();\n        ctx.arc(xs[ai] ?? 0, ys[ai] ?? 0, strokeWidth + 1.5, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.globalAlpha = 1;\n      }\n    };\n\n    const springActive = () =>\n      targetAlpha > 0 && (Math.abs(tx - hx) > 0.3 || Math.abs(hv) > 3);\n    const active = () =>\n      sweepStart >= 0 || springActive() || Math.abs(targetAlpha - alpha) > 0.01;\n\n    const loop = (now: number) => {\n      const dt = Math.min(0.05, last === 0 ? 1 / 60 : (now - last) / 1000);\n      last = now;\n      // critically damped spring toward the snapped target\n      const a = K * (tx - hx) - C * hv;\n      hv += a * dt;\n      hx += hv * dt;\n      if (!springActive() && targetAlpha > 0) {\n        hx = tx;\n        hv = 0;\n      }\n      alpha += (targetAlpha - alpha) * Math.min(1, dt * 14);\n      if (Math.abs(targetAlpha - alpha) <= 0.01) alpha = targetAlpha;\n      draw(now);\n      if (active()) {\n        raf = requestAnimationFrame(loop);\n      } else {\n        // genuinely idle: no rAF at rest — draw once settled, then stop\n        raf = 0;\n        last = 0;\n        draw(now);\n      }\n    };\n    const wake = () => {\n      if (raf === 0 && !reduced) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n\n    const updateReadout = () => {\n      const show = targetAlpha > 0 && ai >= 0 && ai < cur.length;\n      if (show && (readoutAi !== ai || !readoutShown)) {\n        readoutAi = ai;\n        const val = formatRef.current(cur[ai] ?? 0, ai);\n        readout.textContent = val;\n        root.setAttribute(\"aria-label\", `${ariaLabelRef.current}, ${val}`);\n        const rw = readout.offsetWidth;\n        const x = Math.max(0, Math.min(w - rw, (xs[ai] ?? 0) - rw / 2));\n        readout.style.transform = `translateX(${x.toFixed(1)}px)`;\n      }\n      if (show !== readoutShown) {\n        readoutShown = show;\n        readout.style.opacity = show ? \"1\" : \"0\";\n        if (!show) root.setAttribute(\"aria-label\", ariaLabelRef.current);\n      }\n    };\n\n    const setActive = (i: number, show: boolean) => {\n      const n = cur.length;\n      if (n === 0 || w <= 0) return;\n      ai = Math.max(0, Math.min(n - 1, i));\n      tx = xs[ai] ?? 0;\n      targetAlpha = show ? 1 : 0;\n      updateReadout();\n      if (reduced) {\n        hx = tx;\n        hv = 0;\n        alpha = targetAlpha;\n        draw(performance.now());\n      } else {\n        wake();\n      }\n    };\n\n    const rebuild = () => {\n      computeCA();\n      computeGeometry();\n      paintOffscreen();\n    };\n\n    const resize = () => {\n      const rect = root.getBoundingClientRect();\n      w = rect.width;\n      h = rect.height;\n      dpr = Math.min(2, window.devicePixelRatio || 1);\n      canvas.width = Math.max(1, Math.round(w * dpr));\n      canvas.height = Math.max(1, Math.round(h * dpr));\n      rebuild();\n      if (ai >= 0) {\n        tx = xs[ai] ?? 0;\n        readoutAi = -1; // reposition the readout against the new geometry\n        updateReadout();\n      }\n      if (raf === 0) draw(performance.now());\n    };\n    // arm the entrance BEFORE the first paint so frame 0 renders at sweep≈0\n    // (no one-frame flash of the finished texture)\n    if (!reduced && entranceMs > 0) {\n      sweepFromFrac = 0;\n      sweepDur = entranceMs;\n      sweepStart = performance.now();\n    }\n    resize();\n\n    const samePrefix = (a: number[], b: number[]) => {\n      for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n      return true;\n    };\n\n    const setData = (d: number[]) => {\n      if (d === cur) return;\n      const prev = cur;\n      const prevRule = rule;\n      cur = d;\n      const picked = volatilityRule(cur);\n      // tail push: same prefix, same rule bucket → extrude only new columns\n      const isTail =\n        prev.length > 0 &&\n        cur.length > prev.length &&\n        picked.rule === prevRule &&\n        gens.length === prev.length &&\n        rows > 0 &&\n        samePrefix(prev, cur);\n      if (isTail) {\n        rule = picked.rule;\n        let row = gens[gens.length - 1] ?? seedRow(rows, cur[0] ?? 0);\n        for (let i = prev.length; i < cur.length; i++) {\n          row = nextGen(row, rule);\n          gens.push(row);\n        }\n        computeGeometry();\n        paintOffscreen();\n      } else {\n        rebuild();\n      }\n      if (ai >= cur.length) ai = cur.length - 1;\n      if (ai >= 0) {\n        tx = xs[ai] ?? 0;\n        readoutAi = -1; // force readout refresh\n        updateReadout();\n      }\n      if (reduced || w <= 0) {\n        draw(performance.now());\n        return;\n      }\n      // 200ms sweep over the new tail only (full width on replacement)\n      sweepFromFrac =\n        isTail && cur.length > 1 ? (prev.length - 1) / (cur.length - 1) : 0;\n      sweepDur = 200;\n      sweepStart = performance.now();\n      wake();\n    };\n    engineRef.current = { setData };\n\n    // -- entrance: 900ms left→right reveal, skipped under reduced motion ----\n    if (!reduced && entranceMs > 0) wake();\n\n    // -- events -------------------------------------------------------------\n    const onMove = (e: PointerEvent) => {\n      const rect = canvas.getBoundingClientRect();\n      const n = cur.length;\n      if (n === 0 || rect.width <= 0) return;\n      const x = e.clientX - rect.left;\n      setActive(Math.round((x / rect.width) * (n - 1)), true);\n    };\n    const onLeave = () => {\n      targetAlpha = 0;\n      updateReadout();\n      if (reduced) {\n        alpha = 0;\n        draw(performance.now());\n      } else {\n        wake();\n      }\n    };\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key !== \"ArrowLeft\" && e.key !== \"ArrowRight\") return;\n      e.preventDefault();\n      const step = e.key === \"ArrowLeft\" ? -1 : 1;\n      setActive((ai < 0 ? cur.length - 1 : ai) + step, true);\n    };\n    const onBlur = () => onLeave();\n    root.addEventListener(\"pointermove\", onMove);\n    root.addEventListener(\"pointerdown\", onMove);\n    root.addEventListener(\"pointerleave\", onLeave);\n    root.addEventListener(\"keydown\", onKey);\n    root.addEventListener(\"blur\", onBlur);\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(root);\n    // live theme re-derive: repaint the cached CA in the new ink\n    const mo = new MutationObserver(() => {\n      derive();\n      paintOffscreen();\n      if (raf === 0) draw(performance.now());\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      mo.disconnect();\n      root.removeEventListener(\"pointermove\", onMove);\n      root.removeEventListener(\"pointerdown\", onMove);\n      root.removeEventListener(\"pointerleave\", onLeave);\n      root.removeEventListener(\"keydown\", onKey);\n      root.removeEventListener(\"blur\", onBlur);\n      engineRef.current = null;\n    };\n  }, [cellSize, entranceMs, strokeWidth]);\n\n  useEffect(() => {\n    dataRef.current = data;\n    engineRef.current?.setData(data);\n  }, [data]);\n\n  return (\n    <div\n      ref={rootRef}\n      role=\"group\"\n      aria-label={ariaLabel}\n      tabIndex={0}\n      style={{ touchAction: \"pan-y\" }}\n      className={`relative w-full cursor-crosshair overflow-hidden rounded-sm outline-none transition-colors duration-200 hover:bg-foreground/[0.03] focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent ${className}`}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden\n        className=\"absolute inset-0 h-full w-full\"\n      />\n      <div\n        ref={readoutRef}\n        role=\"status\"\n        aria-live=\"polite\"\n        aria-atomic=\"true\"\n        className=\"pointer-events-none absolute left-0 top-0.5 rounded-sm border border-border bg-surface px-1.5 py-0.5 font-mono text-[10px] tabular-nums text-foreground opacity-0 transition-opacity duration-150\"\n      />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/sparkline-automaton.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",
      "data-viz",
      "sparkline",
      "cellular-automaton",
      "generative",
      "scrub",
      "kpi"
    ],
    "instruction": "An inline KPI sparkline on a single DPR-aware (clamp 2) Canvas 2D where the backdrop is an elementary 1D cellular automaton that reads the data instead of decorating it. CA: cells 3px tall, state vector vertical, one generation COLUMN per data point advancing left to right — each data point extrudes the next generation. The Wolfram rule is selected by the series' coefficient of variation (stddev/mean, zero-mean and zero-size guarded): cv < 0.08 → rule 4 (sparse), 0.08–0.2 → rule 108, 0.2–0.4 → rule 110, > 0.4 → rule 30 (chaotic). The seed row derives from the first value's IEEE-754 bits through a mulberry32 walk so identical data reproduces identical texture. The CA is computed once per data update and painted to a cached offscreen canvas in --foreground ink at 8–14% per-cell hashed alpha — never per frame. Motion is entrance-only: CA and the --ns-accent polyline reveal left→right over 900ms with cubic-bezier(0.22,1,0.36,1) — the CA via a clip-rect (source-rect blit) sweep, the line via canvas setLineDash([pathLen*frac, pathLen]) dash-offset; a data push extrudes only the new tail generations and re-sweeps just that region over 200ms. Cursor scrub: a vertical hairline eases to the snapped nearest-point x with a critically damped spring (k=250 s^-2, zeta=1.0) plus an accent dot on the line and a font-mono DOM readout positioned by direct transform writes; the wrapper is focusable, shows a visible focus ring, and left/right arrows step the readout, which is an aria-live region and also mirrors its value into the wrapper's aria-label while scrubbing. rAF exists only during entrance/tail sweeps and unsettled scrub — cancelled otherwise, the texture is the ambient interest. All drawn colors (--foreground, --ns-accent, --ns-muted) are read via getComputedStyle at mount and re-derived (offscreen CA repainted) by a MutationObserver on documentElement class changes so both themes survive; ResizeObserver recomputes the cell grid and geometry; every observer/listener/rAF torn down on unmount. prefers-reduced-motion: finished CA texture and full line painted instantly, scrub readout still works with the hairline snapping instead of easing. Demo: three stacked metric tiles (calm Revenue / moderate Latency / volatile Errors) on a surface card with mono labels, large tabular numbers, success/error delta pills, and per-tile RULE/CV captions so the rule-vs-volatility mapping is visible by comparison."
  },
  "type": "registry:ui"
}