{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fireflies",
  "type": "registry:component",
  "title": "Fireflies",
  "description": "Fireflies that drift at three depths and blink with a warm glow; they curiously approach the cursor and circle it.",
  "author": "Marcelo Treviño Velázquez <https://furin.dev>",
  "dependencies": [],
  "categories": [
    "Backgrounds"
  ],
  "docs": "Installed in components/furin/fireflies/. Docs, live controls and props table: https://furin.dev/components/fireflies/",
  "files": [
    {
      "path": "registry/fireflies/Fireflies.tsx",
      "content": "\"use client\";\nimport { useEffect, useRef } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport \"./Fireflies.css\";\n\nexport interface FirefliesProps {\n  /** Contenido que va encima de las luciérnagas. */\n  children?: ReactNode;\n  /** Color cálido de la luz. */\n  color?: string;\n  /** Color del fondo de la noche. */\n  background?: string;\n  /** Cuántas luciérnagas hay (1 = normal). */\n  count?: number;\n  /** Qué tan rápido vuelan y parpadean. */\n  speed?: number;\n  /** Qué tanto se acercan al cursor por curiosidad. */\n  curiosity?: number;\n  /** Clase extra para el contenedor. */\n  className?: string;\n  /** Estilos extra para el contenedor. */\n  style?: CSSProperties;\n}\n\nlet colorCtx: CanvasRenderingContext2D | null = null;\nconst RGB_CACHE = new Map<string, [number, number, number]>();\nfunction rgb(css: string, def: [number, number, number]): [number, number, number] {\n  const hit = RGB_CACHE.get(css);\n  if (hit) return hit;\n  try {\n    colorCtx ??= document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true })!;\n    colorCtx.clearRect(0, 0, 1, 1);\n    colorCtx.fillStyle = \"#010203\";\n    colorCtx.fillStyle = css;\n    colorCtx.fillRect(0, 0, 1, 1);\n    const d = colorCtx.getImageData(0, 0, 1, 1).data;\n    const out: [number, number, number] = [d[0], d[1], d[2]];\n    if (RGB_CACHE.size > 64) RGB_CACHE.clear();\n    RGB_CACHE.set(css, out);\n    return out;\n  } catch {\n    return def;\n  }\n}\n\nconst TRAZO = 8;\ninterface Luciernaga {\n  x: number; y: number; vx: number; vy: number;\n  rumbo: number; giro: number; vel: number;\n  z: number; periodo: number; fase: number; curio: number; hold: number;\n  hx: Float32Array; hy: Float32Array; hi: number; ht: number;\n}\n\n/** Fondo de luciérnagas: puntos de luz cálida que deambulan en tres profundidades, parpadean con su propio ritmo y se acercan con curiosidad al cursor. */\nexport function Fireflies({\n  children,\n  color = \"#ffd26b\",\n  background = \"#06080a\",\n  count = 1,\n  speed = 1,\n  curiosity = 1,\n  className = \"\",\n  style,\n}: FirefliesProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const props = useRef({ color, count, speed, curiosity });\n  props.current = { color, count, speed, curiosity };\n  const wakeRef = useRef<(() => void) | null>(null);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n    const canvas = document.createElement(\"canvas\");\n    canvas.className = \"furin-fireflies__lienzo\";\n    canvas.setAttribute(\"aria-hidden\", \"true\");\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n    root.insertBefore(canvas, root.firstChild);\n\n    const reducido = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    let w = 0, h = 0, dpr = 1, raf = 0, visible = true, muerto = false, ult = 0, t = reducido ? 5 : 0;\n    let bichos: Luciernaga[] = [];\n    let sprite: HTMLCanvasElement | null = null, spriteSuave: HTMLCanvasElement | null = null;\n    const ptr = { x: 0, y: 0, hay: false, k: 0 };\n    let cantClave = -1;\n\n    const armarSprites = () => {\n      const [r, g, b] = rgb(props.current.color, [255, 210, 107]);\n      const mk = (pinta: (c: CanvasRenderingContext2D) => void) => { const cv = document.createElement(\"canvas\"); cv.width = cv.height = 128; pinta(cv.getContext(\"2d\")!); return cv; };\n      // Nítido: núcleo casi blanco, halo cálido y una neblina muy tenue alrededor.\n      sprite = mk((c) => {\n        const gr = c.createRadialGradient(64, 64, 0, 64, 64, 64);\n        gr.addColorStop(0, \"rgba(255,252,235,1)\");\n        gr.addColorStop(0.05, `rgba(${Math.min(255, r + 25)},${Math.min(255, g + 30)},${Math.min(255, b + 70)},0.95)`);\n        gr.addColorStop(0.16, `rgba(${r},${g},${b},0.5)`);\n        gr.addColorStop(0.42, `rgba(${r},${g},${b},0.14)`);\n        gr.addColorStop(0.75, `rgba(${r},${g},${b},0.03)`);\n        gr.addColorStop(1, `rgba(${r},${g},${b},0)`);\n        c.fillStyle = gr; c.fillRect(0, 0, 128, 128);\n      });\n      // Bokeh de las cercanas: disco grande y difuso, sin núcleo.\n      spriteSuave = mk((c) => {\n        const gr = c.createRadialGradient(64, 64, 0, 64, 64, 64);\n        gr.addColorStop(0, `rgba(${r},${g},${b},0.55)`);\n        gr.addColorStop(0.6, `rgba(${r},${g},${b},0.4)`);\n        gr.addColorStop(0.85, `rgba(${r},${g},${b},0.14)`);\n        gr.addColorStop(1, `rgba(${r},${g},${b},0)`);\n        c.fillStyle = gr; c.fillRect(0, 0, 128, 128);\n      });\n    };\n\n    const armar = () => {\n      cantClave = props.current.count;\n      const n = Math.max(6, Math.round(((w * h) / 5200) * props.current.count));\n      bichos = Array.from({ length: n }, (_, i) => {\n        const z = Math.pow(Math.random(), 1.2);\n        const x = Math.random() * w, y = Math.random() * h;\n        return {\n          x, y, vx: 0, vy: 0, rumbo: Math.random() * 6.28, giro: 0, vel: 10 + Math.random() * 18, z,\n          periodo: 2.4 + Math.random() * 3.8, fase: Math.random() * 6.28 + i, curio: 0.35 + Math.random() * 0.65, hold: 38 + Math.random() * 60,\n          hx: new Float32Array(TRAZO).fill(x), hy: new Float32Array(TRAZO).fill(y), hi: 0, ht: 0,\n        };\n      });\n    };\n\n    const brillo = (b: Luciernaga) => {\n      // Destello estrecho con subida y bajada suaves; entre destellos queda una brasa tenue.\n      const s = 0.5 + 0.5 * Math.sin((t * Math.PI * 2) / b.periodo + b.fase);\n      return 0.1 + 0.9 * Math.pow(s, 2.6);\n    };\n\n    const pintar = (dt: number) => {\n      const p = props.current;\n      ptr.k += ((ptr.hay ? 1 : 0) - ptr.k) * (1 - Math.exp(-dt * 3));\n      ctx.setTransform(1, 0, 0, 1, 0, 0);\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      ctx.globalCompositeOperation = \"lighter\";\n\n      // Las lejanas se pintan primero (más pequeñas y tenues), las cercanas al final.\n      const orden = bichos.slice().sort((a, b) => a.z - b.z);\n      for (const b of orden) {\n        const sp = b.z; // 0 lejos, 1 cerca\n        // ── Vuelo: rumbo con giro aleatorio suave (ruido de Ornstein-Uhlenbeck) ──\n        b.giro += ((Math.random() - 0.5) * 9 - b.giro * 1.4) * dt;\n        b.rumbo += b.giro * dt;\n        const v = b.vel * (0.45 + 0.9 * sp) * p.speed * (0.8 + 0.4 * Math.sin(t * 0.7 + b.fase));\n        let ax = (Math.cos(b.rumbo) * v - b.vx) * 1.3, ay = (Math.sin(b.rumbo) * v * 0.8 - b.vy) * 1.3;\n        // Curiosidad: se acerca al cursor, se queda rondando a su distancia y lo orbita.\n        let cerca = 0;\n        if (ptr.k > 0.02 && p.curiosity > 0) {\n          const dx = ptr.x - b.x, dy = ptr.y - b.y, d = Math.hypot(dx, dy) || 1;\n          const R = 260 * b.curio;\n          if (d < R) {\n            const f = (1 - d / R);\n            cerca = f;\n            const nx = dx / d, ny = dy / d;\n            if (d > b.hold) { ax += nx * 170 * b.curio * f * p.curiosity * ptr.k; ay += ny * 170 * b.curio * f * p.curiosity * ptr.k; }\n            else { const e = (b.hold - d) / b.hold; ax -= nx * 120 * e; ay -= ny * 120 * e; }\n            // Órbita lenta alrededor del cursor.\n            ax += -ny * 55 * f * p.curiosity * ptr.k; ay += nx * 55 * f * p.curiosity * ptr.k;\n          }\n        }\n        // Frontera blanda: si se sale mucho de la escena, regresa.\n        const m = 30;\n        if (b.x < -m) ax += (-m - b.x) * 3; else if (b.x > w + m) ax -= (b.x - w - m) * 3;\n        if (b.y < -m) ay += (-m - b.y) * 3; else if (b.y > h + m) ay -= (b.y - h - m) * 3;\n        b.vx += ax * dt; b.vy += ay * dt;\n        const sv = Math.hypot(b.vx, b.vy), tope = 120;\n        if (sv > tope) { b.vx *= tope / sv; b.vy *= tope / sv; }\n        b.x += b.vx * dt; b.y += b.vy * dt + Math.sin(t * 1.9 + b.fase) * 4 * dt;\n\n        // ── Luz ──\n        const base = reducido ? 0.75 : brillo(b);\n        const luz = Math.min(1, base * (1 + cerca * 0.5));\n        // Estela corta que se guarda cada 45 ms.\n        b.ht += dt;\n        if (b.ht > 0.045) { b.ht = 0; b.hi = (b.hi + 1) % TRAZO; b.hx[b.hi] = b.x; b.hy[b.hi] = b.y; }\n\n        const esBokeh = sp > 0.88;\n        const radio = esBokeh ? 26 + 14 * sp : 9 + 20 * sp;\n        const alfa = esBokeh ? 0.12 + 0.3 * luz : (0.5 + 0.5 * sp) * luz;\n        const img = esBokeh ? spriteSuave : sprite;\n        if (!img) continue;\n        if (!esBokeh && luz > 0.25) {\n          for (let k = 1; k < TRAZO; k++) {\n            const q = (b.hi - k + TRAZO) % TRAZO;\n            const f = 1 - k / TRAZO;\n            ctx.globalAlpha = alfa * f * f * 0.5;\n            const rr = radio * (0.3 + 0.35 * f);\n            ctx.drawImage(img, b.hx[q] - rr, b.hy[q] - rr, rr * 2, rr * 2);\n          }\n        }\n        ctx.globalAlpha = Math.min(1, alfa);\n        ctx.drawImage(img, b.x - radio, b.y - radio, radio * 2, radio * 2);\n      }\n      ctx.globalAlpha = 1;\n      ctx.globalCompositeOperation = \"source-over\";\n    };\n\n    const paso = (ts: number) => {\n      raf = 0;\n      if (muerto || !visible) return;\n      const dt = Math.min(0.05, ult ? (ts - ult) / 1000 : 0.016);\n      ult = ts;\n      t += dt * props.current.speed;\n      pintar(dt);\n      raf = requestAnimationFrame(paso);\n    };\n    const wake = () => { if (reducido) return; if (!raf && visible && !muerto) { ult = 0; raf = requestAnimationFrame(paso); } };\n    wakeRef.current = () => {\n      armarSprites();\n      if (cantClave !== props.current.count) armar();\n      if (reducido) pintar(0.016); else wake();\n    };\n\n    const medir = () => {\n      dpr = Math.min(window.devicePixelRatio || 1, 2);\n      const r = root.getBoundingClientRect();\n      w = Math.max(1, r.width); h = Math.max(1, r.height);\n      canvas.width = Math.round(w * dpr); canvas.height = Math.round(h * dpr);\n      armarSprites();\n      armar();\n      if (reducido) pintar(0.016); else { for (let i = 0; i < 20; i++) pintar(0.03); wake(); }\n    };\n    const ro = new ResizeObserver(medir);\n    ro.observe(root);\n    const io = new IntersectionObserver(([e]) => { visible = e.isIntersecting; if (visible) wake(); }, { rootMargin: \"80px\" });\n    io.observe(root);\n\n    const onMove = (e: PointerEvent) => { const r = root.getBoundingClientRect(); ptr.x = e.clientX - r.left; ptr.y = e.clientY - r.top; ptr.hay = true; };\n    const onLeave = () => { ptr.hay = false; };\n    if (!reducido) {\n      root.addEventListener(\"pointermove\", onMove, { passive: true });\n      root.addEventListener(\"pointerdown\", onMove, { passive: true });\n      root.addEventListener(\"pointerleave\", onLeave);\n    }\n    medir();\n\n    return () => {\n      muerto = true;\n      wakeRef.current = null;\n      cancelAnimationFrame(raf);\n      ro.disconnect(); io.disconnect();\n      root.removeEventListener(\"pointermove\", onMove);\n      root.removeEventListener(\"pointerdown\", onMove);\n      root.removeEventListener(\"pointerleave\", onLeave);\n      canvas.remove();\n    };\n  }, []);\n\n  useEffect(() => { wakeRef.current?.(); }, [color, count, speed, curiosity]);\n\n  const vars = { \"--furin-fireflies-bg\": background } as CSSProperties;\n  return (\n    <div ref={rootRef} className={`furin-fireflies ${className}`} style={{ ...vars, ...style }}>\n      <span className=\"furin-fireflies__bruma\" aria-hidden=\"true\" />\n      {children != null ? <div className=\"furin-fireflies__contenido\">{children}</div> : null}\n    </div>\n  );\n}\n\nexport default Fireflies;\n",
      "type": "registry:component",
      "target": "components/furin/fireflies/Fireflies.tsx"
    },
    {
      "path": "registry/fireflies/Fireflies.css",
      "content": ".furin-fireflies {\n  position: absolute;\n  inset: 0;\n  display: grid;\n  place-items: center;\n  overflow: hidden;\n  isolation: isolate;\n  background: var(--furin-fireflies-bg, #06080a);\n  touch-action: pan-y;\n}\n.furin-fireflies__lienzo { position: absolute; inset: 0; width: 100%; height: 100%; display: block; z-index: 0; }\n/* Bruma baja de claro de bosque y viñeta: la luz de las luciérnagas se lee mejor sobre un fondo con aire. */\n.furin-fireflies__bruma {\n  position: absolute;\n  inset: 0;\n  z-index: 1;\n  pointer-events: none;\n  background:\n    radial-gradient(ellipse 80% 45% at 50% 105%, rgba(120, 140, 60, 0.13), transparent 70%),\n    radial-gradient(ellipse 92% 88% at 50% 45%, transparent 42%, rgba(0, 0, 0, 0.55) 118%);\n}\n.furin-fireflies__contenido { position: relative; z-index: 2; max-width: 100%; pointer-events: none; }\n.furin-fireflies__contenido > * { pointer-events: auto; }\n",
      "type": "registry:file",
      "target": "components/furin/fireflies/Fireflies.css"
    }
  ]
}
