{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"bars-wave","type":"registry:ui","title":"Bars Wave","description":"A row of glowing equalizer bars whose heights roll across the screen as a layered wave. Canvas 2D.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/bars-wave.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface BarsWaveProps {\n  /** Color at the tops of the bars. */\n  color?: string;\n  /** Color at the base of the bars. */\n  accent?: string;\n  /** Number of bars across the width. */\n  bars?: number;\n  /** Wave speed multiplier; 0 freezes the wave. */\n  speed?: number;\n  /** Wave height, 0..1 of the container. */\n  amplitude?: number;\n  /** Glow strength. */\n  glow?: number;\n  className?: string;\n}\n\nexport function BarsWave({\n  color = \"#67e8f9\",\n  accent = \"#7c3aed\",\n  bars = 64,\n  speed = 1,\n  amplitude = 0.6,\n  glow = 1,\n  className,\n}: BarsWaveProps) {\n  const host = useRef<HTMLDivElement>(null);\n  const opts = useRef({ color, accent, bars, speed, amplitude, glow });\n  const redraw = useRef<(() => void) | null>(null);\n\n  useEffect(() => {\n    opts.current = { color, accent, bars, speed, amplitude, glow };\n    redraw.current?.();\n  }, [color, accent, bars, speed, amplitude, glow]);\n\n  useEffect(() => {\n    const el = host.current!;\n    const canvas = el.querySelector(\"canvas\")!;\n    const ctx = canvas.getContext(\"2d\");\n    // Low-res copy of the bars; scaled up it becomes a cheap soft glow.\n    const haze = document.createElement(\"canvas\");\n    const hctx = haze.getContext(\"2d\");\n    if (!ctx || !hctx) return;\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const HAZE = 10;\n\n    let raf = 0;\n    let visible = true;\n    let last = 0;\n    let time = 3;\n    let jitter = new Float32Array(0);\n    let jitterTarget = new Float32Array(0);\n\n    const heights = (n: number, t: number) => {\n      if (jitter.length !== n) {\n        jitter = new Float32Array(n);\n        jitterTarget = new Float32Array(n);\n      }\n      const out = new Float32Array(n);\n      for (let i = 0; i < n; i++) {\n        const x = i / n;\n        // Two rolling swells plus a slow breathing term, then per-bar equalizer jitter.\n        const a = Math.sin(x * 9 - t * 1.6);\n        const b = Math.sin(x * 4.3 - t * 0.9 + 1.7);\n        const c = Math.sin(x * 17 - t * 2.7) * 0.35;\n        const v = 0.5 + 0.28 * a + 0.2 * b + 0.12 * c;\n        out[i] = Math.min(1, Math.max(0.03, v * (0.82 + jitter[i] * 0.3)));\n      }\n      return out;\n    };\n\n    const bar = (c: CanvasRenderingContext2D, x: number, y: number, w: number, h: number) => {\n      const r = Math.min(w / 2, h);\n      c.beginPath();\n      c.moveTo(x, y + h);\n      c.lineTo(x, y + r);\n      c.arc(x + w / 2, y + r, w / 2, Math.PI, 0);\n      c.lineTo(x + w, y + h);\n      c.closePath();\n    };\n\n    const draw = () => {\n      const o = opts.current;\n      const W = canvas.width;\n      const H = canvas.height;\n      const n = Math.max(4, Math.round(o.bars));\n      const hs = heights(n, time);\n      const pitch = W / n;\n      const bw = Math.max(1, pitch * 0.58);\n      const floor = H * 0.92;\n      const maxH = floor * Math.min(Math.max(o.amplitude, 0), 1);\n\n      ctx.clearRect(0, 0, W, H);\n      const grad = ctx.createLinearGradient(0, floor - maxH, 0, floor);\n      grad.addColorStop(0, o.color);\n      grad.addColorStop(1, o.accent);\n\n      // Glow pass: draw the bars small, then stretch them back up.\n      if (o.glow > 0) {\n        const s = 1 / HAZE;\n        hctx.setTransform(1, 0, 0, 1, 0, 0);\n        hctx.clearRect(0, 0, haze.width, haze.height);\n        hctx.setTransform(s, 0, 0, s, 0, 0);\n        const hg = hctx.createLinearGradient(0, floor - maxH, 0, floor);\n        hg.addColorStop(0, o.color);\n        hg.addColorStop(1, o.accent);\n        hctx.fillStyle = hg;\n        // Blurring the small buffer is far cheaper than blurring the full canvas.\n        hctx.filter = `blur(${((pitch * 1.2) / HAZE).toFixed(2)}px)`;\n        for (let i = 0; i < n; i++) {\n          const h = hs[i] * maxH;\n          hctx.fillRect(i * pitch + (pitch - bw) / 2, floor - h, bw, h);\n        }\n        ctx.save();\n        ctx.globalCompositeOperation = \"lighter\";\n        ctx.imageSmoothingEnabled = true;\n        ctx.imageSmoothingQuality = \"high\";\n        ctx.globalAlpha = Math.min(o.glow, 2) * 0.8;\n        ctx.drawImage(haze, 0, 0, haze.width * HAZE, haze.height * HAZE);\n        ctx.restore();\n      }\n\n      // Crisp bars.\n      ctx.fillStyle = grad;\n      for (let i = 0; i < n; i++) {\n        const h = hs[i] * maxH;\n        bar(ctx, i * pitch + (pitch - bw) / 2, floor - h, bw, h);\n        ctx.fill();\n      }\n      // Bright caps.\n      ctx.fillStyle = \"rgba(255,255,255,0.85)\";\n      for (let i = 0; i < n; i++) {\n        const h = hs[i] * maxH;\n        const x = i * pitch + (pitch - bw) / 2;\n        ctx.fillRect(x + bw * 0.2, floor - h + bw * 0.25, bw * 0.6, Math.max(1, bw * 0.12));\n      }\n      // Faint mirrored reflection below the floor.\n      ctx.save();\n      ctx.globalAlpha = 0.22;\n      const rg = ctx.createLinearGradient(0, floor, 0, H);\n      rg.addColorStop(0, o.accent);\n      rg.addColorStop(1, \"transparent\");\n      ctx.fillStyle = rg;\n      for (let i = 0; i < n; i++) {\n        const h = Math.min(hs[i] * maxH * 0.4, H - floor);\n        ctx.fillRect(i * pitch + (pitch - bw) / 2, floor + 2, bw, h);\n      }\n      ctx.restore();\n    };\n\n    const loop = (now: number) => {\n      const dt = (Math.min(now - last, 50) / 1000) * opts.current.speed;\n      last = now;\n      time += dt;\n      const k = 1 - Math.exp(-dt * 6);\n      for (let i = 0; i < jitter.length; i++) {\n        if (Math.random() < dt * 3) jitterTarget[i] = Math.random() * 2 - 1;\n        jitter[i] += (jitterTarget[i] - jitter[i]) * k;\n      }\n      draw();\n      raf = requestAnimationFrame(loop);\n    };\n    const play = () => {\n      if (raf || !visible || reduced) return;\n      last = performance.now();\n      raf = requestAnimationFrame(loop);\n    };\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      canvas.width = Math.max(1, Math.round(el.clientWidth * dpr));\n      canvas.height = Math.max(1, Math.round(el.clientHeight * dpr));\n      haze.width = Math.max(1, Math.ceil(canvas.width / HAZE));\n      haze.height = Math.max(1, Math.ceil(canvas.height / HAZE));\n      draw();\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(el);\n    const io = new IntersectionObserver(([e]) => {\n      visible = e.isIntersecting;\n      if (visible) play();\n      else {\n        cancelAnimationFrame(raf);\n        raf = 0;\n      }\n    });\n    io.observe(el);\n    redraw.current = draw;\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      redraw.current = null;\n    };\n  }, []);\n\n  return (\n    <div\n      ref={host}\n      aria-hidden\n      className={className}\n      style={{ position: \"absolute\", inset: 0, overflow: \"hidden\", pointerEvents: \"none\" }}\n    >\n      <canvas style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\", display: \"block\" }} />\n    </div>\n  );\n}\n"}]}