{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tufting-gun-loop-pile",
  "title": "Tufting Gun Loop Pile",
  "description": "A card whose backing texture is a genuine tufting-gun loop-pile build: a carriage sweeping row by row across a backing grid, plunging a loop of pile at each cell just behind it, the finished field feeding continuously off the top edge as new rows tuft in below.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/tufting-gun-loop-pile/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport type { CSSProperties } from \"react\";\n\n// ---------------------------------------------------------------------------\n// TuftingGunLoopPile — an ambient card texture reproducing a tufting gun's\n// row-by-row loop-pile build (hand/machine tufting gun operation, carpet\n// manufacture), not a generic \"loading tiles\" fill. A hollow needle plunges\n// straight down through a backing cloth, grips a strand of yarn, withdraws\n// leaving a loop of pile standing on the front face, the carriage then\n// advances one fixed pitch along the row and repeats. Real guns run\n// 800-1500 punches/minute per needle; per the round 9 decoupling rule that\n// rate is never animated 1:1 — a single tuft \"pop\" (backing -> filled loop)\n// is fixed at 90ms regardless of gun cadence, fast enough to read as a\n// discrete event without simultaneous rows and without aliasing against the\n// paint rate.\n//\n// The active row's carriage crosses the row in a fixed 1100ms (per-cell\n// interval = 1100 / cols, so a wider card gets a faster per-cell tick but\n// the SAME row-crossing time — the ~24-36 cells/s spec figure falls out of\n// that division rather than being hand-tuned per width) with a crosshair\n// drawn at --foreground continuously interpolated across the row so the eye\n// can track carriage position at any instant, cells popping in just behind\n// it. Once every cell in the active row is filled, the whole tufted field\n// scrolls up by exactly one row-pitch over a 260ms ease — old rows climbing\n// toward the top edge, a fixed strip of not-yet-reached rows always resting\n// below the active slot — then the field settles, a new row starts empty at\n// the same active slot, and the cycle repeats. This never stops or resets:\n// the row history is uncapped in principle, only culled from the draw loop\n// once it has scrolled fully above the canvas, which is why the specific\n// filled/empty pattern visible at t0 is guaranteed to have scrolled\n// entirely off-frame by t=5s.\n//\n// Geometry is derived from the container's own smaller dimension: cell\n// pitch = min(width,height)/26, clamped to [6,14]px (so a 240px-tall card\n// lands on the spec's ~9px pitch and ~26 rows), and the column count is\n// sized a little past width/pitch so the field overhangs both edges rather\n// than ending on a half-cell.\n// ---------------------------------------------------------------------------\n\nexport interface TuftingGunLoopPileProps {\n  /** card heading */\n  title?: string;\n  /** card body copy */\n  description?: string;\n  /** trailing link label; omit to render the card with no link */\n  linkLabel?: string;\n  /** link href, used only when linkLabel is set */\n  href?: string;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n  /** inline styles merged onto the root element */\n  style?: CSSProperties;\n}\n\ntype RGB = [number, number, number];\n\nfunction parseColor(raw: string): RGB | null {\n  const s = raw.trim();\n  if (s.startsWith(\"#\")) {\n    let hex = s.slice(1);\n    if (hex.length === 3) hex = hex.split(\"\").map((c) => c + c).join(\"\");\n    if (hex.length < 6) return null;\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  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\nfunction mixRGB(a: RGB, b: RGB, t: number): RGB {\n  return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];\n}\n\nfunction relLuminance([r, g, b]: RGB): number {\n  return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;\n}\n\n/** Nudge a token's own channels up by a fixed delta (never a colour\n * literal blended in) — the additive analogue of \"lighter\", stays legible\n * even where the source is already near-black in light theme. */\nfunction lighten(c: RGB, delta: number): RGB {\n  return [Math.min(255, c[0] + delta), Math.min(255, c[1] + delta), Math.min(255, c[2] + delta)];\n}\n\nfunction rgbStr([r, g, b]: RGB, alpha = 1): string {\n  return `rgba(${r.toFixed(0)}, ${g.toFixed(0)}, ${b.toFixed(0)}, ${alpha})`;\n}\n\nfunction easeOutCubic(t: number): number {\n  const p = 1 - t;\n  return 1 - p * p * p;\n}\n\nconst PITCH_DIVISOR = 26; // cell pitch = min(w,h) / 26 — a 240px-tall card lands on ~9px, ~26 rows\nconst PITCH_MIN = 6;\nconst PITCH_MAX = 14;\nconst ROW_SWEEP_MS = 1100; // fixed row-crossing time regardless of column count\nconst POP_MS = 90; // single tuft backing -> filled-loop pop, decoupled from real gun cadence\nconst SCROLL_MS = 260; // field feed after a row completes\nconst EMPTY_ROWS_BELOW = 2; // fixed strip of not-yet-reached backing kept below the active row\nconst FIELD_OVERHANG_CELLS = 2; // extra columns so the field overhangs both edges\n\ntype Phase = \"sweeping\" | \"scrolling\";\n\n// Reduced motion freezes with the head paused ~50% through its row, a\n// handful of completed rows above and empty backing weave below — the most\n// structured single frame (mixed filled/unfilled state, head mid-sweep, not\n// at a row boundary). Named GUN_MIDROW, exposed via data-reduced-motion-freeze.\nconst FREEZE_PHASE = \"GUN_MIDROW\";\nconst FREEZE_FILLED_FRAC = 0.5;\nconst FREEZE_COMPLETED_ROWS = 4;\n\nexport function TuftingGunLoopPile({\n  title = \"Backing, row 1,842\",\n  description = \"The carriage plunges a loop, advances one pitch, and the field feeds beneath it — the pile never stops building.\",\n  linkLabel = \"Read the process card\",\n  href = \"#\",\n  className = \"\",\n  style,\n}: TuftingGunLoopPileProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    const canvas = canvasRef.current;\n    if (!root || !canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let reduced = mq.matches;\n\n    // -- token-derived colour, re-read on any theme class flip, never a\n    // literal (not even a fallback) — nothing paints until this has run\n    // successfully once. --\n    let fg: RGB | null = null;\n    let border: RGB | null = null;\n    let rim: RGB | null = null;\n    let colorsReady = false;\n    const deriveColors = () => {\n      const cs = getComputedStyle(document.documentElement);\n      const nextFg = parseColor(cs.getPropertyValue(\"--foreground\"));\n      const nextBg = parseColor(cs.getPropertyValue(\"--background\"));\n      const nextBorder = parseColor(cs.getPropertyValue(\"--border\"));\n      const nextMuted = parseColor(cs.getPropertyValue(\"--ns-muted\"));\n      if (!nextFg || !nextBg || !nextBorder) return;\n      fg = nextFg;\n      // rim: the same --foreground token nudged up by a fixed additive\n      // delta, luminance only, never --ns-accent — sells the loop's\n      // rounded top catching light.\n      rim = lighten(nextFg, 46);\n      // --border is a separator token at ~1.1:1 contrast in light theme —\n      // as a crosshatch fill it would be unreadable there, so in light\n      // theme it is mixed toward --ns-muted (same precedent as the\n      // peen-coverage sibling's light-theme branch) rather than used raw.\n      const isDark = relLuminance(nextBg) < 0.5;\n      border = isDark || !nextMuted ? nextBorder : mixRGB(nextBorder, nextMuted, 0.7);\n      colorsReady = true;\n    };\n    deriveColors();\n\n    let w = 0;\n    let h = 0;\n    let dpr = 1;\n    // derived from the container's own smaller dimension every layout pass\n    let CELL_PITCH = PITCH_MIN;\n    let cols = 1;\n    let fieldW = 0;\n    let fieldOffsetX = 0;\n    let activeY = 0; // fixed canvas y (top edge) of the active row's slot\n    let visible = true;\n    let raf = 0;\n\n    let phase: Phase = \"sweeping\";\n    let filled = 0; // cells confirmed filled in the active row\n    let rowElapsed = 0; // ms into the current row's sweep\n    let scrollElapsed = 0; // ms into the current scroll transition\n    let completedRows = 0; // count of fully-filled rows resting above the active slot\n    let lastNow = 0;\n\n    const maxCompletedRows = () => Math.ceil(activeY / CELL_PITCH) + 3;\n\n    const cellInterval = () => ROW_SWEEP_MS / Math.max(1, cols);\n\n    const resetRow = () => {\n      filled = 0;\n      rowElapsed = 0;\n      phase = \"sweeping\";\n    };\n\n    const advanceSweep = (dtMs: number) => {\n      rowElapsed = Math.min(ROW_SWEEP_MS + POP_MS, rowElapsed + dtMs);\n      const target = Math.min(cols, Math.floor(rowElapsed / cellInterval()));\n      if (target > filled) filled = target;\n      // wait for the LAST cell's own 90ms pop to finish, not just the\n      // sweep clock, so the row never snaps its final cells into place.\n      if (filled >= cols && rowElapsed >= ROW_SWEEP_MS + POP_MS) {\n        phase = \"scrolling\";\n        scrollElapsed = 0;\n      }\n    };\n\n    const advanceScroll = (dtMs: number) => {\n      scrollElapsed = Math.min(SCROLL_MS, scrollElapsed + dtMs);\n      if (scrollElapsed >= SCROLL_MS) {\n        completedRows = Math.min(maxCompletedRows(), completedRows + 1);\n        resetRow();\n      }\n    };\n\n    // -- draw a faint crosshatch backing weave for an unfilled cell,\n    // --border used correctly as a separator, never a fill of solid shapes --\n    const drawBacking = (x: number, y: number) => {\n      if (!border) return;\n      ctx.beginPath();\n      ctx.moveTo(x + 1, y + 1);\n      ctx.lineTo(x + CELL_PITCH - 1, y + CELL_PITCH - 1);\n      ctx.moveTo(x + CELL_PITCH - 1, y + 1);\n      ctx.lineTo(x + 1, y + CELL_PITCH - 1);\n      ctx.strokeStyle = rgbStr(border, 0.9);\n      ctx.lineWidth = 1;\n      ctx.stroke();\n    };\n\n    // -- a filled loop: a short vertical ellipse in --foreground with a\n    // lighter rim arc along its top-left, selling a rounded pile top --\n    const drawLoop = (x: number, y: number, popT: number) => {\n      if (!fg || !rim) return;\n      const cx = x + CELL_PITCH / 2;\n      const cy = y + CELL_PITCH / 2;\n      const rx = (CELL_PITCH * 0.34) * popT;\n      const ry = (CELL_PITCH * 0.42) * popT;\n      if (rx <= 0.2 || ry <= 0.2) return;\n      ctx.beginPath();\n      ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);\n      ctx.fillStyle = rgbStr(fg, 1);\n      ctx.fill();\n      ctx.beginPath();\n      ctx.ellipse(cx - rx * 0.28, cy - ry * 0.32, rx * 0.55, ry * 0.4, -0.6, 0, Math.PI * 2);\n      ctx.strokeStyle = rgbStr(rim, 0.85);\n      ctx.lineWidth = 1;\n      ctx.stroke();\n    };\n\n    const drawHead = (x: number, y: number) => {\n      if (!fg) return;\n      const cx = x;\n      const cy = y + CELL_PITCH / 2;\n      const arm = CELL_PITCH * 0.75;\n      ctx.strokeStyle = rgbStr(fg, 1);\n      ctx.lineWidth = 1.5;\n      ctx.beginPath();\n      ctx.moveTo(cx - arm, cy);\n      ctx.lineTo(cx + arm, cy);\n      ctx.moveTo(cx, cy - arm);\n      ctx.lineTo(cx, cy + arm);\n      ctx.stroke();\n    };\n\n    const draw = () => {\n      if (w <= 0 || h <= 0 || !colorsReady) return;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.clearRect(0, 0, w, h);\n\n      const scrollT = phase === \"scrolling\" ? easeOutCubic(scrollElapsed / SCROLL_MS) : 0;\n      const scrollPx = scrollT * CELL_PITCH;\n\n      // completed rows, oldest at the top, scrolling up together with the\n      // in-progress transition; culled once fully above the canvas top\n      for (let n = 1; n <= completedRows; n++) {\n        const y = activeY - n * CELL_PITCH + scrollPx;\n        if (y + CELL_PITCH < -CELL_PITCH) continue;\n        if (y > h) continue;\n        for (let c = 0; c < cols; c++) {\n          drawLoop(fieldOffsetX + c * CELL_PITCH, y, 1);\n        }\n      }\n\n      // active row, still mid-sweep or mid-scroll (fully filled, sliding\n      // toward the \"completed\" slot one pitch above)\n      {\n        const y = activeY + scrollPx;\n        for (let c = 0; c < cols; c++) {\n          const x = fieldOffsetX + c * CELL_PITCH;\n          if (c < filled) {\n            const cellStart = c * cellInterval();\n            const popT = phase === \"scrolling\" ? 1 : Math.min(1, (rowElapsed - cellStart) / POP_MS);\n            drawLoop(x, y, Math.max(0, popT));\n          } else {\n            drawBacking(x, y);\n          }\n        }\n        // the head stays visible through the scroll transition too, riding\n        // up with the row it just finished — round 9's rule that a\n        // discrete transition must show departure and arrival, never a\n        // blink, applies to the head as much as to the cells.\n        const headFrac = phase === \"sweeping\" ? Math.min(1, rowElapsed / ROW_SWEEP_MS) : 1;\n        const headX = fieldOffsetX + headFrac * cols * CELL_PITCH;\n        drawHead(headX, y);\n      }\n\n      // fixed strip of not-yet-reached backing rows, always resting below\n      // the active slot — these never scroll, the active slot is pinned\n      for (let n = 1; n <= EMPTY_ROWS_BELOW; n++) {\n        const y = activeY + n * CELL_PITCH;\n        if (y > h) continue;\n        for (let c = 0; c < cols; c++) {\n          drawBacking(fieldOffsetX + c * CELL_PITCH, y);\n        }\n      }\n    };\n\n    const loop = (now: number) => {\n      const dt = lastNow === 0 ? 0 : Math.min(100, now - lastNow);\n      lastNow = now;\n      if (phase === \"sweeping\") advanceSweep(dt);\n      else advanceScroll(dt);\n      draw();\n      if (!reduced && visible) raf = requestAnimationFrame(loop);\n      else raf = 0;\n    };\n    const wake = () => {\n      if (raf === 0 && !reduced && visible) {\n        lastNow = 0;\n        raf = requestAnimationFrame(loop);\n      }\n    };\n\n    let hasSeeded = false;\n\n    const layout = () => {\n      const rect = root.getBoundingClientRect();\n      w = rect.width;\n      h = rect.height;\n      if (w < 2 || h < 2) return;\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\n      CELL_PITCH = Math.min(PITCH_MAX, Math.max(PITCH_MIN, Math.min(w, h) / PITCH_DIVISOR));\n      cols = Math.max(4, Math.ceil(w / CELL_PITCH) + FIELD_OVERHANG_CELLS);\n      fieldW = cols * CELL_PITCH;\n      fieldOffsetX = -(fieldW - w) / 2;\n\n      const activeSlotFromBottom = EMPTY_ROWS_BELOW + 1;\n      activeY = h - activeSlotFromBottom * CELL_PITCH;\n\n      if (reduced) {\n        phase = \"sweeping\";\n        scrollElapsed = 0;\n        completedRows = Math.min(maxCompletedRows(), FREEZE_COMPLETED_ROWS);\n        filled = Math.round(cols * FREEZE_FILLED_FRAC);\n        rowElapsed = filled * cellInterval() + POP_MS; // every filled cell's pop finished\n        draw();\n        return;\n      }\n\n      if (!hasSeeded) {\n        // Seed the resting loop already mid-flight instead of blank: t0 is\n        // \"head mid-row, roughly a third filled, several completed rows\n        // above\" per spec, not an empty card that takes ~30s to fill in.\n        hasSeeded = true;\n        completedRows = maxCompletedRows();\n        filled = Math.max(1, Math.round(cols / 3));\n        rowElapsed = filled * cellInterval();\n        phase = \"sweeping\";\n      } else {\n        // a genuine resize (not the resting loop) — keep progress, just\n        // clamp it to the new geometry rather than blanking the field.\n        completedRows = Math.min(completedRows, maxCompletedRows());\n        filled = Math.min(filled, cols);\n        rowElapsed = Math.min(rowElapsed, ROW_SWEEP_MS + POP_MS);\n      }\n      draw();\n    };\n\n    layout();\n    if (!reduced) wake();\n\n    const ro = new ResizeObserver(layout);\n    ro.observe(root);\n\n    const io = new IntersectionObserver((entries) => {\n      visible = entries[0]?.isIntersecting ?? true;\n      if (visible) wake();\n      else if (raf) {\n        cancelAnimationFrame(raf);\n        raf = 0;\n      }\n    });\n    io.observe(root);\n\n    const mo = new MutationObserver(() => {\n      deriveColors();\n      draw();\n    });\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] });\n\n    const onReducedChange = () => {\n      reduced = mq.matches;\n      cancelAnimationFrame(raf);\n      raf = 0;\n      layout();\n      if (!reduced) wake();\n    };\n    mq.addEventListener(\"change\", onReducedChange);\n\n    const onVisibility = () => {\n      if (document.visibilityState === \"visible\") {\n        wake();\n      } else if (raf) {\n        cancelAnimationFrame(raf);\n        raf = 0;\n      }\n    };\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    let disposed = false;\n    document.fonts.ready.then(() => {\n      if (!disposed) layout();\n    });\n\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(raf);\n      raf = 0;\n      ro.disconnect();\n      io.disconnect();\n      mo.disconnect();\n      mq.removeEventListener(\"change\", onReducedChange);\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n    };\n  }, []);\n\n  return (\n    <div\n      ref={rootRef}\n      data-reduced-motion-freeze={FREEZE_PHASE}\n      className={`ns-tufting-gun-loop-pile relative w-full max-w-sm overflow-hidden rounded-[14px] border border-border bg-background ${className}`}\n      style={style}\n    >\n      <canvas ref={canvasRef} aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 h-full w-full\" />\n      <div ref={contentRef} className=\"pointer-events-none relative flex flex-col gap-3 p-6\">\n        <h3 className=\"text-balance font-sans text-lg font-medium text-foreground\">{title}</h3>\n        <p className=\"text-pretty font-mono text-xs leading-relaxed text-ns-muted\">{description}</p>\n        {linkLabel ? (\n          <a\n            href={href}\n            className=\"pointer-events-auto mt-1 inline-flex w-fit items-center gap-1 rounded-sm font-mono text-xs font-medium text-foreground underline decoration-border underline-offset-4 transition-colors duration-150 hover:decoration-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n          >\n            {linkLabel}\n            <span aria-hidden=\"true\">&rarr;</span>\n          </a>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n\nTuftingGunLoopPile.displayName = \"TuftingGunLoopPile\";\n\nexport default TuftingGunLoopPile;\n",
      "type": "registry:ui",
      "target": "components/ui/tufting-gun-loop-pile.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "card",
      "ambient",
      "canvas",
      "texture",
      "tufting",
      "carpet",
      "process",
      "monochrome",
      "empty-state"
    ],
    "instruction": "Build a card whose entire backing surface is a canvas-rendered tufting-gun loop-pile simulation, sourced from real hand/machine tufting gun operation (textile/carpet manufacture), not a generic 'loading tiles' fill. Root is a `rounded-[14px] border border-border bg-background` card holding an absolutely positioned, `aria-hidden`, `pointer-events-none` canvas filling the card behind a `pointer-events-none` content stack (heading, mono body copy, optional underlined link that re-enables `pointer-events-auto` and carries a `focus-visible` ring in `--ns-accent`); the demo wraps it with an explicit minimum height (`min-h-[280px]`) so the field has room to read as a building carpet rather than a sliver behind the copy.\n\nGrid geometry is derived from the container's own smaller dimension via ResizeObserver: cell pitch = `clamp(6, min(width,height)/26, 14)` px in both axes (a 240px-tall card lands on the spec's ~9px pitch, ~26 rows), and the column count is `ceil(width/pitch) + 2` so the field overhangs both edges rather than ending on a visible half-cell, centred by offsetting the draw origin by `-(fieldWidth - width)/2`. A hollow-needle gun head sweeps left to right across one 'active' row at a time: the whole row-crossing takes a fixed 1100ms regardless of column count (`cellInterval = 1100 / cols`, so a wider card ticks faster per-cell but crosses the row in the same wall-clock time — the resulting ~24-36 cells/second figure falls out of that division, never hand-tuned per width), with a crosshair drawn at `--foreground` whose x-position is continuously interpolated from elapsed sweep time (not snapped to the cell grid) so a viewer can track the head's position at any instant. Cells behind the head pop from backing to filled loop over a fixed 90ms — decoupled per the round 9 rule from the gun's real 800-1500 punches/minute rate, which would alias against paint; the 90ms pop is a legible discrete event, never simultaneous across the row. The row only hands off to the scroll transition once the LAST cell's own 90ms pop has actually finished (`rowElapsed >= ROW_SWEEP_MS + POP_MS`, not just the sweep clock), so the final cells never snap into place. Once that's true, the whole tufted field scrolls up by exactly one row-pitch over a 260ms ease-out — completed rows climbing toward the top edge, culled from the draw loop once fully above the canvas — then the active row resets empty at the same fixed canvas slot and the next row's sweep begins; this never stops, restarts, or caps, so the specific filled/empty pattern visible at t0 is guaranteed to have scrolled entirely off-frame by t=5s. The gun head crosshair stays visible through the scroll transition too, riding up at the row-end x with the row it just finished, so the handoff shows departure and arrival rather than blinking out for the 260ms (round 9's cadence rule). A fixed strip of `EMPTY_ROWS_BELOW = 2` not-yet-reached backing rows always rests below the active row's slot and never itself scrolls (the active slot is pinned; the field feeds up through it) — this is what reads as 'empty backing weave below the head' in the resting frame. On mount only (not on later resizes), the field is seeded already mid-flight rather than blank: completed rows filled up to the canvas top, the active row a third filled, per the spec's own t0 description — a resize afterward clamps existing progress to the new geometry instead of re-blanking it. Completed and active-row loops are rendered as a short vertical ellipse (`rx = 0.34*pitch, ry = 0.42*pitch` at full pop) filled solid `--foreground`, with a lighter rim arc along the top-left stroked in the SAME `--foreground` token nudged by a fixed additive delta (+46 per channel, clamped to 255) in luminance only — never a literal colour, never `--ns-accent` — to sell the loop's rounded top catching light. Unfilled cells render a faint two-line diagonal crosshatch (a separator token used correctly as a separator, never a fill of solid shapes): in dark theme that's `--border` directly, but `--border` alone is only ~1.1:1 against `--background` in light theme, so in light theme the crosshatch stroke is `--border` mixed 70% toward `--ns-muted` (branch chosen from `--background`'s own relative luminance) to stay legible — light theme was checked first, not as a final pass.\n\nColour is read via `getComputedStyle(document.documentElement)` for `--foreground`, `--background`, `--border` and `--ns-muted` at mount, before any canvas paint (nothing paints, not even a fallback, until that first read resolves all three required tokens), and re-derived on a `MutationObserver` watching `documentElement`'s class. DPR-capped (max 2) backing store sized off the card's own `getBoundingClientRect`, resized on `ResizeObserver` (which also re-derives cell pitch, column count and the active row's fixed y-slot) and re-laid-out once more after `document.fonts.ready` (guarded against firing post-unmount). The render loop pauses via `IntersectionObserver` (not scrolled into view) and `visibilitychange` (tab hidden), and is otherwise a plain `requestAnimationFrame` driven by real elapsed time (never frame-count), so sweep speed and pop/scroll durations hold regardless of frame rate. Interaction: none — this is a genuine unconditional rAF loop with no pointer response and no autoplay-mode dependency, `autoplay.mode` is `\"none\"` because nothing about it depends on synthetic pointer/press/scroll input. Under `prefers-reduced-motion: reduce`, the loop never starts: layout bakes the head paused 50% through the active row (`filled = round(cols*0.5)`, every one of those cells' 90ms pops already resolved), 4 completed rows resting above it, and the fixed empty strip below, named `GUN_MIDROW` and exposed via `data-reduced-motion-freeze` on the root — the most structured single frame (mixed filled/unfilled state, head visibly mid-sweep, not at a row boundary). No dependencies."
  },
  "type": "registry:ui"
}