{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fireflies",
  "type": "registry:ui",
  "title": "Fireflies",
  "description": "A calm, magical night: warm glowing points drift along gentle seeded wandering paths, each softly breathing inside an additive-glow halo and trailing a short fading wake. Near fireflies big/bright/roaming, far ones dim pinpricks — real depth parallax. 2D canvas with additive blending, seeded PRNG for a deterministic SSR-safe layout, pauses offscreen, honors prefers-reduced-motion with a still field of glowing points.",
  "dependencies": [],
  "registryDependencies": [
    "https://crucibleui.com/r/use-visibility-pause.json",
    "https://crucibleui.com/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/default/backgrounds/fireflies/fireflies.tsx",
      "type": "registry:ui",
      "target": "components/crucible/fireflies.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 FirefliesProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  /**\n   * How many fireflies drift in the field. Clamped 8–160. Near ones are large,\n   * bright and roam widely; far ones are tiny, dim pinpricks — real depth.\n   * @default 46\n   */\n  count?: number;\n  /**\n   * Glow palette, hottest → coolest, mapped across the depth field (nearest\n   * fireflies take the first color). Defaults to a warm bioluminescent glow:\n   * soft gold melting into a gentle leaf-green. Pass your own for a different\n   * night — cool `[\"#dbeafe\", \"#a5f3fc\", \"#818cf8\"]` reads as winter starlight.\n   * @default [\"#fff1c1\", \"#ffd166\", \"#b5e48c\"]\n   */\n  colors?: string[];\n  /** Drift / breathing-speed multiplier. 1 = default, 2 = twice as lively. @default 1 */\n  speed?: number;\n  /** Overall glow brightness and bloom, 0–2. @default 1 */\n  intensity?: number;\n  /** Deterministic seed for the wander layout — same seed, same night. @default 1 */\n  seed?: number;\n  /** devicePixelRatio ceiling. Clamped 1–2. @default 2 */\n  dpr?: number;\n  /** Freeze the field at a static, aesthetic frame of glowing points. @default false */\n  paused?: boolean;\n}\n\nconst DEFAULT_COLORS = [\"#fff1c1\", \"#ffd166\", \"#b5e48c\"];\n// Deep midnight bed — cool blue-black so the warm fireflies read as light.\nconst BG: [number, number, number] = [7, 9, 16];\n// Per-frame fade toward the bed: leaves each firefly a short, soft trail.\nconst TRAIL_FADE = 0.14;\n\n/** Deterministic mulberry32 PRNG — same seed always produces the same field. */\nfunction mulberry32(seed: number): () => number {\n  let a = seed >>> 0;\n  return () => {\n    a |= 0;\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\n/** Normalize any CSS color to [r,g,b] via a throwaway 1×1 draw. */\nfunction colorToRgb(ctx: CanvasRenderingContext2D, color: string): [number, number, number] {\n  ctx.clearRect(0, 0, 1, 1);\n  ctx.fillStyle = color;\n  ctx.fillRect(0, 0, 1, 1);\n  const d = ctx.getImageData(0, 0, 1, 1).data;\n  return [d[0], d[1], d[2]];\n}\n\n/**\n * Pre-render one soft radial-glow sprite per palette color: a near-white hot\n * core fading through the color into a transparent halo. Built once, then\n * blitted additively per firefly so overlaps bloom — no per-frame gradients.\n */\nfunction makeSprite(rgb: [number, number, number], scale: number): HTMLCanvasElement {\n  const D = Math.max(24, Math.round(80 * scale));\n  const c = document.createElement(\"canvas\");\n  c.width = D;\n  c.height = D;\n  const g = c.getContext(\"2d\")!;\n  const cx = D / 2;\n  const [r, gg, b] = rgb;\n  // Hot core mixes heavily toward white so each point reads incandescent.\n  const cr = Math.round(r * 0.3 + 255 * 0.7);\n  const cg = Math.round(gg * 0.3 + 255 * 0.7);\n  const cb = Math.round(b * 0.3 + 255 * 0.7);\n  const grad = g.createRadialGradient(cx, cx, 0, cx, cx, cx);\n  grad.addColorStop(0, `rgba(${cr},${cg},${cb},1)`);\n  grad.addColorStop(0.12, `rgba(${r},${gg},${b},0.9)`);\n  grad.addColorStop(0.34, `rgba(${r},${gg},${b},0.28)`);\n  grad.addColorStop(1, `rgba(${r},${gg},${b},0)`);\n  g.fillStyle = grad;\n  g.fillRect(0, 0, D, D);\n  return c;\n}\n\ninterface Firefly {\n  /** Home anchor in device px — the wander orbits this point. */\n  hx: number;\n  hy: number;\n  /** Depth 0 (far) → 1 (near): drives size, brightness, wander amplitude. */\n  z: number;\n  /** Glow sprite draw size, device px. */\n  size: number;\n  spriteIndex: number;\n  brightness: number;\n  // Two-frequency Lissajous wander per axis → organic, non-obviously-looping.\n  ampX: number;\n  ampY: number;\n  fx1: number;\n  fx2: number;\n  fy1: number;\n  fy2: number;\n  phx1: number;\n  phx2: number;\n  phy1: number;\n  phy2: number;\n  // Breathing pulse.\n  pulseFreq: number;\n  pulsePhase: number;\n}\n\nfunction makeFirefly(\n  rand: () => number,\n  w: number,\n  h: number,\n  scale: number,\n  paletteLen: number\n): Firefly {\n  // Skew depth toward \"far\" so a few near fireflies float over a dim deep field.\n  const z = rand() * rand();\n  const size = (11 + z * 34) * scale;\n  return {\n    hx: rand() * w,\n    hy: rand() * h,\n    z,\n    size,\n    spriteIndex: Math.min(paletteLen - 1, Math.floor((1 - z) * paletteLen)),\n    brightness: 0.32 + z * 0.55,\n    ampX: (10 + z * 46) * scale,\n    ampY: (8 + z * 38) * scale,\n    fx1: 0.12 + rand() * 0.22,\n    fx2: 0.05 + rand() * 0.12,\n    fy1: 0.1 + rand() * 0.2,\n    fy2: 0.04 + rand() * 0.11,\n    phx1: rand() * Math.PI * 2,\n    phx2: rand() * Math.PI * 2,\n    phy1: rand() * Math.PI * 2,\n    phy2: rand() * Math.PI * 2,\n    pulseFreq: 0.5 + rand() * 1.6,\n    pulsePhase: rand() * Math.PI * 2,\n  };\n}\n\nfunction buildField(\n  count: number,\n  seed: number,\n  w: number,\n  h: number,\n  scale: number,\n  paletteLen: number\n): Firefly[] {\n  const n = Math.min(160, Math.max(8, Math.round(count)));\n  const rand = mulberry32(seed);\n  const out: Firefly[] = [];\n  for (let i = 0; i < n; i++) out.push(makeFirefly(rand, w, h, scale, paletteLen));\n  return out;\n}\n\ninterface Internals {\n  ctx: CanvasRenderingContext2D;\n  w: number;\n  h: number;\n  scale: number;\n  sprites: HTMLCanvasElement[];\n  flies: Firefly[];\n  intensity: number;\n}\n\n/** Position of a firefly at effective time `t` (seconds, speed already applied). */\nfunction posX(f: Firefly, t: number): number {\n  return f.hx + f.ampX * (Math.sin(t * f.fx1 + f.phx1) * 0.7 + Math.sin(t * f.fx2 + f.phx2) * 0.3);\n}\nfunction posY(f: Firefly, t: number): number {\n  return f.hy + f.ampY * (Math.sin(t * f.fy1 + f.phy1) * 0.7 + Math.sin(t * f.fy2 + f.phy2) * 0.3);\n}\n\n/** Draw one firefly's additive glow. `pulse` in [0,1] scales alpha (breathing). */\nfunction drawFly(it: Internals, f: Firefly, x: number, y: number, pulse: number) {\n  const alpha = Math.max(0, Math.min(1, f.brightness * pulse * it.intensity));\n  if (alpha <= 0.004) return;\n  const { ctx } = it;\n  ctx.globalCompositeOperation = \"lighter\";\n  ctx.globalAlpha = alpha;\n  const s = f.size;\n  ctx.drawImage(it.sprites[f.spriteIndex], x - s / 2, y - s / 2, s, s);\n}\n\nfunction paintBackground(it: Internals) {\n  it.ctx.globalCompositeOperation = \"source-over\";\n  it.ctx.globalAlpha = 1;\n  it.ctx.fillStyle = `rgb(${BG[0]},${BG[1]},${BG[2]})`;\n  it.ctx.fillRect(0, 0, it.w, it.h);\n}\n\n/** One live frame: fade toward the bed (soft trails), then wander + breathe. */\nfunction tickFrame(it: Internals, t: number) {\n  it.ctx.globalCompositeOperation = \"source-over\";\n  it.ctx.globalAlpha = 1;\n  it.ctx.fillStyle = `rgba(${BG[0]},${BG[1]},${BG[2]},${TRAIL_FADE})`;\n  it.ctx.fillRect(0, 0, it.w, it.h);\n\n  for (const f of it.flies) {\n    const pulse = 0.42 + 0.58 * (0.5 + 0.5 * Math.sin(t * f.pulseFreq + f.pulsePhase));\n    drawFly(it, f, posX(f, t), posY(f, t), pulse);\n  }\n  it.ctx.globalCompositeOperation = \"source-over\";\n  it.ctx.globalAlpha = 1;\n}\n\n/** Static frame for paused / reduced-motion: a still field of glowing points. */\nfunction drawStatic(it: Internals) {\n  paintBackground(it);\n  for (const f of it.flies) {\n    // Vary brightness by phase so the frozen field reads as scattered lights,\n    // not a uniform grid — never blank, never flat.\n    const pulse = 0.55 + 0.45 * (0.5 + 0.5 * Math.sin(f.pulsePhase));\n    drawFly(it, f, posX(f, 0), posY(f, 0), pulse);\n  }\n  it.ctx.globalCompositeOperation = \"source-over\";\n  it.ctx.globalAlpha = 1;\n}\n\n/**\n * Fireflies — a calm, magical night. Warm glowing points drift along gentle,\n * seeded wandering paths in the dark, each softly breathing (pulsing brightness)\n * inside an additive-glow halo and trailing a short, fading wake. Near fireflies\n * are large, bright and roam widely; far ones are dim pinpricks — a real depth\n * parallax, not a uniform sprinkle. 2D canvas with additive blending; seeded\n * PRNG for a deterministic, SSR-safe layout, pauses offscreen and on hidden\n * tabs, and honors prefers-reduced-motion with a still field of glowing points.\n * Drop it inside any `relative` container.\n */\nexport function Fireflies({\n  count = 46,\n  colors = DEFAULT_COLORS,\n  speed = 1,\n  intensity = 1,\n  seed = 1,\n  dpr = 2,\n  paused = false,\n  className,\n  style,\n  ...props\n}: FirefliesProps) {\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const internalsRef = React.useRef<Internals | null>(null);\n  const reducedMotion = useReducedMotion();\n\n  const frozen = paused || reducedMotion;\n  const frozenRef = React.useRef(frozen);\n  frozenRef.current = frozen;\n  const speedFactor = Math.max(speed, 0);\n  const colorsKey = colors.join(\",\");\n\n  // Speed-scaled clock, accumulated per frame so speed changes never jump the swarm.\n  const phaseRef = React.useRef(0);\n  const lastElapsedRef = React.useRef(0);\n  const tick = React.useCallback(\n    (elapsed: number) => {\n      const it = internalsRef.current;\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      phaseRef.current += dt * speedFactor;\n      if (!it) return;\n      tickFrame(it, phaseRef.current);\n    },\n    [speedFactor]\n  );\n\n  const containerRef = useVisibilityPause<HTMLDivElement>(tick, { paused: frozen });\n\n  // Set up canvas, sprites and the field; rebuild on size / palette / seed / count.\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\n    const scale = Math.min(\n      typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1,\n      Math.min(Math.max(dpr, 1), 2)\n    );\n    const palette = colors.length ? colors : DEFAULT_COLORS;\n    const sprites = palette.map((c) => makeSprite(colorToRgb(ctx, c), scale));\n\n    const onContextLost = (e: Event) => e.preventDefault();\n    canvas.addEventListener(\"contextlost\", onContextLost);\n\n    const build = () => {\n      const w = Math.max(1, Math.round(container.clientWidth * scale));\n      const h = Math.max(1, Math.round(container.clientHeight * scale));\n      canvas.width = w;\n      canvas.height = h;\n      canvas.style.width = \"100%\";\n      canvas.style.height = \"100%\";\n      const it: Internals = {\n        ctx,\n        w,\n        h,\n        scale,\n        sprites,\n        flies: buildField(count, seed, w, h, scale, sprites.length),\n        intensity: Math.max(intensity, 0),\n      };\n      internalsRef.current = it;\n      // Paint one frame immediately so mount / resize never flashes empty.\n      if (frozenRef.current) drawStatic(it);\n      else {\n        paintBackground(it);\n        for (const f of it.flies) {\n          const pulse = 0.42 + 0.58 * (0.5 + 0.5 * Math.sin(f.pulsePhase));\n          drawFly(it, f, posX(f, 0), posY(f, 0), pulse);\n        }\n      }\n    };\n\n    const ro = new ResizeObserver(build);\n    ro.observe(container);\n    build();\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  }, [count, seed, colorsKey, dpr]);\n\n  // Live-sync intensity without rebuilding the field.\n  React.useEffect(() => {\n    const it = internalsRef.current;\n    if (!it) return;\n    it.intensity = Math.max(intensity, 0);\n    if (frozenRef.current) drawStatic(it);\n  }, [intensity]);\n\n  // Re-render the static frame in place when paused / reduced-motion toggles on.\n  React.useEffect(() => {\n    const it = internalsRef.current;\n    if (!it || !frozen) return;\n    drawStatic(it);\n  }, [frozen]);\n\n  return (\n    <div\n      ref={containerRef}\n      aria-hidden\n      data-crucible=\"fireflies\"\n      className={cn(\"absolute inset-0 -z-10 overflow-hidden\", className)}\n      style={style}\n      {...props}\n    >\n      <canvas ref={canvasRef} className=\"block h-full w-full\" />\n      {/* Soft vignette so the swarm sinks into the frame edges. */}\n      <div\n        className=\"pointer-events-none absolute inset-0\"\n        style={{\n          background:\n            \"radial-gradient(120% 120% at 50% 45%, transparent 52%, rgba(0,0,0,0.55) 100%)\",\n        }}\n      />\n    </div>\n  );\n}\n"
    }
  ]
}