{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "meter-matrix-scan",
  "title": "Meter Matrix Scan",
  "description": "A level meter rendered as a genuinely row-multiplexed LED dot-matrix panel: five rows addressed round-robin off a real 240Hz-per-row clock, and each LED's brightness is a literal 8-step PWM duty-cycle quantization rather than a smooth analog fill. The row-address concept shows as a calm, low-amplitude brightness gradient a couple of rows wide, drifting slowly down the panel and back: legible on a second look, never a strobe.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/meter-matrix-scan/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// MeterMatrixScan — a level meter rendered as a genuinely row-multiplexed\n// LED dot-matrix panel.\n//\n// SOURCE, NOT INVENTED: cheap commercial LED dot-matrix signage (scrolling\n// ticker boards, gym scoreboards, elevator floor indicators, budget\n// character displays built on row/column multiplex driver chips) can't\n// afford one continuous driver per LED. Instead the panel scans one row at\n// a time at a frequency well above human flicker fusion, relying on\n// persistence of vision to read a complete static image, and within each\n// row's brief active slice an individual LED's apparent brightness is set\n// by PWM (pulse-width modulation) duty cycle — fully on or fully off at any\n// instant, perceived brightness being the fraction of the row's active\n// window it spends on, quantized to a small number of duty steps. That\n// quantized time-division duty cycle (not a continuous alpha/density value)\n// is this component's entire identity, distinct from every other\n// ASCII/glyph-luminance component in the registry.\n//\n// THE ROW-SCAN ARTIFACT: a real multiplex board's per-row scan is invisible\n// at rest — that's the whole point of persistence of vision. It only ever\n// becomes visible when something samples the panel at a rate that doesn't\n// divide evenly into its own full-panel refresh, e.g. filming an LED sign\n// with a camera whose shutter/frame rate doesn't line up with the panel's\n// scan rate produces a soft band drifting through the image (the reason\n// dashcam footage sometimes shows a faint bar through LED signage). A literal\n// 1:1 real-time render of the raw ROW_SCAN_HZ clock against a ~60Hz browser\n// paint rate aliases close enough to the paint rate itself to read as a hard\n// strobe/flicker — a rendering-pipeline artifact, not the calm hardware\n// phenomenon it's meant to represent. So the scan address (ROW_SCAN_HZ,\n// documented below, is the real underlying clock) is deliberately mapped onto\n// a slow, continuous sweep position instead of a discrete per-frame row\n// index: a soft brightness gradient a couple of rows wide, low amplitude,\n// drifting the full height of the panel and back over several seconds — the\n// same round-robin row addressing concept, legible on a second look, without\n// ever strobing near the paint rate.\n// ---------------------------------------------------------------------------\n\nconst ROWS = 5;\nconst ROW_SCAN_HZ = 240; // real per-row multiplex clock this component represents\nconst PWM_LEVELS = 8; // 3-bit duty-cycle depth\nconst GUTTER_PX = 3; // gap between dots, ~2-4px per spec\nconst SWEEP_PERIOD_S = 7.5; // one full down-and-back sweep across the panel\nconst SWEEP_SIGMA_ROWS = 1.4; // gradient softness, in rows — wide, not a 1-row strip\nconst SCAN_HIGHLIGHT_ALPHA = 0.055; // luminance-only, low-amplitude boost at the sweep's center\nconst MIN_ON_ALPHA = 0.16; // floor so PWM band 1/8 never rounds to invisible in light theme\n\n// slow generative \"sensor\" field used only when no external `value` prop is\n// supplied — three non-commensurate traveling sine components, amplitude\n// bounded so the simulated reading stays comfortably inside 0..100.\nfunction sensorValue(t: number) {\n  const f =\n    Math.sin(t * 0.17) + 0.5 * Math.sin(t * 0.43 + 1.3) + 0.3 * Math.sin(t * 0.08 + 2.1);\n  const norm = f / 1.8; // amplitude sum 1.8 -> -1..1\n  return 55 + norm * 32; // ~23..87\n}\n\n// continuous sweep position, in row units (0..ROWS-1), a slow triangle wave\n// so the drift is smooth and reverses without a jump-cut at either end\nfunction sweepPosition(t: number) {\n  const phase = (t % SWEEP_PERIOD_S) / SWEEP_PERIOD_S; // 0..1\n  const tri = phase < 0.5 ? phase * 2 : 2 - phase * 2; // 0 -> 1 -> 0\n  return tri * (ROWS - 1);\n}\n\nexport interface MeterMatrixScanProps {\n  /** current level, 0..max. Omit to drive the meter from an internal simulated sensor read. */\n  value?: number;\n  /** domain ceiling for value. Default 100. */\n  max?: number;\n  /** accessible name for the reading, e.g. \"CPU load\". Default \"Level\". */\n  label?: string;\n  /** panel height in px; ROWS=5 fixed, cell size derives as height / 5. Default 60. */\n  height?: number;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function MeterMatrixScan({\n  value,\n  max = 100,\n  label = \"Level\",\n  height = 60,\n  className = \"\",\n}: MeterMatrixScanProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const wrapperRef = useRef<HTMLDivElement>(null);\n  const valueRef = useRef(value);\n  valueRef.current = value;\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    const wrapper = wrapperRef.current;\n    if (!canvas || !wrapper) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    let bg = \"\";\n    let fg = \"\";\n\n    const cellSize = Math.max(2, height / ROWS);\n    const radius = Math.max(1, (cellSize - GUTTER_PX) / 2);\n\n    let cols = 0;\n    let sized = false;\n    let lastWidth = 0;\n\n    const readTokens = () => {\n      const root = getComputedStyle(document.documentElement);\n      // fallbacks are CSS keywords, never literal colour values\n      bg = root.getPropertyValue(\"--background\").trim() || \"transparent\";\n      fg = root.getPropertyValue(\"--foreground\").trim() || \"currentColor\";\n    };\n\n    const resize = () => {\n      const { width } = canvas.getBoundingClientRect();\n      if (width < 2) {\n        sized = false;\n        return;\n      }\n      if (sized && Math.abs(width - lastWidth) < 1) return;\n      lastWidth = width;\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.max(1, Math.round(width * dpr));\n      canvas.height = Math.max(1, Math.round(height * dpr));\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      cols = Math.max(6, Math.floor(width / cellSize));\n      sized = true;\n    };\n\n    // globalT: seconds, drives both the simulated sensor field (when\n    // `value` is uncontrolled) and the sweep position. Never resets.\n    let globalT = 0;\n\n    const currentValue = () => {\n      const external = valueRef.current;\n      const v = external === undefined ? sensorValue(globalT) : external;\n      return Math.min(max, Math.max(0, v));\n    };\n\n    const draw = (sweepPos: number | null) => {\n      if (!sized) return;\n      const w = cols * cellSize;\n      ctx.clearRect(0, 0, w, height);\n      ctx.fillStyle = bg;\n      ctx.fillRect(0, 0, w, height);\n\n      const filled = (currentValue() / max) * cols;\n      const fullCols = Math.min(cols, Math.floor(filled));\n      const frac = filled - fullCols;\n      // fractional boundary LED's brightness is a literal quantized PWM\n      // duty step, never a smooth alpha ramp\n      const boundaryLevel = Math.round(frac * PWM_LEVELS);\n\n      for (let r = 0; r < ROWS; r++) {\n        const cy = r * cellSize + cellSize / 2;\n        // soft Gaussian weight of this row against the sweep's current\n        // center — a wide, low-amplitude bump, not a hard on/off strip, so\n        // several rows share a gentle gradient rather than one strobing\n        let sweepBoost = 0;\n        if (sweepPos !== null) {\n          const d = r - sweepPos;\n          sweepBoost = SCAN_HIGHLIGHT_ALPHA * Math.exp(-(d * d) / (2 * SWEEP_SIGMA_ROWS * SWEEP_SIGMA_ROWS));\n        }\n        for (let c = 0; c < cols; c++) {\n          let level = 0;\n          if (c < fullCols) level = PWM_LEVELS;\n          else if (c === fullCols) level = boundaryLevel;\n          if (level <= 0) continue;\n\n          let alpha = level / PWM_LEVELS;\n          alpha = Math.max(MIN_ON_ALPHA, alpha);\n          alpha = Math.min(1, alpha + sweepBoost);\n\n          const cx = c * cellSize + cellSize / 2;\n          ctx.globalAlpha = alpha;\n          ctx.fillStyle = fg;\n          ctx.beginPath();\n          ctx.arc(cx, cy, radius, 0, Math.PI * 2);\n          ctx.fill();\n        }\n\n        // the same soft gradient laid across only the off (background)\n        // columns of this row — what makes the sweep legible independent of\n        // the value fill, luminance only, never a hard band. Confined to the\n        // unlit region so it never stacks on top of an already-boosted dot.\n        const offStartX = Math.min(w, (fullCols + 1) * cellSize);\n        if (sweepBoost > 0.002 && offStartX < w) {\n          ctx.globalAlpha = sweepBoost * 0.7;\n          ctx.fillStyle = fg;\n          ctx.fillRect(offStartX, r * cellSize, w - offStartX, cellSize);\n        }\n      }\n      ctx.globalAlpha = 1;\n\n      wrapper.setAttribute(\"aria-valuenow\", String(Math.round(currentValue())));\n    };\n\n    // -- loop ----------------------------------------------------------------\n    let raf = 0;\n    let last = 0;\n\n    const loop = (now: number) => {\n      const dtMs = last ? Math.min(250, now - last) : 1000 / 60;\n      last = now;\n      globalT += dtMs / 1000;\n      draw(sweepPosition(globalT));\n      if (!document.hidden) raf = requestAnimationFrame(loop);\n    };\n\n    const mo = new MutationObserver(() => {\n      readTokens();\n      if (reduced) draw(null);\n    });\n    mo.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    let resizeTimer: ReturnType<typeof setTimeout> | null = null;\n    const onResize = () => {\n      if (resizeTimer) clearTimeout(resizeTimer);\n      resizeTimer = setTimeout(() => {\n        resizeTimer = null;\n        readTokens();\n        resize();\n        draw(reduced ? null : sweepPosition(globalT));\n      }, 150);\n    };\n    const ro = new ResizeObserver(onResize);\n    ro.observe(canvas);\n\n    const io = new IntersectionObserver(\n      (entries) => {\n        const visible = entries[0]?.isIntersecting;\n        if (visible && !reduced && sized) {\n          cancelAnimationFrame(raf);\n          last = 0;\n          raf = requestAnimationFrame(loop);\n        } else if (!visible) {\n          cancelAnimationFrame(raf);\n        }\n      },\n      { threshold: 0 }\n    );\n    io.observe(canvas);\n\n    const onVis = () => {\n      cancelAnimationFrame(raf);\n      if (!document.hidden && !reduced && sized) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVis);\n\n    // no paint before the first token read\n    readTokens();\n    resize();\n\n    if (reduced) {\n      // freeze with the sweep locked off entirely — no gradient visible,\n      // every row rendered as if simultaneously lit at its true duty level —\n      // at a value past the field's cold-start instant, same convention as\n      // this registry's other generative components.\n      globalT = 1.4;\n      draw(null);\n    } else {\n      draw(sweepPosition(globalT));\n      raf = requestAnimationFrame(loop);\n    }\n\n    return () => {\n      cancelAnimationFrame(raf);\n      if (resizeTimer) clearTimeout(resizeTimer);\n      mo.disconnect();\n      ro.disconnect();\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVis);\n    };\n  }, [height, max]);\n\n  return (\n    <div\n      ref={wrapperRef}\n      role=\"meter\"\n      aria-label={label}\n      aria-valuemin={0}\n      aria-valuemax={max}\n      className={`ns-mms w-full ${className}`}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden=\"true\"\n        className=\"block w-full\"\n        style={{ height }}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/meter-matrix-scan.tsx"
    }
  ],
  "meta": {
    "collection": "core",
    "tags": [
      "meter",
      "level",
      "gauge",
      "led",
      "dot-matrix",
      "pwm",
      "multiplex",
      "canvas",
      "monochrome"
    ],
    "instruction": "Build <MeterMatrixScan value? max? label? height? className?> as a full-width <canvas> panel wrapped in a <div role=\"meter\" aria-label={label} aria-valuemin={0} aria-valuemax={max}>, a drop-in level/value meter. SOURCE, NOT INVENTED: cheap commercial LED dot-matrix signage (scrolling ticker boards, gym scoreboards, elevator floor indicators, budget character displays built on row/column multiplex driver chips) can't afford one continuous driver per LED — the panel scans one row at a time at a frequency well above human flicker fusion, relying on persistence of vision to read a complete static image, and within each row's brief active slice an individual LED's apparent brightness is set by PWM (pulse-width modulation) duty cycle: fully on or fully off at any instant, perceived brightness being the fraction of the row's active window it spends on, quantized to a small number of duty steps (a driver's PWM bit depth). That literal quantized time-division duty cycle, never a continuous alpha/density value, is this component's entire identity and must remain visibly stepped, not anti-aliased into a smooth gradient. REAL NUMBERS: ROWS=5 (fixed); ROW_SCAN_HZ=240, each row the active scan target for 1000/240 ~= 4.17ms, a full 5-row panel completing one full-frame refresh every ~20.8ms (~48Hz), comfortably above flicker fusion, matching real multiplex-board practice; PWM_LEVELS=8, a 3-bit duty-cycle depth typical of budget driver chips. GEOMETRY: cellSize = containerHeight / 5 (square cells, so exactly 5 rows fill the panel's own height); columns = floor(containerWidth / cellSize); each LED is drawn as a filled circle (canvas arc), radius = (cellSize - gutterPx) / 2, gutterPx in the 2-4px range, not a Unicode glyph. VALUE / FILL: the meter reads a single scalar 0..max mapped left-to-right across the column count — filled = (value/max) * cols; every column below the integer floor of filled is drawn at full PWM_LEVELS brightness; the single boundary column at that floor is drawn at a duty level quantized from its fractional remainder (Math.round(frac * PWM_LEVELS), 0..8 discrete steps) — this is the real technique budget LED bargraphs use to fake sub-LED resolution by PWM-dimming exactly one boundary LED rather than genuinely lighting a fractional pixel; columns past the boundary are undrawn (--background shows through, meaning fully off). SIMULATED SENSOR, WHEN value IS OMITTED: when the value prop isn't supplied, a self-contained generative field drives the reading continuously — three non-commensurate traveling sine components on the component's own never-resetting clock, amplitude-bounded to stay inside roughly 23-87 on a 0-100 domain — so the meter is a genuine 'alive at rest' ambient demo with zero external wiring; when value IS supplied, that value renders directly and is the source of truth, letting the same component serve as a real production meter. ROW-SCAN, RENDERED CALM ON PURPOSE: a real board's per-row scan is invisible at rest (that's the entire point of persistence of vision) and only becomes visible when something samples the panel at a rate that doesn't divide evenly into its own refresh rate — the well-documented reason a camera filming multiplexed LED signage sometimes shows a faint drifting band. A literal 1:1 real-time render of the raw ROW_SCAN_HZ clock against a ~60Hz browser paint rate aliases close enough to the paint rate itself to read as a hard strobe/flicker rather than that calm hardware phenomenon — a rendering-pipeline artifact, not the mechanic. So the row-address concept is deliberately mapped onto a slow, continuous sweep position instead of a discrete per-frame row index: sweepPosition(t) is a triangle wave over SWEEP_PERIOD_S=7.5s (one full down-and-back pass across the 5 rows, smooth, no jump-cut at either end). Each row's brightness contribution is a soft Gaussian weight against the sweep's current center (SWEEP_SIGMA_ROWS=1.4, so 2-3 rows share a gentle gradient, never a 1-row strip) scaled by a low-amplitude SCAN_HIGHLIGHT_ALPHA=0.055 — luminance-only (never accent), added onto an already-lit LED's alpha for lit columns, and painted as a fainter version of the same gradient across only the off/background columns of that row (never stacking on top of an already-boosted dot) so the sweep reads independent of how much of the meter is filled. LIGHT THEME: the lowest PWM band (1 of 8, alpha 1/8 = 0.125) is floored to a MIN_ON_ALPHA (0.16) so it never rounds to invisible against a near-white --background. TOKENS: bg/fg are read once from getComputedStyle(document.documentElement) against --background/--foreground before the first paint, re-derived on a documentElement class MutationObserver so a theme flip is live; --ns-accent never appears anywhere in the LED brightness/duty-cycle mapping (luminance only, per spec) — there is no pointer-scrub affordance in this build, so accent is never reached for at all. A ResizeObserver on the canvas reflows on container size changes; an IntersectionObserver pauses the rAF loop off-screen and resumes it (fresh last-timestamp, no giant delta-time jump) on re-entry. RESTING LOOP: t0/2.5s/5s differ on two independent axes compounding — the sweep's slow triangle-wave position (always cycling, real clock) and, in uncontrolled mode, the slowly drifting simulated value — so the lit pattern, the PWM boundary step, and the sweep's gradient position are all visibly different at every timestamp, unbounded, zero input, and calm rather than flickering. prefers-reduced-motion freezes with the sweep locked off entirely (no gradient overlay drawn at all, every row rendered identically at its true duty level) at a value taken past the field's cold-start instant (uncontrolled mode) or the supplied static value (controlled mode) — the fully legible static readout with the PWM quantization bands still visible, no sweep artifact. The render loop pauses on document.hidden and resumes cleanly on visibilitychange. Direct-DOM rAF, zero React state on the hot path (value is read through a ref so a controlled prop updates without re-subscribing the effect), zero dependencies. A11Y: role=meter with aria-valuemin/aria-valuemax fixed from props and aria-valuenow written imperatively to the wrapper on every draw (cheap DOM attribute write, no aria-live spam) so assistive tech querying the node gets the true current reading whether it's externally controlled or internally simulated; the canvas itself is aria-hidden decoration. There is no keyboard surface because there is nothing to operate — non-interactive by design, correctly exempt from the registry's tab-reachability check as a display-only meter. Props: value (0..max, omit for the internal simulated sensor read), max (domain ceiling, default 100), label (accessible name, default 'Level'), height (panel height px, default 60; ROWS is fixed at 5, cell size derives as height / 5), className."
  },
  "type": "registry:ui"
}