{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "wavy-background",
  "title": "Wavy Background",
  "description": "Blurred, translucent ribbons of brand colour flowing sideways on a canvas.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/wavy-background.tsx",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype Draw = (t: number, dt: number, still: boolean) => void;\ntype Build = (ctx: CanvasRenderingContext2D, w: number, h: number, el: HTMLCanvasElement) => Draw;\n\n// Resolves any CSS colour (or a \"--custom-property\") to \"r, g, b\" through a 1×1\n// canvas, so oklch() theme tokens work in gradients on every browser.\nfunction rgb(el: Element, color: string) {\n  const c = document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true })!;\n  c.fillStyle = color.startsWith(\"--\") ? getComputedStyle(el).getPropertyValue(color) : color;\n  c.fillRect(0, 0, 1, 1);\n  const [r, g, b] = c.getImageData(0, 0, 1, 1).data;\n  return `${r}, ${g}, ${b}`;\n}\n\n// Runs `build` on resize and theme change; loops only while on screen, the tab\n// is visible and motion is allowed — otherwise paints a single still frame.\nfunction useCanvasLoop(ref: React.RefObject<HTMLCanvasElement | null>, build: Build) {\n  useEffect(() => {\n    const el = ref.current;\n    const ctx = el?.getContext(\"2d\");\n    if (!el || !ctx) return;\n    const mq = matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let draw: Draw | undefined;\n    let raf = 0;\n    let t = 0;\n    let last = 0;\n    let seen = false;\n    const frame = (now: number) => {\n      const dt = Math.max(0, Math.min(now - last, 50));\n      last = now;\n      t += dt;\n      draw?.(t, dt, false);\n      raf = requestAnimationFrame(frame);\n    };\n    const sync = () => {\n      cancelAnimationFrame(raf);\n      if (!draw) return;\n      if (mq.matches) draw(t, 0, true);\n      else if (seen && !document.hidden) {\n        last = performance.now();\n        raf = requestAnimationFrame(frame);\n      }\n    };\n    const setup = () => {\n      const dpr = Math.min(devicePixelRatio || 1, 2);\n      const w = el.clientWidth;\n      const h = el.clientHeight;\n      el.width = w * dpr;\n      el.height = h * dpr;\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      draw = w && h ? build(ctx, w, h, el) : undefined;\n      sync();\n    };\n    const ro = new ResizeObserver(setup);\n    const io = new IntersectionObserver(([e]) => {\n      seen = e.isIntersecting;\n      sync();\n    });\n    const mo = new MutationObserver(setup);\n    ro.observe(el);\n    io.observe(el);\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\", \"style\"] });\n    mo.observe(document.head, { childList: true, subtree: true, characterData: true });\n    document.addEventListener(\"visibilitychange\", sync);\n    mq.addEventListener(\"change\", sync);\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      mo.disconnect();\n      document.removeEventListener(\"visibilitychange\", sync);\n      mq.removeEventListener(\"change\", sync);\n    };\n  }, [ref, build]);\n}\n\n// Stable pseudo-random 0–1 per wave, so every wave keeps its shape across resizes.\nconst seed = (i: number, k: number) => {\n  const s = Math.sin(i * 12.9898 + k * 78.233) * 43758.5453;\n  return s - Math.floor(s);\n};\n\ninterface WavyBackgroundProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Number of layered waves */\n  waves?: number;\n  /** How quickly the waves drift */\n  speed?: \"slow\" | \"fast\";\n  /** Blur radius in px applied to the wave layer */\n  blur?: number;\n  /** Any CSS colours or custom property names (e.g. \"--brand-from\"); cycled across the waves */\n  colors?: string[];\n  /** Opacity of each wave, 0–1 */\n  opacity?: number;\n}\n\n/**\n * Layered, blurred ribbons of colour flowing sideways on a canvas. Absolutely\n * positioned — place inside a `relative overflow-hidden` section and layer\n * content above it. Defaults to the brand ramp and follows theme changes;\n * ribbons blend additively when the inherited text colour is light.\n */\nexport function WavyBackground({\n  waves = 5,\n  speed = \"slow\",\n  blur = 16,\n  colors = [\"--brand-from\", \"--brand-via\", \"--brand-to\"],\n  opacity = 0.5,\n  className,\n  ...props\n}: WavyBackgroundProps) {\n  const ref = useRef<HTMLCanvasElement>(null);\n  const palette = colors.join(\"|\");\n\n  const build = useCallback<Build>(\n    (ctx, w, h, el) => {\n      const fills = palette.split(\"|\").map((c) => rgb(el, c));\n      const rate = speed === \"fast\" ? 0.0009 : 0.0003;\n      const step = 12;\n      // Light text means a dark section: let overlapping ribbons add up to a glow.\n      const [r, g, b] = rgb(el, getComputedStyle(el).color).split(\", \").map(Number);\n      ctx.globalCompositeOperation = r * 0.3 + g * 0.59 + b * 0.11 > 128 ? \"lighter\" : \"source-over\";\n      const tau = Math.PI * 2;\n      return (t) => {\n        ctx.clearRect(0, 0, w, h);\n        ctx.globalAlpha = opacity;\n        for (let i = 0; i < waves; i++) {\n          const f1 = tau * (0.5 + seed(i, 1));\n          const f2 = tau * (1.4 + seed(i, 2) * 1.6);\n          const p = seed(i, 3) * tau;\n          const s = rate * (0.6 + seed(i, 4) * 0.8) * (i % 2 ? -1 : 1);\n          const amp = h * (0.1 + seed(i, 5) * 0.08);\n          const base = h * (0.52 + (i - (waves - 1) / 2) * 0.07);\n          const top: number[] = [];\n          ctx.beginPath();\n          for (let x = 0; x <= w + step; x += step) {\n            const u = x / w;\n            const y =\n              base +\n              amp * (0.7 * Math.sin(u * f1 + t * s + p) + 0.3 * Math.sin(u * f2 - t * s * 1.7 + p * 2));\n            top.push(y);\n            ctx.lineTo(x, y);\n          }\n          // Walk back along the lower edge; the ribbon swells and thins as it goes.\n          for (let j = top.length - 1; j >= 0; j--) {\n            const u = (j * step) / w;\n            ctx.lineTo(j * step, top[j] + amp * (0.45 + 0.35 * Math.sin(u * f1 * 1.3 - t * s + p)));\n          }\n          const g = ctx.createLinearGradient(0, 0, w, 0);\n          g.addColorStop(0, `rgb(${fills[i % fills.length]})`);\n          g.addColorStop(1, `rgb(${fills[(i + 1) % fills.length]})`);\n          ctx.fillStyle = g;\n          ctx.fill();\n        }\n      };\n    },\n    [waves, speed, opacity, palette]\n  );\n  useCanvasLoop(ref, build);\n\n  return (\n    <div\n      aria-hidden\n      data-slot=\"wavy-background\"\n      className={cn(\"pointer-events-none absolute inset-0 overflow-hidden\", className)}\n      {...props}\n    >\n      {/* Oversized so the blur never shows a soft edge at the sides */}\n      <canvas\n        ref={ref}\n        className=\"absolute\"\n        style={{\n          inset: -blur * 2,\n          width: `calc(100% + ${blur * 4}px)`,\n          height: `calc(100% + ${blur * 4}px)`,\n          filter: `blur(${blur}px)`,\n        }}\n      />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/wavy-background.tsx"
    }
  ],
  "type": "registry:ui"
}