{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "arcade",
  "type": "registry:ui",
  "title": "Arcade",
  "description": "8-bit attract-mode background: a chunky pixel starfield drifting on three parallax depth layers, seeded space-shooter sprites gliding across on a two-frame flap, and a faint pixel grid receding to a horizon. Crisp unsmoothed pixels. Canvas 2D, seeded, offscreen-paused, reduced-motion-safe.",
  "dependencies": [],
  "registryDependencies": [
    "https://crucibleui.com/r/use-visibility-pause.json",
    "https://crucibleui.com/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/default/backgrounds/arcade/arcade.tsx",
      "type": "registry:ui",
      "target": "components/crucible/arcade.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"@/hooks/use-reduced-motion\";\nimport { useVisibilityPause } from \"@/hooks/use-visibility-pause\";\n\nexport interface ArcadeProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /**\n   * Star / grid / sprite palette, brightest → deepest. Index `0` is the near\n   * (foreground) star + sprite tint, `1` the mid layer + floor grid, `2` the\n   * far layer. Extra entries recolor the drifting sprites. Defaults to a cool\n   * cyan-to-indigo starfield on near-black — pass warm tones for a synthwave\n   * cabinet or all-white for classic mono.\n   * @default [\"#eaf6ff\", \"#7fdfff\", \"#4aa6ff\"]\n   */\n  colors?: string[];\n  /** Drift / flap / floor-scroll speed multiplier. 1 = default. @default 1 */\n  speed?: number;\n  /** Overall star brightness and grid/sprite opacity, 0–2. @default 1 */\n  intensity?: number;\n  /** Star-count multiplier across all layers, 0.2–3. @default 1 */\n  density?: number;\n  /** Chunk size of one \"pixel\" in CSS px — the whole scene snaps to it. Clamped 2–8. @default 3 */\n  pixelSize?: number;\n  /** devicePixelRatio ceiling. Clamped 1–2. @default 2 */\n  dpr?: number;\n  /** Freeze the scene at a static, populated attract-mode frame. @default false */\n  paused?: boolean;\n}\n\nconst DEFAULT_COLORS: string[] = [\"#eaf6ff\", \"#7fdfff\", \"#4aa6ff\"];\n\n/** Deterministic mulberry32 — seeded so the field is identical SSR intent ↔ CSR. */\nfunction mulberry32(seed: number): () => number {\n  let a = seed >>> 0;\n  return () => {\n    a = (a + 0x6d2b79f5) | 0;\n    let t = Math.imul(a ^ (a >>> 15), 1 | a);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nconst SEED = 0x51ed270b;\n\nfunction hexToRgb(hex: string): [number, number, number] {\n  let h = hex.replace(\"#\", \"\").trim();\n  if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];\n  const int = parseInt(h, 16);\n  if (Number.isNaN(int) || h.length !== 6) return [234, 246, 255];\n  return [(int >> 16) & 255, (int >> 8) & 255, int & 255];\n}\n\n/**\n * 8-bit sprite bitmaps — two flap frames each. `#` = a filled pixel-block.\n * Clean-room classic space-shooter silhouettes (crab + octopus invaders).\n */\nconst SPRITES: { w: number; h: number; frames: string[][] }[] = [\n  {\n    w: 11,\n    h: 8,\n    frames: [\n      [\n        \"..#.....#..\",\n        \"...#...#...\",\n        \"..#######..\",\n        \".##.###.##.\",\n        \"###########\",\n        \"#.#######.#\",\n        \"#.#.....#.#\",\n        \"...##.##...\",\n      ],\n      [\n        \"..#.....#..\",\n        \"#..#...#..#\",\n        \"#.#######.#\",\n        \"###.###.###\",\n        \"###########\",\n        \".#########.\",\n        \"..#.....#..\",\n        \".#.......#.\",\n      ],\n    ],\n  },\n  {\n    w: 12,\n    h: 8,\n    frames: [\n      [\n        \"....####....\",\n        \".##########.\",\n        \"############\",\n        \"###..##..###\",\n        \"############\",\n        \"...##..##...\",\n        \"..##.##.##..\",\n        \"##........##\",\n      ],\n      [\n        \"....####....\",\n        \".##########.\",\n        \"############\",\n        \"###..##..###\",\n        \"############\",\n        \"..###..###..\",\n        \".##..##..##.\",\n        \"..##....##..\",\n      ],\n    ],\n  },\n];\n\nconst NLINES = 16; // floor rows\nconst NVERT = 18; // floor verticals\n\ninterface Star {\n  x: number; // CSS px\n  y: number;\n  layer: 0 | 1 | 2; // 0 near, 1 mid, 2 far\n  vy: number; // CSS px/s drift\n  baseAlpha: number;\n  colorIdx: number;\n  twPhase: number;\n  twFreq: number;\n}\n\ninterface Sprite {\n  x: number; // CSS px (top-left)\n  y: number;\n  dir: 1 | -1;\n  vx: number; // CSS px/s\n  type: number; // SPRITES index\n  colorIdx: number;\n  flapPhase: number;\n  flapRate: number; // toggles/sec\n}\n\ninterface Internals {\n  ctx: CanvasRenderingContext2D;\n  width: number;\n  height: number;\n  P: number; // pixel-block size, CSS px\n  palette: [number, number, number][];\n  stars: Star[];\n  sprites: Sprite[];\n  gridOffset: number;\n  intensity: number;\n  speed: number;\n  rng: () => number;\n  bg: string;\n}\n\nfunction buildStars(w: number, h: number, P: number, density: number): Star[] {\n  const rng = mulberry32(SEED ^ 0x1b56c4e9);\n  const area = w * h;\n  const farCount = Math.min(360, Math.max(12, Math.round((area / 2600) * density)));\n  const midCount = Math.round(farCount * 0.5);\n  const nearCount = Math.round(farCount * 0.16);\n  const stars: Star[] = [];\n\n  const push = (layer: 0 | 1 | 2, n: number) => {\n    for (let i = 0; i < n; i++) {\n      const near = layer === 0;\n      const mid = layer === 1;\n      stars.push({\n        x: rng() * w,\n        y: rng() * h,\n        layer,\n        vy: (near ? 34 : mid ? 18 : 8) * (0.75 + rng() * 0.5),\n        baseAlpha: near ? 0.85 : mid ? 0.6 : 0.32,\n        colorIdx: layer, // near→0, mid→1, far→2\n        twPhase: rng() * Math.PI * 2,\n        twFreq: near ? 2.2 + rng() * 2.4 : mid ? 1.2 + rng() * 1.6 : 0,\n      });\n    }\n  };\n  push(2, farCount);\n  push(1, midCount);\n  push(0, nearCount);\n  return stars;\n}\n\nfunction buildSprites(w: number, h: number, paletteLen: number): Sprite[] {\n  const rng = mulberry32(SEED ^ 0x27d4eb2f);\n  const out: Sprite[] = [];\n  const band = Math.max(h * 0.12, 40);\n  for (let i = 0; i < 3; i++) {\n    const dir: 1 | -1 = rng() < 0.5 ? 1 : -1;\n    out.push({\n      // Seeded across the frame so the static / first frame already shows craft.\n      x: rng() * w,\n      y: band + rng() * (h * 0.42),\n      dir,\n      vx: (22 + rng() * 20) * (0.85 + rng() * 0.4),\n      type: (rng() * SPRITES.length) | 0,\n      colorIdx: (rng() * paletteLen) | 0,\n      flapPhase: rng() * 10,\n      flapRate: 1.4 + rng() * 0.9,\n    });\n  }\n  return out;\n}\n\nfunction recycleSprite(s: Sprite, it: Internals) {\n  const rng = it.rng;\n  const spr = SPRITES[s.type];\n  const wpx = spr.w * it.P * 2;\n  s.dir = rng() < 0.5 ? 1 : -1;\n  s.type = (rng() * SPRITES.length) | 0;\n  s.colorIdx = (rng() * it.palette.length) | 0;\n  s.vx = (22 + rng() * 20) * (0.85 + rng() * 0.4);\n  s.y = Math.max(it.height * 0.12, 40) + rng() * (it.height * 0.42);\n  s.flapRate = 1.4 + rng() * 0.9;\n  // Enter from the side it travels toward, with a seeded gap so appearances\n  // are occasional rather than a constant conveyor.\n  const gap = wpx + rng() * (it.width * 0.9 + wpx);\n  s.x = s.dir === 1 ? -gap : it.width + gap;\n}\n\nfunction drawBlock(\n  ctx: CanvasRenderingContext2D,\n  x: number,\n  y: number,\n  size: number,\n  rgb: [number, number, number],\n  alpha: number\n) {\n  if (alpha <= 0.01) return;\n  ctx.fillStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${alpha < 1 ? alpha : 1})`;\n  ctx.fillRect(Math.round(x), Math.round(y), size, size);\n}\n\n/** Perspective pixel-grid floor: rows compress toward a horizon, verticals fan from a vanishing point. */\nfunction drawGrid(it: Internals, offset: number) {\n  const { ctx, width, height, P } = it;\n  const hY = Math.round(height * 0.66);\n  const H = height - hY;\n  if (H <= 4) return;\n  const cx = width / 2;\n  const rgb = it.palette[Math.min(1, it.palette.length - 1)];\n  const thick = Math.max(1, Math.round(P * 0.6));\n  const dot = P;\n  const gAmt = Math.min(Math.max(it.intensity, 0), 2);\n\n  for (let k = 0; k < NLINES; k++) {\n    let p = ((k + offset) % NLINES) / NLINES;\n    if (p < 0) p += 1;\n    const persp = p * p; // near horizon → 0, bottom → 1\n    const sy = hY + H * persp;\n    const fade = (0.12 + 0.88 * p) * gAmt;\n    // Floor row — a thin chunky full-width scan line.\n    ctx.fillStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${0.06 * fade})`;\n    ctx.fillRect(0, Math.round(sy), width, thick);\n    // Vertical intersections — dots converging on the vanishing point.\n    for (let v = 0; v <= NVERT; v++) {\n      const bx = (v / NVERT) * width * 2 - width * 0.5;\n      const x = cx + (bx - cx) * persp;\n      if (x < -dot || x > width + dot) continue;\n      drawBlock(ctx, x, sy, dot, rgb, 0.16 * fade);\n    }\n  }\n}\n\nfunction drawSprite(it: Internals, s: Sprite, frame: number) {\n  const spr = SPRITES[s.type];\n  const rows = spr.frames[frame % spr.frames.length];\n  const block = it.P * 2;\n  const rgb = it.palette[s.colorIdx % it.palette.length];\n  const alpha = Math.min(0.85 * it.intensity, 1);\n  const x0 = Math.round(s.x / it.P) * it.P;\n  const y0 = Math.round(s.y / it.P) * it.P;\n  it.ctx.fillStyle = `rgba(${rgb[0]},${rgb[1]},${rgb[2]},${alpha})`;\n  for (let r = 0; r < rows.length; r++) {\n    const row = rows[r];\n    for (let c = 0; c < row.length; c++) {\n      if (row[c] === \"#\") {\n        it.ctx.fillRect(x0 + c * block, y0 + r * block, block, block);\n      }\n    }\n  }\n}\n\n/** One frame. `t` is elapsed seconds (phases); `useStatic` freezes drift, flap and scroll. */\nfunction draw(it: Internals, dt: number, t: number, useStatic: boolean) {\n  const { ctx, width, height, P } = it;\n\n  ctx.globalCompositeOperation = \"source-over\";\n  ctx.fillStyle = it.bg;\n  ctx.fillRect(0, 0, width, height);\n\n  // ------------------------------------------------------------------ floor --\n  if (!useStatic) it.gridOffset += dt * it.speed * 1.1;\n  drawGrid(it, useStatic ? 0 : it.gridOffset);\n\n  // ------------------------------------------------------------------ stars --\n  for (let i = 0; i < it.stars.length; i++) {\n    const s = it.stars[i];\n    if (!useStatic) {\n      s.y += s.vy * it.speed * dt;\n      if (s.y > height + P) s.y -= height + P * 2;\n    }\n    const rgb = it.palette[s.colorIdx % it.palette.length];\n    const tw =\n      s.twFreq && !useStatic ? 0.55 + 0.45 * Math.sin(s.twPhase + t * s.twFreq) : 0.9;\n    const alpha = Math.min(s.baseAlpha * tw * it.intensity, 1);\n    const x = Math.round(s.x / P) * P;\n    const y = Math.round(s.y / P) * P;\n    if (s.layer === 0) {\n      // Near stars are a bright pixel-cross (classic twinkle sprite).\n      drawBlock(ctx, x, y, P, rgb, alpha);\n      drawBlock(ctx, x - P, y, P, rgb, alpha * 0.7);\n      drawBlock(ctx, x + P, y, P, rgb, alpha * 0.7);\n      drawBlock(ctx, x, y - P, P, rgb, alpha * 0.7);\n      drawBlock(ctx, x, y + P, P, rgb, alpha * 0.7);\n    } else {\n      drawBlock(ctx, x, y, P, rgb, alpha);\n    }\n  }\n\n  // ---------------------------------------------------------------- sprites --\n  for (let i = 0; i < it.sprites.length; i++) {\n    const s = it.sprites[i];\n    const spr = SPRITES[s.type];\n    if (!useStatic) {\n      s.x += s.vx * s.dir * it.speed * dt;\n      const wpx = spr.w * it.P * 2;\n      if (s.dir === 1 && s.x > width + wpx) recycleSprite(s, it);\n      else if (s.dir === -1 && s.x < -wpx) recycleSprite(s, it);\n    }\n    const frame = useStatic ? 0 : Math.floor(s.flapPhase + t * s.flapRate) % 2;\n    drawSprite(it, s, frame);\n  }\n}\n\n/** Cool near-black bed derived from the far-star hue — never flat #000. */\nfunction baseFill(far: [number, number, number]): string {\n  return `rgb(${(4 + far[0] * 0.01) | 0}, ${(6 + far[1] * 0.012) | 0}, ${(10 + far[2] * 0.018) | 0})`;\n}\n\n/**\n * Arcade — an 8-bit attract-mode background. A chunky pixel starfield drifts on\n * three parallax depth layers (bright near cross-stars, mid and far dots),\n * seeded space-shooter sprites glide across on a two-frame flap, and a faint\n * pixel grid recedes to a horizon like a cabinet's demo screen. Crisp,\n * unsmoothed pixels snapped to a block grid. Canvas 2D: seeded and SSR-safe,\n * paused offscreen and on hidden tabs, reduced-motion-safe (a static, fully\n * populated frame — no drift). Drop it inside any `relative` container.\n */\nexport function Arcade({\n  colors = DEFAULT_COLORS,\n  speed = 1,\n  intensity = 1,\n  density = 1,\n  pixelSize = 3,\n  dpr = 2,\n  paused = false,\n  className,\n  ...props\n}: ArcadeProps) {\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const internalsRef = React.useRef<Internals | null>(null);\n  const reducedMotion = useReducedMotion();\n  const staticFrame = paused || reducedMotion;\n  const staticFrameRef = React.useRef(staticFrame);\n  staticFrameRef.current = staticFrame;\n  const lastElapsedRef = React.useRef(0);\n\n  const P = Math.round(Math.min(Math.max(pixelSize, 2), 8));\n  const dens = Math.min(Math.max(density, 0.2), 3);\n  const colorsKey = colors.join(\",\");\n\n  const tick = React.useCallback((elapsed: number) => {\n    const it = internalsRef.current;\n    if (!it) return;\n    let dt = elapsed - lastElapsedRef.current;\n    lastElapsedRef.current = elapsed;\n    if (dt > 0.1) dt = 0.1; // clamp tab-restore jumps\n    if (dt < 0) dt = 0;\n    draw(it, dt, elapsed, staticFrameRef.current);\n  }, []);\n\n  const containerRef = useVisibilityPause<HTMLDivElement>(tick, { paused: staticFrame });\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    const container = containerRef.current;\n    if (!canvas || !container) return;\n\n    const ctx = canvas.getContext(\"2d\", { alpha: false });\n    if (!ctx) return;\n    ctx.imageSmoothingEnabled = false;\n\n    const effectiveDpr = Math.min(\n      typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1,\n      Math.min(Math.max(dpr, 1), 2)\n    );\n\n    const source = colors.length ? colors : DEFAULT_COLORS;\n    const palette = source.map(hexToRgb);\n    while (palette.length < 3) palette.push(palette[palette.length - 1]);\n\n    const it: Internals = {\n      ctx,\n      width: 0,\n      height: 0,\n      P,\n      palette,\n      stars: [],\n      sprites: [],\n      gridOffset: 0,\n      intensity,\n      speed,\n      rng: mulberry32(SEED ^ 0x85ebca6b),\n      bg: baseFill(palette[Math.min(2, palette.length - 1)]),\n    };\n    internalsRef.current = it;\n\n    const onContextLost = (e: Event) => e.preventDefault();\n    canvas.addEventListener(\"contextlost\", onContextLost);\n\n    const resize = () => {\n      const w = Math.max(container.clientWidth, 1);\n      const h = Math.max(container.clientHeight, 1);\n      canvas.width = Math.round(w * effectiveDpr);\n      canvas.height = Math.round(h * effectiveDpr);\n      canvas.style.width = \"100%\";\n      canvas.style.height = \"100%\";\n      ctx.setTransform(effectiveDpr, 0, 0, effectiveDpr, 0, 0);\n      ctx.imageSmoothingEnabled = false;\n\n      it.width = w;\n      it.height = h;\n      it.stars = buildStars(w, h, P, dens);\n      it.sprites = buildSprites(w, h, palette.length);\n      it.gridOffset = 0;\n\n      // Never flash empty — paint one frame immediately.\n      draw(it, 0, 0, staticFrameRef.current);\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(container);\n    resize();\n\n    return () => {\n      ro.disconnect();\n      canvas.removeEventListener(\"contextlost\", onContextLost);\n      internalsRef.current = null;\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [dpr, P, dens, colorsKey]);\n\n  // Live-sync speed / intensity without rebuilding (and resetting) the field.\n  React.useEffect(() => {\n    const it = internalsRef.current;\n    if (!it) return;\n    it.speed = speed;\n    it.intensity = intensity;\n    if (staticFrameRef.current) draw(it, 0, 0, true);\n  }, [speed, intensity]);\n\n  // Re-render the frozen frame in place when paused / reduced-motion toggles.\n  React.useEffect(() => {\n    const it = internalsRef.current;\n    if (!it || !staticFrame) return;\n    draw(it, 0, 0, true);\n  }, [staticFrame]);\n\n  return (\n    <div\n      ref={containerRef}\n      aria-hidden\n      data-crucible=\"arcade\"\n      className={cn(\"absolute inset-0 -z-10 overflow-hidden bg-neutral-950\", className)}\n      {...props}\n    >\n      <canvas ref={canvasRef} className=\"block h-full w-full\" />\n      {/* CRT scanlines + faint vignette — sink the cabinet into the frame. */}\n      <div\n        className=\"pointer-events-none absolute inset-0\"\n        style={{\n          backgroundImage:\n            \"repeating-linear-gradient(0deg, rgba(0,0,0,0.32) 0px, rgba(0,0,0,0.32) 1px, transparent 1px, transparent 3px)\",\n          opacity: 0.5,\n        }}\n      />\n      <div\n        className=\"pointer-events-none absolute inset-0\"\n        style={{\n          background:\n            \"radial-gradient(120% 120% at 50% 40%, transparent 58%, rgba(0,0,0,0.6) 100%)\",\n        }}\n      />\n    </div>\n  );\n}\n"
    }
  ]
}