{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spectrum-bars",
  "title": "Spectrum Bars",
  "description": "A signal background of smoothly sliding color stripes that fill the block and fade into your page background, with four themes.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/clover-ui/spectrum-bars.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type SpectrumBarsTheme = \"signal\" | \"neon\" | \"sunset\" | \"mono\";\n\nexport const SPECTRUM_BARS_THEMES: Record<SpectrumBarsTheme, string[]> = {\n  signal: [\"#16d872\", \"#1ff0c8\", \"#22d8ff\", \"#3a5bff\", \"#ff2f8e\", \"#ff1e3c\", \"#ff6a00\", \"#ffd400\"],\n  neon: [\"#6a2bff\", \"#b14dff\", \"#ff4fd8\", \"#4df0ff\", \"#00ffa3\", \"#c6ff3d\", \"#fff04d\"],\n  sunset: [\"#5a189a\", \"#9d4edd\", \"#f72585\", \"#ff5e1a\", \"#ff9f1c\", \"#ffd23f\", \"#fff1e0\"],\n  mono: [\"#3a3a3a\", \"#6f6f6f\", \"#a3a3a3\", \"#d6d6d6\", \"#ffffff\"],\n};\n\nexport interface SpectrumBarsProps {\n  theme?: SpectrumBarsTheme;\n  colors?: string[];\n  bandWidth?: number;\n  bandHeight?: number;\n  columns?: number;\n  glow?: number;\n  glitch?: number;\n  speed?: number;\n  animate?: boolean;\n  backgroundColor?: string;\n  radius?: string;\n  style?: React.CSSProperties;\n  className?: string;\n}\n\ntype RGB = [number, number, number];\n\nfunction parseColor(input: string): RGB {\n  const s = input.trim();\n  if (s.startsWith(\"#\")) {\n    let c = s.slice(1);\n    if (c.length === 3 || c.length === 4)\n      c = c\n        .split(\"\")\n        .map((x) => x + x)\n        .join(\"\");\n    if (c.length >= 6) {\n      return [\n        parseInt(c.slice(0, 2), 16),\n        parseInt(c.slice(2, 4), 16),\n        parseInt(c.slice(4, 6), 16),\n      ];\n    }\n  }\n  const m = s.match(/rgba?\\(([^)]+)\\)/i);\n  if (m) {\n    const [r, g, b] = m[1].split(\",\").map((x) => parseFloat(x));\n    return [r, g, b];\n  }\n  return [0, 0, 0];\n}\n\nfunction hash(x: number, y: number) {\n  const h = Math.sin(x * 127.1 + y * 311.7) * 43758.5453;\n  return h - Math.floor(h);\n}\n\nfunction noise(x: number, y: number) {\n  const ix = Math.floor(x);\n  const iy = Math.floor(y);\n  const fx = x - ix;\n  const fy = y - iy;\n  const ux = fx * fx * (3 - 2 * fx);\n  const uy = fy * fy * (3 - 2 * fy);\n  const a = hash(ix, iy);\n  const b = hash(ix + 1, iy);\n  const c = hash(ix, iy + 1);\n  const d = hash(ix + 1, iy + 1);\n  return a + (b - a) * ux + (c - a) * uy + (a - b - c + d) * ux * uy;\n}\n\nfunction sample(palette: RGB[], t: number): RGB {\n  const last = palette.length - 1;\n  if (last <= 0) return palette[0];\n  const p = Math.min(Math.max(t, 0), 1) * last;\n  const i = Math.min(Math.floor(p), last - 1);\n  const f = p - i;\n  const a = palette[i];\n  const b = palette[i + 1];\n  return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f, a[2] + (b[2] - a[2]) * f];\n}\n\nconst smooth = (edge0: number, edge1: number, x: number) => {\n  const t = Math.min(1, Math.max(0, (x - edge0) / (edge1 - edge0)));\n  return t * t * (3 - 2 * t);\n};\n\ntype Live = {\n  palette: RGB[];\n  bandWidth: number;\n  bandHeight: number;\n  columns: number;\n  glow: number;\n  glitch: number;\n  speed: number;\n  animate: boolean;\n};\n\nfunction SpectrumBars({\n  theme = \"signal\",\n  colors,\n  bandWidth = 0.86,\n  bandHeight = 1,\n  columns = 112,\n  glow = 2,\n  glitch = 0.5,\n  speed = 3.6,\n  animate = true,\n  backgroundColor,\n  radius,\n  style,\n  className,\n}: SpectrumBarsProps) {\n  const hostRef = React.useRef<HTMLDivElement>(null);\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const syncRef = React.useRef<(() => void) | null>(null);\n  const key = (colors ?? SPECTRUM_BARS_THEMES[theme] ?? SPECTRUM_BARS_THEMES.signal).join(\",\");\n\n  const palette = React.useMemo(() => (key ? key.split(\",\") : [\"#000000\"]).map(parseColor), [key]);\n\n  const live = React.useRef<Live>({\n    palette,\n    bandWidth,\n    bandHeight,\n    columns,\n    glow,\n    glitch,\n    speed,\n    animate,\n  });\n\n  React.useEffect(() => {\n    live.current = { palette, bandWidth, bandHeight, columns, glow, glitch, speed, animate };\n    syncRef.current?.();\n  }, [palette, bandWidth, bandHeight, columns, glow, glitch, speed, animate]);\n\n  React.useEffect(() => {\n    const host = hostRef.current;\n    const canvas = canvasRef.current;\n    const ctx = canvas?.getContext(\"2d\");\n    if (!host || !canvas || !ctx) return;\n\n    const strip = document.createElement(\"canvas\");\n    const stripCtx = strip.getContext(\"2d\");\n    if (!stripCtx) return;\n\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    let raf = 0;\n    let visible = true;\n    let last = 0;\n    let time = Math.random() * 100;\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 1.5);\n      const w = Math.max(1, Math.round(host.clientWidth * dpr));\n      const h = Math.max(1, Math.round(host.clientHeight * dpr));\n      if (canvas.width !== w || canvas.height !== h) {\n        canvas.width = w;\n        canvas.height = h;\n      }\n    };\n\n    const draw = () => {\n      const L = live.current;\n      const W = canvas.width;\n      const H = canvas.height;\n      const fill = L.bandWidth >= 1;\n      const bw = Math.max(1, Math.round(W * Math.min(1, Math.max(0.05, L.bandWidth))));\n      const bh = H * Math.min(1, Math.max(0.05, L.bandHeight));\n      if (strip.width !== bw || strip.height !== 1) {\n        strip.width = bw;\n        strip.height = 1;\n      }\n      const count = Math.max(4, Math.min(256, L.columns));\n      const stripe = bw / count;\n      const sway = (noise(time * 0.35, 7.3) - 0.5) * 0.08 * L.glitch * bw;\n      const slide = time * 0.6 * stripe + (fill ? sway : 0);\n      const image = stripCtx.createImageData(bw, 1);\n      for (let px = 0; px < bw; px++) {\n        const c = Math.floor((px + slide) / stripe);\n        const u = c / count;\n        const v = Math.min(\n          1,\n          Math.max(\n            0,\n            (0.62 * noise(u * 3.2, time * 0.12) + 0.38 * noise(u * 11, time * 0.2) - 0.15) / 0.72,\n          ),\n        );\n        let light = 0.3 + 0.7 * smooth(0.14, 0.3, noise(u * 23, time * 0.25));\n        if (!fill) light *= smooth(0, 0.3, Math.min(px, bw - 1 - px) / bw);\n        const color = sample(L.palette, v);\n        const white = smooth(0.86, 1, v) * 0.6;\n        const o = px * 4;\n        image.data[o] = color[0] + (255 - color[0]) * white;\n        image.data[o + 1] = color[1] + (255 - color[1]) * white;\n        image.data[o + 2] = color[2] + (255 - color[2]) * white;\n        image.data[o + 3] = light * 255;\n      }\n      stripCtx.putImageData(image, 0, 0);\n      const x0 = (W - bw) / 2 + (fill ? 0 : sway);\n      const y0 = (H - bh) / 2;\n      ctx.globalCompositeOperation = \"source-over\";\n      ctx.globalAlpha = 1;\n      ctx.filter = \"none\";\n      ctx.imageSmoothingEnabled = false;\n      ctx.clearRect(0, 0, W, H);\n      ctx.drawImage(strip, 0, 0, bw, 1, x0, y0, bw, bh);\n      if (L.glow > 0) {\n        ctx.globalCompositeOperation = \"lighter\";\n        ctx.globalAlpha = Math.min(1, 0.45 * L.glow);\n        const spread = Math.max(1, W * 0.01 * L.glow);\n        const padX = fill ? spread * 3 : 0;\n        const padY = bh >= H ? spread * 3 : 0;\n        ctx.filter = `blur(${spread}px)`;\n        ctx.imageSmoothingEnabled = true;\n        ctx.drawImage(strip, 0, 0, bw, 1, x0 - padX, y0 - padY, bw + padX * 2, bh + padY * 2);\n        ctx.filter = \"none\";\n        ctx.globalAlpha = 1;\n        ctx.globalCompositeOperation = \"source-over\";\n      }\n    };\n\n    const tick = (now: number) => {\n      raf = 0;\n      const L = live.current;\n      const moving = L.animate && L.speed !== 0 && !reduced;\n      const dt = last ? Math.min((now - last) / 1000, 0.1) : 0;\n      last = now;\n      if (moving) time += dt * L.speed;\n      draw();\n      if (moving && visible && !document.hidden) raf = requestAnimationFrame(tick);\n    };\n\n    const schedule = () => {\n      if (raf) return;\n      last = 0;\n      raf = requestAnimationFrame(tick);\n    };\n\n    syncRef.current = () => {\n      resize();\n      schedule();\n    };\n\n    resize();\n    schedule();\n\n    const ro = new ResizeObserver(() => syncRef.current?.());\n    ro.observe(host);\n\n    const io = new IntersectionObserver(([entry]) => {\n      visible = entry.isIntersecting;\n      if (visible) schedule();\n    });\n    io.observe(host);\n\n    const onVisibility = () => {\n      if (!document.hidden) schedule();\n    };\n    document.addEventListener(\"visibilitychange\", onVisibility);\n\n    return () => {\n      if (raf) cancelAnimationFrame(raf);\n      syncRef.current = null;\n      ro.disconnect();\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", onVisibility);\n    };\n  }, []);\n\n  return (\n    <div\n      ref={hostRef}\n      aria-hidden=\"true\"\n      className={cn(\n        \"bg-background pointer-events-none absolute inset-0 overflow-hidden\",\n        className,\n      )}\n      style={{ borderRadius: radius, backgroundColor, ...style }}\n    >\n      <canvas ref={canvasRef} className=\"block h-full w-full\" />\n    </div>\n  );\n}\n\nSpectrumBars.displayName = \"SpectrumBars\";\n\nexport { SpectrumBars };\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "addedAt": "2026-10-18"
  },
  "type": "registry:component"
}