{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bitmap",
  "type": "registry:ui",
  "title": "Bitmap",
  "description": "Chunky 8-bit pixel loader — a hard-edged grid that fills cell-by-cell. Determinate bar (with a pulsing frontier) or an indeterminate marching band, plus a crisp bitmap-radar spinner that snaps cell-to-cell via steps(). Zero dependencies, SSR-safe, role=progressbar with aria-valuenow; collapses to a designed static frame under reduced motion.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/loaders/bitmap/bitmap.tsx",
      "type": "registry:ui",
      "target": "components/crucible/bitmap.tsx",
      "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface BitmapProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /**\n   * Which loader to draw.\n   * - `bar` (default): a chunky pixel-grid progress bar that fills cell-by-cell.\n   *   Give it a `value` (0–100) for a determinate fill; omit it for an\n   *   indeterminate lit band that marches across and loops.\n   * - `spinner`: a crisp bitmap radar — a square grid of pixels with a bright\n   *   arc sweeping around a dim disc, snapping cell-to-cell like an 8-bit\n   *   throbber.\n   * @default \"bar\"\n   */\n  variant?: \"bar\" | \"spinner\";\n  /**\n   * Determinate progress, `0`–`100`. When set, the `bar` fills to this\n   * fraction and reports `aria-valuenow`; when omitted (or `undefined`) the\n   * loader is indeterminate. Ignored by `spinner`.\n   * @default undefined\n   */\n  value?: number;\n  /** Number of columns in the `bar` grid. @default 24 */\n  cells?: number;\n  /** Number of rows (thickness) in the `bar` grid. @default 3 */\n  rows?: number;\n  /** Grid dimension of the `spinner` (forced odd so it has a true centre). @default 7 */\n  grid?: number;\n  /** Edge length of a single pixel cell, in px. @default 10 */\n  size?: number;\n  /** Gap between cells, in px — the dark mortar of the grid. @default 2 */\n  gap?: number;\n  /** Corner radius of each cell in px (0 = hard bitmap edge). @default 1 */\n  radius?: number;\n  /** Animation speed multiplier. 1 = default, 2 = twice as fast. @default 1 */\n  speed?: number;\n  /** Freeze on a designed, legible resting frame. @default false */\n  paused?: boolean;\n  /** Lit-pixel color. @default \"#e8ecf5\" */\n  onColor?: string;\n  /** Unlit-pixel (track) color. @default \"#22252e\" */\n  offColor?: string;\n  /**\n   * Bloom color of a lit pixel. A faint cool halo by default; set equal to\n   * `onColor` for a flat, glow-free bitmap. @default \"#9fd8ff\"\n   */\n  glowColor?: string;\n  /** Accessible label announced while loading. @default \"Loading\" */\n  label?: string;\n  /** Render a blocky, marching label row beneath the grid. @default false */\n  showLabel?: boolean;\n  /** Text of the marching label (uppercased, letter-spaced). @default \"LOADING\" */\n  labelText?: string;\n}\n\n// Pure-CSS bitmap loader. Each pixel is an \"off\" cell holding an absolutely\n// positioned \"lit\" overlay; only the overlay's opacity animates, so the grid\n// (the retro mortar) never moves — crisp, no anti-alias, no layout thrash.\n// Every animated overlay also carries an inline --bm-rest (its resting\n// opacity), so `paused` and `prefers-reduced-motion` collapse to a designed,\n// legible frame — a half-filled bar / a lit arc — never a blank grid. Browsers\n// already throttle CSS animations offscreen and on hidden tabs, so no JS\n// observer is needed to pause.\nconst STYLE = `\n[data-crucible=\"bitmap\"] .bm-grid { display: grid; }\n[data-crucible=\"bitmap\"] .bm-cell {\n  position: relative;\n  background-color: var(--bm-off);\n  border-radius: var(--bm-radius);\n  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.04);\n}\n[data-crucible=\"bitmap\"] .bm-lit {\n  position: absolute;\n  inset: 0;\n  border-radius: var(--bm-radius);\n  background-color: var(--bm-on);\n  box-shadow: 0 0 4px 0 color-mix(in oklab, var(--bm-glow) 70%, transparent),\n    inset 0 0 0 1px color-mix(in oklab, var(--bm-on) 60%, #ffffff);\n  opacity: var(--bm-o, 0);\n  will-change: opacity;\n}\n@keyframes crucible-bitmap-march {\n  0%   { opacity: 1; }\n  30%  { opacity: 0.1; }\n  100% { opacity: 0.1; }\n}\n@keyframes crucible-bitmap-sweep {\n  0%   { opacity: 1; }\n  24%  { opacity: 0.12; }\n  100% { opacity: 0.12; }\n}\n@keyframes crucible-bitmap-pulse {\n  0%, 100% { opacity: 1; }\n  50%      { opacity: 0.5; }\n}\n@keyframes crucible-bitmap-dot {\n  0%, 100% { opacity: 0.18; }\n  50%      { opacity: 1; }\n}\n[data-crucible=\"bitmap\"][data-anim=\"march\"]:not([data-paused=\"true\"]) .bm-lit {\n  animation: crucible-bitmap-march var(--bm-dur) steps(6, end) infinite;\n}\n[data-crucible=\"bitmap\"][data-anim=\"sweep\"]:not([data-paused=\"true\"]) .bm-lit {\n  animation: crucible-bitmap-sweep var(--bm-dur) steps(7, end) infinite;\n}\n[data-crucible=\"bitmap\"] .bm-frontier { opacity: 1; }\n[data-crucible=\"bitmap\"]:not([data-paused=\"true\"]) .bm-frontier {\n  animation: crucible-bitmap-pulse calc(var(--bm-dur) * 0.5) ease-in-out infinite;\n}\n[data-crucible=\"bitmap\"] .bm-dot {\n  width: var(--bm-dotsize);\n  height: var(--bm-dotsize);\n  background-color: var(--bm-on);\n  border-radius: var(--bm-radius);\n  box-shadow: 0 0 4px 0 color-mix(in oklab, var(--bm-glow) 60%, transparent);\n  opacity: 0.18;\n}\n[data-crucible=\"bitmap\"]:not([data-paused=\"true\"]) .bm-dot {\n  animation: crucible-bitmap-dot calc(var(--bm-dur) * 0.75) steps(3, end) infinite;\n}\n/* Paused → collapse animated overlays to their resting pose. */\n[data-crucible=\"bitmap\"][data-paused=\"true\"] .bm-lit { animation: none !important; opacity: var(--bm-rest, 0) !important; }\n[data-crucible=\"bitmap\"][data-paused=\"true\"] .bm-frontier { animation: none !important; }\n[data-crucible=\"bitmap\"][data-paused=\"true\"] .bm-dot { animation: none !important; opacity: 1 !important; }\n/* Reduced motion → same designed static frame, no motion. */\n@media (prefers-reduced-motion: reduce) {\n  [data-crucible=\"bitmap\"] .bm-lit { animation: none !important; opacity: var(--bm-rest, 0) !important; }\n  [data-crucible=\"bitmap\"] .bm-frontier { animation: none !important; }\n  [data-crucible=\"bitmap\"] .bm-dot { animation: none !important; opacity: 1 !important; }\n}\n`;\n\nconst TAU = Math.PI * 2;\n\n// A single pixel: off base + a lit overlay whose opacity is driven by --bm-o\n// (static) and, when animated, restores to --bm-rest under paused/reduced.\nfunction Cell({\n  o,\n  rest,\n  frontier,\n  animated,\n  delay,\n}: {\n  o?: number;\n  rest: number;\n  frontier?: boolean;\n  animated?: boolean;\n  delay?: string;\n}) {\n  const litStyle: React.CSSProperties = {\n    \"--bm-rest\": rest,\n    ...(o !== undefined ? { \"--bm-o\": o } : null),\n    ...(animated ? { animationDelay: delay } : null),\n  } as React.CSSProperties;\n  return (\n    <span className=\"bm-cell\" aria-hidden>\n      <span className={cn(\"bm-lit\", frontier && \"bm-frontier\")} style={litStyle} />\n    </span>\n  );\n}\n\n/**\n * Bitmap — a chunky 8-bit pixel loader. `bar` is a grid of hard-edged cells\n * that fill cell-by-cell: pass a `value` for a determinate fill (with a\n * pulsing frontier column) or omit it for an indeterminate lit band that\n * marches across and loops. `spinner` is a crisp bitmap radar — a square pixel\n * grid with a bright arc sweeping around a dim disc, snapping cell-to-cell via\n * `steps()` for a true retro throbber, no anti-alias in sight.\n *\n * Zero dependencies, SSR-safe, GPU-light (only pixel opacity animates). Sizes,\n * grid dimensions, colors, glow, and speed are all themeable; the region\n * carries `role=\"progressbar\"` with `aria-valuenow` when determinate. Under\n * `paused` or reduced motion it collapses to a designed resting frame — a\n * half-filled bar, a lit arc — never a blank grid.\n */\nexport function Bitmap({\n  variant = \"bar\",\n  value,\n  cells = 24,\n  rows = 3,\n  grid = 7,\n  size = 10,\n  gap = 2,\n  radius = 1,\n  speed = 1,\n  paused = false,\n  onColor = \"#e8ecf5\",\n  offColor = \"#22252e\",\n  glowColor = \"#9fd8ff\",\n  label = \"Loading\",\n  showLabel = false,\n  labelText = \"LOADING\",\n  className,\n  style,\n  ...rest\n}: BitmapProps) {\n  const cols = Math.max(3, Math.min(Math.round(cells), 80));\n  const rowCount = Math.max(1, Math.min(Math.round(rows), 12));\n  // Force odd so the spinner has a true single-cell centre.\n  const gridN = (() => {\n    const n = Math.max(3, Math.min(Math.round(grid), 21));\n    return n % 2 === 0 ? n + 1 : n;\n  })();\n  const duration = `${(1.6 / Math.max(speed, 0.05)).toFixed(3)}s`;\n\n  const determinate = variant === \"bar\" && value !== undefined && Number.isFinite(value);\n  const clamped = determinate ? Math.max(0, Math.min(100, value as number)) : 0;\n  const filledCols = determinate ? Math.round((clamped / 100) * cols) : 0;\n\n  const vars = {\n    \"--bm-on\": onColor,\n    \"--bm-off\": offColor,\n    \"--bm-glow\": glowColor,\n    \"--bm-radius\": `${Math.max(0, radius)}px`,\n    \"--bm-dur\": duration,\n    \"--bm-dotsize\": `${Math.max(4, Math.round(size * 0.7))}px`,\n    ...style,\n  } as React.CSSProperties;\n\n  const gridStyle: React.CSSProperties = { gap };\n\n  // --- BAR ---------------------------------------------------------------\n  let body: React.ReactNode = null;\n  let animMode: \"march\" | \"sweep\" | undefined;\n\n  if (variant === \"bar\") {\n    animMode = determinate ? undefined : \"march\";\n    const restBand = Math.round(cols * 0.6); // frozen \"loading\" frame\n    body = (\n      <div\n        className=\"bm-grid\"\n        style={{\n          ...gridStyle,\n          gridTemplateColumns: `repeat(${cols}, ${size}px)`,\n          gridTemplateRows: `repeat(${rowCount}, ${size}px)`,\n          gridAutoFlow: \"column\",\n        }}\n      >\n        {Array.from({ length: cols }, (_, x) =>\n          Array.from({ length: rowCount }, (_, y) => {\n            if (determinate) {\n              const on = x < filledCols;\n              const isFrontier = on && x === filledCols - 1;\n              return (\n                <Cell\n                  key={`${x}-${y}`}\n                  o={on ? 1 : 0}\n                  rest={on ? 1 : 0}\n                  frontier={isFrontier}\n                />\n              );\n            }\n            // Indeterminate: every column marches, phase-offset by its position\n            // so a lit band sweeps left→right (column x peaks x/cols of a cycle\n            // after column 0). Resting frame = first 60% lit.\n            return (\n              <Cell\n                key={`${x}-${y}`}\n                rest={x < restBand ? 1 : 0.1}\n                animated\n                delay={`${(-(1 - x / cols) * (1.6 / Math.max(speed, 0.05))).toFixed(3)}s`}\n              />\n            );\n          })\n        )}\n      </div>\n    );\n  } else {\n    // --- SPINNER ---------------------------------------------------------\n    animMode = \"sweep\";\n    const c = (gridN - 1) / 2;\n    const discR = c + 0.55; // include the near-round disc, drop the corners\n    body = (\n      <div\n        className=\"bm-grid\"\n        style={{\n          ...gridStyle,\n          gridTemplateColumns: `repeat(${gridN}, ${size}px)`,\n          gridTemplateRows: `repeat(${gridN}, ${size}px)`,\n        }}\n      >\n        {Array.from({ length: gridN }, (_, y) =>\n          Array.from({ length: gridN }, (_, x) => {\n            const dx = x - c;\n            const dy = y - c;\n            const dist = Math.hypot(dx, dy);\n            // Centre + corners stay dark, so the arc sweeps a clean disc.\n            if (dist < 0.9 || dist > discR) {\n              return (\n                <span key={`${x}-${y}`} className=\"bm-cell\" aria-hidden>\n                  <span className=\"bm-lit\" style={{ \"--bm-rest\": 0 } as React.CSSProperties} />\n                </span>\n              );\n            }\n            // Angle, 0 at top, increasing clockwise.\n            const angle = ((Math.atan2(dx, -dy) + TAU) % TAU) / TAU; // 0..1\n            // Phase-offset so the head travels clockwise (later angle = later peak).\n            const delay = `${(-(1 - angle) * (1.6 / Math.max(speed, 0.05))).toFixed(3)}s`;\n            // Resting comet: brightest at the head, fading over ~110°.\n            const headDist = Math.min(angle, 1 - angle) * 2; // 0 at head, 1 opposite\n            const rest = Math.max(0.14, 1 - headDist * 2.6);\n            return (\n              <Cell\n                key={`${x}-${y}`}\n                rest={Number(rest.toFixed(3))}\n                animated\n                delay={delay}\n              />\n            );\n          })\n        )}\n      </div>\n    );\n  }\n\n  return (\n    <div\n      {...rest}\n      data-crucible=\"bitmap\"\n      data-anim={animMode}\n      data-paused={paused ? \"true\" : undefined}\n      role={determinate ? \"progressbar\" : \"status\"}\n      aria-label={label}\n      aria-busy={determinate ? clamped < 100 : true}\n      aria-valuemin={determinate ? 0 : undefined}\n      aria-valuemax={determinate ? 100 : undefined}\n      aria-valuenow={determinate ? Math.round(clamped) : undefined}\n      className={cn(\"relative inline-flex flex-col items-center gap-3\", className)}\n      style={vars}\n    >\n      <style>{STYLE}</style>\n      {body}\n\n      {showLabel && (\n        <div\n          aria-hidden\n          className=\"flex items-center\"\n          style={{ gap: `${Math.max(3, gap * 2)}px` }}\n        >\n          <span\n            className=\"font-mono uppercase leading-none\"\n            style={{\n              color: onColor,\n              fontSize: `${Math.max(8, Math.round(size * 0.9))}px`,\n              letterSpacing: `${Math.max(2, Math.round(size * 0.35))}px`,\n            }}\n          >\n            {labelText}\n          </span>\n          <span className=\"flex items-center\" style={{ gap: `${Math.max(2, gap)}px` }}>\n            {[0, 1, 2].map((i) => (\n              <span\n                key={i}\n                className=\"bm-dot\"\n                style={{ animationDelay: `${(-(1 - i / 3) * (0.75 * (1.6 / Math.max(speed, 0.05)))).toFixed(3)}s` }}\n              />\n            ))}\n          </span>\n        </div>\n      )}\n\n      <span className=\"sr-only\">{label}</span>\n    </div>\n  );\n}\n"
    }
  ]
}