{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chargen-rom-slice",
  "title": "Chargen ROM Slice",
  "description": "A status glyph that reconstructs a real 1970s-80s CRT character-generator ROM read pattern: each glyph in a short string is built top-to-bottom from horizontal scanline slices arriving on a beam-synced sweep, so most on-screen glyphs are only ever a clean stack of fetched rows sitting above a hard-edged gap, whole for a single instant per cycle.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/chargen-rom-slice/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// ChargenRomSlice — a status glyph that reconstructs the way 1970s-80s CRT\n// text-mode video controllers actually drew characters: a character-generator\n// ROM stores each glyph as an 8-row bitmap, but the controller never fetches\n// a whole glyph in one go. It reads the ROM one scanline row at a time, in\n// step with the beam, shifts that row of bits out across the row, then drops\n// to the next row and re-addresses the ROM for the next slice. A glyph is\n// only ever \"whole\" for an instant, once per frame — every other instant it\n// is a stack of independently-fetched horizontal slices with a hard gap\n// below the last one fetched.\n//\n// Each on-screen character gets its own 35ms-per-row sweep (row N becomes\n// visible at N*35ms into that character's build), 8 rows -> 280ms per glyph.\n// Characters start staggered 60ms apart left-to-right, so a 6-character\n// string is fully clean ~580ms after the cycle starts. It then holds fully\n// assembled for 500ms before every row resets to 0 and the sweep restarts —\n// a single fixed 1080ms cycle, run on real elapsed time (not frame count) so\n// speed never depends on paint rate.\n// ---------------------------------------------------------------------------\n\n// Built-in 5x7 block font (uppercase A-Z, 0-9, space, punctuation used by the\n// default string) with a blank 8th row appended — real chargen ROMs commonly\n// address an 8- or 9-row cell for a 7-row glyph, leaving a descender row\n// blank. Hand-drawn geometric shapes, not a traced copy of a real ROM dump.\nconst FONT: Record<string, string[]> = {\n  A: [\"01110\", \"10001\", \"10001\", \"11111\", \"10001\", \"10001\", \"10001\", \"00000\"],\n  B: [\"11110\", \"10001\", \"10001\", \"11110\", \"10001\", \"10001\", \"11110\", \"00000\"],\n  C: [\"01111\", \"10000\", \"10000\", \"10000\", \"10000\", \"10000\", \"01111\", \"00000\"],\n  D: [\"11110\", \"10001\", \"10001\", \"10001\", \"10001\", \"10001\", \"11110\", \"00000\"],\n  E: [\"11111\", \"10000\", \"10000\", \"11110\", \"10000\", \"10000\", \"11111\", \"00000\"],\n  F: [\"11111\", \"10000\", \"10000\", \"11110\", \"10000\", \"10000\", \"10000\", \"00000\"],\n  G: [\"01111\", \"10000\", \"10000\", \"10011\", \"10001\", \"10001\", \"01110\", \"00000\"],\n  H: [\"10001\", \"10001\", \"10001\", \"11111\", \"10001\", \"10001\", \"10001\", \"00000\"],\n  I: [\"01110\", \"00100\", \"00100\", \"00100\", \"00100\", \"00100\", \"01110\", \"00000\"],\n  J: [\"00111\", \"00010\", \"00010\", \"00010\", \"00010\", \"10010\", \"01100\", \"00000\"],\n  K: [\"10001\", \"10010\", \"10100\", \"11000\", \"10100\", \"10010\", \"10001\", \"00000\"],\n  L: [\"10000\", \"10000\", \"10000\", \"10000\", \"10000\", \"10000\", \"11111\", \"00000\"],\n  M: [\"10001\", \"11011\", \"10101\", \"10101\", \"10001\", \"10001\", \"10001\", \"00000\"],\n  N: [\"10001\", \"11001\", \"10101\", \"10101\", \"10011\", \"10001\", \"10001\", \"00000\"],\n  O: [\"01110\", \"10001\", \"10001\", \"10001\", \"10001\", \"10001\", \"01110\", \"00000\"],\n  P: [\"11110\", \"10001\", \"10001\", \"11110\", \"10000\", \"10000\", \"10000\", \"00000\"],\n  Q: [\"01110\", \"10001\", \"10001\", \"10001\", \"10101\", \"10010\", \"01101\", \"00000\"],\n  R: [\"11110\", \"10001\", \"10001\", \"11110\", \"10100\", \"10010\", \"10001\", \"00000\"],\n  S: [\"01111\", \"10000\", \"10000\", \"01110\", \"00001\", \"00001\", \"11110\", \"00000\"],\n  T: [\"11111\", \"00100\", \"00100\", \"00100\", \"00100\", \"00100\", \"00100\", \"00000\"],\n  U: [\"10001\", \"10001\", \"10001\", \"10001\", \"10001\", \"10001\", \"01110\", \"00000\"],\n  V: [\"10001\", \"10001\", \"10001\", \"10001\", \"10001\", \"01010\", \"00100\", \"00000\"],\n  W: [\"10001\", \"10001\", \"10001\", \"10101\", \"10101\", \"11011\", \"10001\", \"00000\"],\n  X: [\"10001\", \"10001\", \"01010\", \"00100\", \"01010\", \"10001\", \"10001\", \"00000\"],\n  Y: [\"10001\", \"10001\", \"01010\", \"00100\", \"00100\", \"00100\", \"00100\", \"00000\"],\n  Z: [\"11111\", \"00010\", \"00100\", \"01000\", \"10000\", \"10000\", \"11111\", \"00000\"],\n  \"0\": [\"01110\", \"10001\", \"10011\", \"10101\", \"11001\", \"10001\", \"01110\", \"00000\"],\n  \"1\": [\"00100\", \"01100\", \"00100\", \"00100\", \"00100\", \"00100\", \"01110\", \"00000\"],\n  \"2\": [\"01110\", \"10001\", \"00001\", \"00010\", \"00100\", \"01000\", \"11111\", \"00000\"],\n  \"3\": [\"11111\", \"00010\", \"00100\", \"00010\", \"00001\", \"10001\", \"01110\", \"00000\"],\n  \"4\": [\"00010\", \"00110\", \"01010\", \"10010\", \"11111\", \"00010\", \"00010\", \"00000\"],\n  \"5\": [\"11111\", \"10000\", \"11110\", \"00001\", \"00001\", \"10001\", \"01110\", \"00000\"],\n  \"6\": [\"00110\", \"01000\", \"10000\", \"11110\", \"10001\", \"10001\", \"01110\", \"00000\"],\n  \"7\": [\"11111\", \"00001\", \"00010\", \"00100\", \"01000\", \"01000\", \"01000\", \"00000\"],\n  \"8\": [\"01110\", \"10001\", \"10001\", \"01110\", \"10001\", \"10001\", \"01110\", \"00000\"],\n  \"9\": [\"01110\", \"10001\", \"10001\", \"01111\", \"00001\", \"00010\", \"01100\", \"00000\"],\n  \" \": [\"00000\", \"00000\", \"00000\", \"00000\", \"00000\", \"00000\", \"00000\", \"00000\"],\n  \".\": [\"00000\", \"00000\", \"00000\", \"00000\", \"00000\", \"01100\", \"01100\", \"00000\"],\n  \"_\": [\"00000\", \"00000\", \"00000\", \"00000\", \"00000\", \"00000\", \"11111\", \"00000\"],\n};\n\nconst GLYPH_W = 5;\nconst GLYPH_H = 8;\nconst GLYPH_GAP = 1; // blank columns between characters\nconst ROW_MS = 35; // one ROM scanline slice revealed every 35ms (~28 rows/s)\nconst CHAR_STAGGER_MS = 60; // each character's sweep starts 60ms after the previous\nconst HOLD_MS = 500; // full-string clean hold before the sweep resets to row 0\n\n// Frozen prefers-reduced-motion frame: the live sweep's row-reveal math\n// evaluated at a fixed 300ms cycle position — an early character fully\n// assembled, the next mid-sweep with a visible hard-edged slice seam, later\n// ones still thin or untouched. A named cycle position (not a hardcoded\n// per-index array) so it stays a genuine sweep frame for any string length.\nconst FREEZE_MS = 300;\n\n// Live sweep at t0 is seeded this far into its own cycle rather than at 0 so\n// the very first paint is already mid-sweep (per the resting-loop spec: t0\n// shows glyphs with only their top rows revealed, not a blank canvas). 170\n// is chosen so the cycle-phase-locked t0/2.5s/5s samples (1080ms cycle, 2.5s\n// and 5s land 340ms/680ms further into the cycle) land in three distinct\n// states — mid-sweep, a later mid-sweep, and the clean hold — rather than\n// two of the three sample points both landing inside the 500ms hold.\nconst T0_PHASE_MS = 170;\n\nexport interface ChargenRomSliceProps {\n  /** status string; unsupported characters render as a blank cell */\n  text?: string;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function ChargenRomSlice({\n  text = \"BUFFER\",\n  className = \"\",\n}: ChargenRomSliceProps) {\n  const wrapRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    const wrap = wrapRef.current;\n    const canvas = canvasRef.current;\n    if (!wrap || !canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const chars = Array.from(text.toUpperCase()).slice(0, 12);\n    const cols = chars.length * GLYPH_W + Math.max(0, chars.length - 1) * GLYPH_GAP;\n    const rows = GLYPH_H;\n    const glyphMs = GLYPH_H * ROW_MS; // 280ms — one full glyph sweep\n    const buildMs = (chars.length - 1) * CHAR_STAGGER_MS + glyphMs; // full-string assembly\n    const cycleMs = buildMs + HOLD_MS;\n\n    // resolve which grid cells are \"on\" once — the font map never changes\n    const bitmap: boolean[][] = Array.from({ length: rows }, () => new Array(cols).fill(false));\n    let cx = 0;\n    for (const ch of chars) {\n      const glyph = FONT[ch] ?? FONT[\" \"];\n      for (let r = 0; r < GLYPH_H; r++) {\n        for (let c = 0; c < GLYPH_W; c++) {\n          if (glyph[r][c] === \"1\") bitmap[r][cx + c] = true;\n        }\n      }\n      cx += GLYPH_W + GLYPH_GAP;\n    }\n\n    let fg = getComputedStyle(canvas).color;\n    let cellPx = 12;\n    let dpr = 1;\n    let raf = 0;\n    let cycleStart = 0;\n    let resizeTimer: ReturnType<typeof setTimeout> | null = null;\n\n    // rows currently revealed for each character, 0..GLYPH_H\n    const revealed = new Array(chars.length).fill(0);\n\n    const computeRevealed = (cyclePos: number) => {\n      if (cyclePos >= buildMs) {\n        revealed.fill(GLYPH_H);\n        return;\n      }\n      for (let i = 0; i < chars.length; i++) {\n        const elapsed = cyclePos - i * CHAR_STAGGER_MS;\n        revealed[i] = elapsed <= 0 ? 0 : Math.min(GLYPH_H, Math.floor(elapsed / ROW_MS));\n      }\n    };\n\n    const draw = () => {\n      const width = cols * cellPx;\n      const height = rows * cellPx;\n      ctx.clearRect(0, 0, width, height);\n      ctx.fillStyle = fg;\n      let cxCol = 0;\n      for (let i = 0; i < chars.length; i++) {\n        const n = revealed[i];\n        for (let r = 0; r < n; r++) {\n          for (let c = 0; c < GLYPH_W; c++) {\n            if (!bitmap[r][cxCol + c]) continue;\n            // full cellPx, no inter-row gap — the only horizontal\n            // discontinuity in a glyph must be the sweep seam itself\n            ctx.fillRect((cxCol + c) * cellPx, r * cellPx, cellPx, cellPx);\n          }\n        }\n        cxCol += GLYPH_W + GLYPH_GAP;\n      }\n    };\n\n    const resize = () => {\n      const availW = wrap.clientWidth;\n      const availH = wrap.clientHeight || availW;\n      const minDim = Math.min(availW, availH);\n      cellPx = Math.min(16, Math.max(8, Math.round(minDim / 20)));\n      // the wrap can be smaller than what the geometry formula wants (e.g. a\n      // narrow card) — never let the canvas overflow its own container\n      cellPx = Math.max(1, Math.min(cellPx, Math.floor(availW / cols), Math.floor(availH / rows)));\n      const width = cols * cellPx;\n      const height = rows * cellPx;\n      dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = width * dpr;\n      canvas.height = height * dpr;\n      canvas.style.width = `${width}px`;\n      canvas.style.height = `${height}px`;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.imageSmoothingEnabled = false;\n      const cyclePos = reduced ? FREEZE_MS : (performance.now() - cycleStart + cycleMs) % cycleMs;\n      computeRevealed(cyclePos);\n      draw();\n    };\n\n    const onResize = () => {\n      if (resizeTimer) clearTimeout(resizeTimer);\n      resizeTimer = setTimeout(resize, 100);\n    };\n    const ro = new ResizeObserver(onResize);\n    ro.observe(wrap);\n\n    // the site's theme toggle flips a `.dark` class on <html> live, with no\n    // remount — watch it so the glyph colour updates without a page reload\n    const themeObserver = new MutationObserver(() => {\n      fg = getComputedStyle(canvas).color;\n      draw();\n    });\n    themeObserver.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    let visible = true;\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible && !reduced && !raf) raf = requestAnimationFrame(loop);\n    });\n    io.observe(wrap);\n\n    function loop() {\n      if (!visible) {\n        raf = 0;\n        return;\n      }\n      const cyclePos = (performance.now() - cycleStart) % cycleMs;\n      computeRevealed(cyclePos);\n      draw();\n      raf = requestAnimationFrame(loop);\n    }\n\n    // seed the cycle already mid-sweep so the very first paint (t0) shows\n    // partially-built glyphs, not a blank canvas\n    cycleStart = performance.now() - T0_PHASE_MS;\n    resize();\n    window.addEventListener(\"resize\", onResize);\n\n    if (!reduced) raf = requestAnimationFrame(loop);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      if (resizeTimer) clearTimeout(resizeTimer);\n      window.removeEventListener(\"resize\", onResize);\n      ro.disconnect();\n      io.disconnect();\n      themeObserver.disconnect();\n    };\n  }, [text]);\n\n  return (\n    <div\n      ref={wrapRef}\n      className={`ns-crs-wrap flex h-full w-full items-center justify-center ${className}`}\n    >\n      <canvas ref={canvasRef} aria-hidden className=\"block text-foreground\" />\n      <span className=\"sr-only\">{text}</span>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/chargen-rom-slice.tsx"
    }
  ],
  "meta": {
    "collection": "core",
    "tags": [
      "loader",
      "status",
      "glyph",
      "canvas",
      "bitmap-font",
      "crt",
      "chargen",
      "ascii",
      "monochrome"
    ],
    "instruction": "A card-scale status glyph, canvas-rendered, that simulates how a real VT100/Commodore/Apple II-class video controller reads a character-generator ROM: one scanline row at a time, in sync with the beam, rather than fetching a whole glyph at once. Each character in the (uppercase, 12-char max) `text` prop is drawn from a self-contained hand-drawn 5x8 bitmap font table (5 cols wide, 8 rows tall, the 8th row a blank descender row matching real chargen cell conventions) baked once into a `boolean[8][cols]` bitmap grid at mount. Geometry: `cellPx = clamp(round(min(wrap.clientWidth, wrap.clientHeight) / 20), 8, 16)`, then further clamped to `min(cellPx, floor(availableWidth / cols), floor(availableHeight / rows))` so a wrap smaller than the formula wants never overflows its own container; derived from the wrap's own measured box (the component's root fills `h-full w-full` of whatever the consumer sizes) via ResizeObserver (debounced 100ms) so it reads correctly at card scale. The canvas itself is sized in JS (`canvas.width/height` plus matching `style.width/height`, scaled by a capped devicePixelRatio) rather than relying on intrinsic size, with `ctx.imageSmoothingEnabled = false` and each revealed cell painted as a full, contiguous `cellPx x cellPx` block — no inter-row gap — so the sweep boundary between fetched and unfetched rows is the only horizontal discontinuity in a glyph. The sweep: each character's row-reveal starts `i * 60ms` after the shared cycle start (left-to-right stagger), reveals one additional row every 35ms (`Math.floor(elapsed / 35)`, clamped 0-8), so a single glyph fully assembles in 280ms and the whole string finishes roughly `(chars.length - 1) * 60 + 280` ms in; once every character has all 8 rows, the assembled string holds fully clean for 500ms, after which the cycle position wraps (`(now - cycleStart) % cycleMs`, cycleMs = build time + 500) and every row count resets to 0 — real elapsed time drives the math (via `performance.now()`), not frame count, so speed never depends on paint rate. `cycleStart` is seeded 170ms into the past at mount (not `now`) so t0 itself already reads mid-sweep instead of a blank canvas, and that 170ms offset is chosen specifically so the resting-loop's t0/2.5s/5s sample points — locked 340ms/680ms apart by the fixed 1080ms cycle — land in three visibly distinct states (an early mid-sweep, a later mid-sweep with most glyphs whole, and the clean hold) rather than two of the three both landing inside the hold. Colour is read once via `getComputedStyle(canvas).color` (the canvas carries `text-foreground`) and re-read on a `MutationObserver` watching `document.documentElement`'s `class` attribute so a live theme flip repaints without remount; there is no separate background fill — unrevealed rows are simply unpainted canvas showing the page's own `--background` through, so light and dark read identically (1-bit foreground-on-background, no tokens beyond `--foreground`/the page background, `--ns-accent` and `--border` are never touched). An `IntersectionObserver` on the wrap element halts and resumes the rAF loop as it leaves/enters viewport; a `ResizeObserver` re-measures and redraws on layout change; `prefers-reduced-motion` skips the rAF loop entirely and paints one fixed frame by running the same `computeRevealed` sweep math at a named `FREEZE_MS = 300` cycle position instead of live time — an early character fully assembled, the next mid-sweep with a visible hard slice seam, later ones thin or untouched — the single most legible static frame of the mechanic, deliberately not the clean hold and not the empty t0 state, and one that stays a genuine sweep frame (not a hardcoded per-index array) for any string length. `autoplay.mode` is `\"none\"`: the sweep runs on its own internal clock regardless of pointer/scroll/press input, so there is nothing for the site's synthetic-input driver to trigger. No interaction is implemented — the spec allows an optional hover/focus restart-from-row-0, but resetting `cycleStart` to `now` would blank the canvas for a beat before the 580ms rebuild (the `elapsed <= 0` branch reports zero rows for every character at the instant of reset), which is the wrong risk to carry for a feature the spec itself marks not required; the wrap carries no `tabIndex` since the canvas is `aria-hidden` and there is nothing keyboard-operable to expose. Cleanup on unmount cancels the rAF, disconnects the ResizeObserver, IntersectionObserver and MutationObserver, and removes the pointer/resize listeners. Zero dependencies, canvas + CSS only, no new colours (`--foreground` and the ambient page background only). The component's own root is `h-full w-full` and expects the consumer to size the parent container (see demo: a `h-40 w-80` bordered box), not to pass a sizing `className` onto the component itself."
  },
  "type": "registry:ui"
}