{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dot-display",
  "type": "registry:ui",
  "title": "Dot display",
  "description": "Square dots on one pitch, printed into a well: each dot one px colour or unlit. A slow display that steps six times a second and never tweens. The picture (a sky, a sticker) is the caller's.",
  "dependencies": [],
  "registryDependencies": [
    "https://metalui.dev/r/tokens.json"
  ],
  "files": [
    {
      "path": "packages/metalui/src/components/dot-display/dot-display.tsx",
      "type": "registry:ui",
      "target": "components/metalui/dot-display/dot-display.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\n/* ─────────────────────────────────────────────────────────\n * DOT DISPLAY: square dots on one pitch, printed into a well\n *\n *   dots      one ink index per dot, row by row; the caller draws the picture (a sky, a sticker)\n *   inks      index → a px colour, or [colour, alpha] for a dimmer dot (a glow, fog)\n *   geometry  the recipe's pitch and dot: the SVG draws whole cells, the dot-display utility masks\n *             each down to its square, so no size lives in code\n *   motion    a slow display: useDotTick steps on the recipe's step (6 a second) and never tweens;\n *             it holds still under reduced motion, in a hidden tab and while off screen\n * A part: no role of its own. The object around it names what it shows.\n * ───────────────────────────────────────────────────────── */\n\n/** The dot colours (tokens colorways px-*): an unlit dot, then the lit ones; ink marks \"now\". */\nexport type DotColour = 'off' | 'hz' | 'hill' | 'sun' | 'moon' | 'star' | 'cloud' | 'cloud-dark' | 'rain' | 'snow' | 'ink';\n/** A colour, or a colour at an alpha. */\nexport type DotInk = DotColour | readonly [DotColour, number];\n\nexport interface DotDisplayProps extends Omit<React.SVGProps<SVGSVGElement>, 'children' | 'ref'> {\n  cols: number;\n  rows: number;\n  /** One ink index per dot, row by row (`cols × rows` long). An index with no ink is unlit. */\n  dots: ArrayLike<number>;\n  inks: readonly DotInk[];\n  /** default (pitch 8, dot 6) or mini (pitch 3, dot 2.4): a glyph in a row of text. */\n  size?: 'default' | 'mini';\n}\n\nconst fillOf = (ink: DotInk | undefined) => {\n  const [colour, alpha] = typeof ink === 'string' ? [ink, 1] : ink ?? ['off', 1];\n  return { fill: colour === 'ink' ? 'var(--mu-ink)' : `var(--mu-px-${colour})`, fillOpacity: alpha };\n};\n\nexport const DotDisplay = React.forwardRef<SVGSVGElement, DotDisplayProps>(function DotDisplay({ cols, rows, dots, inks, size = 'default', className, style, ...props }, ref) {\n  // One path per ink: every dot of that ink as a unit cell.\n  const paths = React.useMemo(() => {\n    const byInk: string[] = [];\n    for (let y = 0; y < rows; y++) for (let x = 0; x < cols; x++) {\n      const i = dots[y * cols + x] ?? 0;\n      const at = inks[i] === undefined ? 0 : i;\n      byInk[at] = (byInk[at] ?? '') + `M${x} ${y}h1v1h-1Z`;\n    }\n    return byInk;\n  }, [cols, rows, dots, inks]);\n  const own = size === 'mini' ? 'dot-display dot-display-mini' : 'dot-display';\n  return (\n    <svg\n      ref={ref}\n      aria-hidden\n      viewBox={`0 0 ${cols} ${rows}`}\n      preserveAspectRatio=\"none\"\n      data-cols={cols}\n      data-rows={rows}\n      data-size={size}\n      className={className ? `${own} ${className}` : own}\n      style={{ width: `calc(${cols} * var(--dot-pitch))`, height: `calc(${rows} * var(--dot-pitch))`, ...style }}\n      {...props}\n    >\n      {paths.map((d, i) => (d ? <path key={i} d={d} data-ink={i} style={i === 0 ? fillOf('off') : fillOf(inks[i])} /> : null))}\n    </svg>\n  );\n});\n\nconst stepMs = (el: Element | null) => {\n  const raw = el ? getComputedStyle(el).getPropertyValue('--mu-r-dot-display-self-step').trim() : '';\n  const n = parseFloat(raw);\n  return raw.endsWith('ms') ? n : raw.endsWith('s') ? n * 1000 : n;\n};\n\n/**\n * The display's clock: a frame number that steps on the recipe's step. It holds still under reduced\n * motion, in a hidden tab and while `ref` is off screen, and resumes where it stopped.\n */\nexport function useDotTick(ref: React.RefObject<Element | null>, running = true) {\n  const [tick, setTick] = React.useState(0);\n  React.useEffect(() => {\n    const el = ref.current;\n    if (!running || !el) return;\n    const motion = window.matchMedia('(prefers-reduced-motion: reduce)');\n    let seen = true;\n    let timer = 0;\n    const sync = () => {\n      window.clearInterval(timer);\n      timer = 0;\n      const ms = stepMs(el);\n      if (motion.matches || document.hidden || !seen || !(ms > 0)) return;\n      timer = window.setInterval(() => setTick((t) => t + 1), ms);\n    };\n    const watch = new IntersectionObserver(([e]) => { seen = e?.isIntersecting ?? true; sync(); });\n    watch.observe(el);\n    motion.addEventListener('change', sync);\n    document.addEventListener('visibilitychange', sync);\n    sync();\n    return () => {\n      window.clearInterval(timer);\n      watch.disconnect();\n      motion.removeEventListener('change', sync);\n      document.removeEventListener('visibilitychange', sync);\n    };\n  }, [ref, running]);\n  return tick;\n}\n"
    }
  ],
  "docs": "Agent guide: https://metalui.dev/r/dot-display.md. SwiftUI: MetalDotDisplay in the MetalUI Swift package."
}
