{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scroll-fine-register",
  "title": "Scroll Fine Register",
  "description": "A divider/footer band whose streaming motion is built from a real tilemap-PPU scroll: a fine register sweeps 0..cellPx-1 sub-pixel offsets across 480ms before a coarse register ticks a tile over, with both registers exposed as a live numeric coarse/fine readout beside the strip.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/scroll-fine-register/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\n// ---------------------------------------------------------------------------\n// ScrollFineRegister — a divider/footer band whose \"content is streaming\n// past\" cue is built from the two cooperating registers a tilemap PPU\n// (NES/SNES-class hardware) actually scrolls with, not a plain translateX\n// tween. A COARSE register steps the visible window one whole tile at a\n// time by swapping which tile glyph is addressed; a FINE register sweeps\n// 0..cellPx-1 sub-pixel offsets within the current tile before the coarse\n// register ticks over. What reads as smooth pixel-by-pixel motion is a\n// sawtooth (fine ramping up then snapping to 0) gating a stepped counter\n// (coarse incrementing once per sawtooth cycle) — and both registers are\n// exposed as a live numeric readout so the mechanism is legible, not just\n// its resulting motion.\n//\n// One totalSteps integer (in px) is the single source of truth: fine =\n// totalSteps % cellPx, coarse = floor(totalSteps / cellPx) % tileCount. A\n// fixed-rate accumulator ticks totalSteps by 1 every (480ms / cellPx) —\n// so a full tile's worth of fine steps always takes exactly 480ms,\n// regardless of how many discrete px steps cellPx works out to.\n//\n// tileCount (24-40) is the COARSE WRAP PERIOD — the pattern of tile\n// glyphs repeats every tileCount cells and totalSteps wraps at\n// tileCount*cellPx. That is deliberately kept separate from how many\n// cells are actually rendered: renderCount = ceil(width/cellPx) +\n// tileCount cells are painted (glyph = pattern[j % tileCount]), a\n// full period wider than the viewport, so the strip never runs dry as\n// totalSteps sweeps across an entire wrap cycle — a doubled-strip trick\n// only covers a translate range up to one strip width, this covers the\n// whole period.\n//\n// The glyph sequence itself is not a short repeating ramp: a fixed-seed\n// mulberry32 PRNG draws `tileCount` glyph choices once per build, so the\n// visual texture's true repeat period is the full tileCount*cellPx wrap\n// (11.5-19.2s), not some short arithmetic cycle — otherwise the strip\n// would loop every couple of seconds while only the readout kept counting.\n//\n// A single marker — an absolutely positioned overlay, not a strip cell —\n// tracks `MARKER_LEAD` tiles ahead of the live coarse register at screen\n// x = MARKER_LEAD*cellPx - fine, so it visibly slides left by cellPx px\n// over the 480ms sweep and snaps back at the next coarse tick: the\n// sawtooth made literally visible on the one thing a viewer's eye\n// follows. Drawn as a full-height --foreground bracket (border-left +\n// border-right only, cellPx wide) so it stays legible on top of every\n// glyph underneath it, including the solid block.\n//\n// Geometry is derived from the container's smaller dimension (almost\n// always height, for a horizontal band): cellPx = clamp(round(minDim/24),\n// 8, 16). Translation is written directly to the track's transform and\n// the readout's textContent every tick — no React state on the hot path.\n// Pure DOM, zero colour literals: ink is `text-foreground` /\n// `text-ns-muted`; `border-border` is used only for the band's own\n// dividing rules, never as the marker's paint.\n// ---------------------------------------------------------------------------\n\nexport interface ScrollFineRegisterProps {\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nconst MIN_CELL = 8;\nconst MAX_CELL = 16;\nconst CELL_DIVISOR = 24;\nconst SWEEP_MS = 480; // one full fine-register sweep (0..cellPx-1) = one coarse tick\nconst MIN_TILES = 24;\nconst MAX_TILES = 40;\nconst MARKER_LEAD = 4; // marker sits this many tiles ahead of the coarse index\nconst FROZEN_COARSE_FALLBACK = 12; // reduced-motion freeze target: mid-strip, tile-aligned\n\n// A small \"tile ROM\" restricted to shading blocks — these are solid\n// rectangles in every monospace face, so they read cleanly as tile\n// pixels even at an 8px cell where a glyph with descenders/gaps would not.\nconst GLYPHS = \"░▒▓█\";\n\nfunction mulberry32(seed: number) {\n  let a = seed >>> 0;\n  return () => {\n    a = (a + 0x6d2b79f5) >>> 0;\n    let t = a;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\n/** One glyph choice per tile index, 0..tileCount-1 — the true visual\n * repeat period, not a short arithmetic ramp. */\nfunction buildPattern(tileCount: number): string[] {\n  const rand = mulberry32(0x5f3a11);\n  const pattern: string[] = [];\n  for (let i = 0; i < tileCount; i++) {\n    pattern.push(GLYPHS[Math.floor(rand() * GLYPHS.length)]!);\n  }\n  return pattern;\n}\n\nexport function ScrollFineRegister({ className = \"\" }: ScrollFineRegisterProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const trackRef = useRef<HTMLDivElement>(null);\n  const markerRef = useRef<HTMLDivElement>(null);\n  const probeRef = useRef<HTMLSpanElement>(null);\n  const coarseRef = useRef<HTMLSpanElement>(null);\n  const fineRef = useRef<HTMLSpanElement>(null);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const track = trackRef.current;\n    const marker = markerRef.current;\n    const probe = probeRef.current;\n    if (!root || !track || !marker || !probe) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n\n    let disposed = false;\n    let cellPx = MIN_CELL;\n    let tileCount = MIN_TILES; // coarse wrap period, in tiles\n    let renderCount = MIN_TILES; // cells actually painted, >= period + viewport\n    let stepMs = SWEEP_MS / cellPx;\n    let totalSteps = 0; // px units; fine = totalSteps % cellPx, coarse = floor(totalSteps/cellPx) % tileCount\n    let acc = 0;\n    let last = 0;\n    let raf = 0;\n    let visible = true;\n    let pattern: string[] = [];\n\n    const paint = () => {\n      const fine = totalSteps % cellPx;\n      const coarse = Math.floor(totalSteps / cellPx) % tileCount;\n      track.style.transform = `translate3d(${-totalSteps}px, 0, 0)`;\n      marker.style.width = `${cellPx}px`;\n      marker.style.transform = `translate3d(${MARKER_LEAD * cellPx - fine}px, 0, 0)`;\n      marker.style.visibility = \"visible\";\n      if (coarseRef.current) coarseRef.current.textContent = String(coarse).padStart(2, \"0\");\n      if (fineRef.current) fineRef.current.textContent = String(fine);\n    };\n\n    // -- (re)build the tile strip for the current geometry. renderCount is\n    // one full coarse period wider than the viewport so every reachable\n    // translate offset within a period still fully covers the track. ------\n    const build = () => {\n      const rect = root.getBoundingClientRect();\n      const minDim = Math.min(rect.width, rect.height);\n      cellPx = Math.min(MAX_CELL, Math.max(MIN_CELL, Math.round(minDim / CELL_DIVISOR)));\n      stepMs = SWEEP_MS / cellPx;\n\n      const visibleTiles = Math.ceil(rect.width / Math.max(1, cellPx));\n      tileCount = Math.min(MAX_TILES, Math.max(MIN_TILES, visibleTiles));\n      renderCount = visibleTiles + tileCount + MARKER_LEAD + 1;\n      pattern = buildPattern(tileCount);\n\n      // measure the real glyph advance at this cellPx (monospace advance\n      // is well under 1em) and scale the font so a glyph's rendered width\n      // fills the cell — otherwise adjacent tiles read as separated bars,\n      // not a contiguous tilemap.\n      probe.style.fontSize = `${cellPx}px`;\n      const advance = probe.getBoundingClientRect().width || cellPx * 0.6;\n      const glyphFontPx = Math.max(6, Math.round(cellPx * Math.min(2.5, cellPx / advance)));\n\n      track.textContent = \"\";\n      for (let j = 0; j < renderCount; j++) {\n        const span = document.createElement(\"span\");\n        span.setAttribute(\"aria-hidden\", \"true\");\n        span.style.display = \"inline-flex\";\n        span.style.alignItems = \"center\";\n        span.style.justifyContent = \"center\";\n        span.style.width = `${cellPx}px`;\n        span.style.height = `${cellPx}px`;\n        span.style.overflow = \"hidden\";\n        span.style.fontSize = `${glyphFontPx}px`;\n        span.style.lineHeight = \"1\";\n        span.style.flexShrink = \"0\";\n        span.style.boxSizing = \"border-box\";\n        span.className = \"text-foreground\";\n        span.textContent = pattern[j % tileCount]!;\n        track.appendChild(span);\n      }\n      totalSteps = ((totalSteps % (tileCount * cellPx)) + tileCount * cellPx) % (tileCount * cellPx);\n    };\n\n    const freeze = () => {\n      const frozenCoarse = Math.min(tileCount - 1, FROZEN_COARSE_FALLBACK);\n      totalSteps = frozenCoarse * cellPx; // fine === 0 exactly — a tile boundary flush with the viewport edge\n      paint();\n    };\n\n    const loop = (now: number) => {\n      raf = 0;\n      if (!visible) return;\n      if (last === 0) last = now;\n      const dt = Math.min(100, now - last);\n      last = now;\n      acc += dt;\n      while (acc >= stepMs) {\n        acc -= stepMs;\n        totalSteps += 1;\n        if (totalSteps >= tileCount * cellPx) totalSteps -= tileCount * cellPx;\n      }\n      paint();\n      raf = requestAnimationFrame(loop);\n    };\n\n    build();\n\n    if (reduced) {\n      freeze();\n    } else {\n      // start a few steps into the sweep so t0 already reads as\n      // \"mid-motion\", not a just-ticked-over frame.\n      totalSteps = 3;\n      last = 0;\n      raf = requestAnimationFrame(loop);\n    }\n\n    let resizeTimer = 0;\n    const onResize = () => {\n      window.clearTimeout(resizeTimer);\n      resizeTimer = window.setTimeout(() => {\n        if (disposed) return;\n        build();\n        if (reduced) freeze();\n        else paint();\n      }, 120);\n    };\n    const ro = new ResizeObserver(onResize);\n    ro.observe(root);\n\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible && !reduced && !raf) {\n        last = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    });\n    io.observe(root);\n\n    document.fonts?.ready?.then(() => {\n      if (!disposed) onResize();\n    });\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      window.clearTimeout(resizeTimer);\n      ro.disconnect();\n      io.disconnect();\n    };\n  }, []);\n\n  return (\n    <div\n      ref={rootRef}\n      className={`flex h-12 items-stretch overflow-hidden border-y border-border bg-background ${className}`}\n    >\n      <div className=\"relative min-w-0 flex-1 overflow-hidden\">\n        <span\n          ref={probeRef}\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute -left-[9999px] top-0 font-mono\"\n          style={{ visibility: \"hidden\", whiteSpace: \"pre\" }}\n        >\n          █\n        </span>\n        <div ref={trackRef} className=\"flex h-full items-center font-mono will-change-transform\" />\n        <div\n          ref={markerRef}\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-y-0 left-0 border-x border-foreground will-change-transform\"\n          style={{ visibility: \"hidden\" }}\n        />\n      </div>\n      <div className=\"flex shrink-0 items-center gap-2 border-l border-border px-4 font-mono text-[11px] tabular-nums text-foreground\">\n        <span className=\"text-ns-muted\">C</span>\n        <span ref={coarseRef}>00</span>\n        <span className=\"text-ns-muted\">F</span>\n        <span ref={fineRef}>0</span>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/scroll-fine-register.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)"
    },
    "light": {
      "ns-muted": "#4d4d4d"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "scroll",
      "register",
      "tilemap",
      "hardware",
      "ppu",
      "divider",
      "footer",
      "mono",
      "readout"
    ],
    "instruction": "A horizontal band with no props beyond `className`, split into a flex-1 overflow-hidden tile strip and a shrink-0 numeric readout separated by a `border-border` rule (that rule, plus the band's own `border-y`, are the only uses of `--border` — always a structural separator, never the motion's paint). The strip is built entirely by direct DOM manipulation inside a single effect: on mount (and on every ResizeObserver-driven resize, debounced 120ms), `cellPx = clamp(round(minDim/24), 8, 16)` is derived from the container's smaller dimension (minDim = min(rect.width, rect.height), almost always height for a horizontal band). `tileCount = clamp(ceil(rect.width/cellPx), 24, 40)` is the COARSE WRAP PERIOD — `totalSteps` wraps at `tileCount*cellPx` — and a fixed-seed `mulberry32(0x5f3a11)` PRNG draws one glyph choice per tile index into a `tileCount`-length `pattern` array once per build, so the strip's true visual repeat period is the full wrap (11.5-19.2s at cellPx 8-16), not a short arithmetic ramp. `renderCount = visibleTiles + tileCount + MARKER_LEAD + 1` cells are actually painted (glyph at index `j` = `pattern[j % tileCount]`), one full coarse period wider than the viewport so the strip fully covers the container at every reachable translate offset within a wrap cycle, not just near t=0. Each cell's `fontSize` is not simply `cellPx`: a hidden probe span (same technique as marquee-ticker-glyph) measures the real monospace glyph advance at `cellPx`, and every cell's font is scaled by `cellPx / advance` (capped at 2.5x) so the glyph's rendered width fills the cell instead of leaving gaps between tiles; each cell also gets `overflow: hidden` to clip the resulting taller glyph. One integer, `totalSteps` (in px), is the entire state: `fine = totalSteps % cellPx` and `coarse = floor(totalSteps/cellPx) % tileCount`. A rAF loop accumulates elapsed ms and ticks `totalSteps` by 1 every `480/cellPx` ms — so a full fine sweep across a tile always takes exactly 480ms regardless of cellPx — wrapping `totalSteps` from `tileCount*cellPx` back to 0 the instant one full coarse period has scrolled past (landing on content pixel-identical to the wrap target, since the glyph pattern already repeats with that period). Every tick writes `track.style.transform = translate3d(-totalSteps px, 0, 0)` directly, and writes `coarse`/`fine` as plain zero-padded/unpadded text into two `tabular-nums` readout spans — no React state on the hot path. The marker is not a strip cell: it is a separate, absolutely-positioned full-band-height overlay div (`border-x border-foreground`, `cellPx` wide) drawn on top of the strip so it stays legible over any glyph beneath it, including a solid block. Every tick its transform is set to `translate3d(MARKER_LEAD*cellPx - fine, 0, 0)` (MARKER_LEAD = 4 tiles ahead of the live coarse register), so it visibly slides left by `cellPx` px over the 480ms sweep and snaps back to its start position the instant coarse ticks over — the sawtooth made literally visible on the one thing a viewer's eye is meant to track, tying the numeric coarse tick directly to a visual snap. The marker starts `visibility: hidden` in JSX and is flipped to `visible` on the first `paint()` call, so there is no flash of an unpositioned overlay at x=0 between mount and the first frame. `prefers-reduced-motion: reduce` skips the rAF loop entirely and calls `freeze()` once, which sets `totalSteps = min(tileCount-1, 12) * cellPx` — fine exactly 0, coarse fixed at a mid-strip tile — the one instant a tile boundary sits flush with the viewport's left edge, the most structured resting frame. At t0 (non-reduced) `totalSteps` starts at 3, a few px into the first sweep, so the component reads as already in motion rather than freshly booted; over 5s (~10 sweep cycles at cellPx=8, 480ms each) the coarse counter visibly advances roughly 10 and the strip has scrolled several tile widths, with fine cycling through a different phase than t0 at every checkpoint. An IntersectionObserver stops/resumes the rAF loop when the band scrolls off/on screen; a ResizeObserver rebuilds strip geometry (and the reduced-motion freeze target) whenever the band's own box changes size. No interaction: the strip does not accept drag/scrub, since the mechanic is a fixed-rate hardware register cadence, not a carousel. Ink is `text-foreground` / `text-ns-muted` for glyphs and readout, the `border-foreground` utility for the marker overlay, `border-border` only for the band's dividing rules — zero colour literals, zero `--ns-accent` use, both themes read identically. Pure DOM, zero dependencies."
  },
  "type": "registry:ui"
}