{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "wavy-background",
  "type": "registry:component",
  "title": "Wavy background",
  "description": "Layered canvas waves that roll across the bottom of the panel behind your content, each at its own pace.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://ui.minidev.pro/r/button.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/premium/wavy-background.tsx",
      "type": "registry:ui",
      "target": "components/ui/wavy-background.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { useReducedMotion } from \"motion/react\"\nimport { ArrowRight } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\n\n/**\n * Layered canvas waves that roll across the bottom of the panel behind your content, each at its own pace.\n * Every wave is two sine harmonics with its own phase, so the layers breathe\n * against each other instead of marching in step. Colours resolve through\n * the cascade, so var(--accent) follows theme and material changes. The\n * canvas is sharp on high density screens, resizes with its box, sleeps off\n * screen and in background tabs, and draws one still frame for reduced motion.\n */\ntype WavyBackgroundProps = {\n  /** Colours of the waves, back to front. Any CSS colour, including var(--token). */\n  colors?: string[]\n  /** Speed multiplier. 2 is twice as fast, 0.5 half as fast. */\n  speed?: number\n  /** Height of the swell in pixels. */\n  amplitude?: number\n  /** Softness in pixels. 0 draws crisp edges. */\n  blur?: number\n  /** Share of the panel the waves rise to, from the bottom, 0 to 1. */\n  coverage?: number\n  /** Opacity of each wave, 0 to 1. */\n  opacity?: number\n  className?: string\n  children?: React.ReactNode\n}\n\n/** Per-wave character: wavelengths, phase speeds and offsets, chosen so no two waves line up. */\nconst SHAPES = [\n  { l1: 0.92, l2: 0.41, s1: 0.42, s2: -0.27, p1: 0.0, p2: 1.9 },\n  { l1: 0.78, l2: 0.36, s1: -0.35, s2: 0.31, p1: 2.1, p2: 0.4 },\n  { l1: 1.08, l2: 0.47, s1: 0.29, s2: -0.38, p1: 4.2, p2: 2.8 },\n  { l1: 0.66, l2: 0.3, s1: -0.47, s2: 0.22, p1: 1.2, p2: 5.1 },\n  { l1: 0.86, l2: 0.52, s1: 0.33, s2: 0.44, p1: 3.3, p2: 1.1 },\n]\n\nfunction WavyBackground({\n  colors = [\"var(--chart-2)\", \"var(--info)\", \"var(--accent-2)\", \"var(--accent)\"],\n  speed = 1,\n  amplitude = 26,\n  blur = 8,\n  coverage = 0.46,\n  opacity = 0.46,\n  className,\n  children,\n}: WavyBackgroundProps) {\n  const reduce = useReducedMotion()\n  const boxRef = React.useRef<HTMLDivElement>(null)\n  const canvasRef = React.useRef<HTMLCanvasElement>(null)\n  const pad = Math.max(0, Math.round(blur * 2))\n  // A string key, so an inline colour array does not restart the waves on every render.\n  const colorKey = (colors.length ? colors : [\"var(--accent)\"]).join(\"|\")\n\n  React.useEffect(() => {\n    const box = boxRef.current\n    const canvas = canvasRef.current\n    const ctx = canvas?.getContext(\"2d\")\n    if (!box || !canvas || !ctx) return\n\n    const list = colorKey.split(\"|\")\n    let w = 0\n    let h = 0\n    let dpr = 1\n    let raf = 0\n    let last = 0\n    let clock = 0\n    let colorAt = 0\n    let visible = true\n    let fills: string[] = []\n\n    // Resolve each colour through the cascade, so tokens follow theme and material.\n    // No transition on the canvas colour, or a reduced-motion stylesheet can hand back a mid-transition value.\n    canvas.style.setProperty(\"transition\", \"none\", \"important\")\n    const readColors = () => {\n      fills = list.map((c) => {\n        canvas.style.color = c\n        return getComputedStyle(canvas).color || \"currentColor\"\n      })\n    }\n\n    const resize = () => {\n      const r = box.getBoundingClientRect()\n      w = Math.max(1, Math.round(r.width)) + pad * 2\n      h = Math.max(1, Math.round(r.height)) + pad * 2\n      dpr = Math.min(window.devicePixelRatio || 1, 2)\n      canvas.width = Math.round(w * dpr)\n      canvas.height = Math.round(h * dpr)\n    }\n\n    const draw = (t: number) => {\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n      ctx.clearRect(0, 0, w, h)\n      const n = fills.length\n      const inner = h - pad * 2\n      const cover = Math.min(1, Math.max(0.05, coverage))\n      const top = pad + inner * (1 - cover)\n      const bottom = pad + inner * (1 - cover * 0.42)\n      const amp = Math.max(0, amplitude)\n      const step = 3\n      for (let i = 0; i < n; i++) {\n        const s = SHAPES[i % SHAPES.length]\n        const base = n === 1 ? top : top + ((bottom - top) * i) / (n - 1)\n        const a = amp * (0.7 + (0.3 * i) / Math.max(1, n - 1))\n        const k1 = (Math.PI * 2) / (w * s.l1)\n        const k2 = (Math.PI * 2) / (w * s.l2)\n        ctx.beginPath()\n        ctx.moveTo(-step, h + step)\n        for (let x = -step; x <= w + step; x += step) {\n          const y = base + a * (0.64 * Math.sin(x * k1 + t * s.s1 + s.p1) + 0.36 * Math.sin(x * k2 + t * s.s2 + s.p2))\n          ctx.lineTo(x, y)\n        }\n        ctx.lineTo(w + step, h + step)\n        ctx.closePath()\n        ctx.globalAlpha = Math.min(1, Math.max(0, opacity))\n        ctx.fillStyle = fills[i]\n        ctx.fill()\n      }\n      ctx.globalAlpha = 1\n    }\n\n    const loop = (t: number) => {\n      const dt = last ? Math.min(0.05, (t - last) / 1000) : 0\n      last = t\n      clock += dt * Math.max(0, speed)\n      if (t - colorAt > 800) {\n        colorAt = t\n        readColors()\n      }\n      draw(clock)\n      raf = requestAnimationFrame(loop)\n    }\n    const stop = () => {\n      cancelAnimationFrame(raf)\n      raf = 0\n      last = 0\n    }\n    const sync = () => {\n      if (reduce) {\n        stop()\n        draw(0)\n        return\n      }\n      const run = visible && !document.hidden\n      if (run && !raf) raf = requestAnimationFrame(loop)\n      if (!run && raf) stop()\n    }\n\n    readColors()\n    resize()\n    const ro = new ResizeObserver(() => {\n      resize()\n      draw(clock)\n    })\n    const io = new IntersectionObserver(([e]) => {\n      visible = e.isIntersecting\n      sync()\n    })\n    // Theme and material can change on any ancestor (a docs stage, a section), so watch the whole tree, one read per frame.\n    let pending = 0\n    const mo = new MutationObserver(() => {\n      if (pending) return\n      pending = requestAnimationFrame(() => {\n        pending = 0\n        readColors()\n        if (!raf) draw(clock)\n      })\n    })\n    ro.observe(box)\n    io.observe(box)\n    mo.observe(document.documentElement, { attributes: true, subtree: true, attributeFilter: [\"class\", \"data-material\", \"data-theme\"] })\n    document.addEventListener(\"visibilitychange\", sync)\n    sync()\n\n    return () => {\n      stop()\n      cancelAnimationFrame(pending)\n      ro.disconnect()\n      io.disconnect()\n      mo.disconnect()\n      document.removeEventListener(\"visibilitychange\", sync)\n    }\n  }, [colorKey, speed, amplitude, coverage, opacity, pad, reduce])\n\n  return (\n    <div\n      ref={boxRef}\n      data-slot=\"wavy-background\"\n      className={cn(\"relative isolate h-[360px] w-full overflow-hidden rounded-2xl border border-border bg-bg\", className)}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden\n        data-slot=\"wavy-background-canvas\"\n        className=\"pointer-events-none absolute z-0\"\n        style={{\n          top: -pad,\n          left: -pad,\n          width: `calc(100% + ${pad * 2}px)`,\n          height: `calc(100% + ${pad * 2}px)`,\n          filter: blur > 0 ? `blur(${blur}px)` : undefined,\n        }}\n      />\n      <div className=\"relative z-10 h-full\">\n        {children ?? (\n          <div className=\"flex h-full flex-col items-center gap-4 px-6 pt-10 text-center\">\n            <span className=\"rounded-full border border-accent-line bg-accent-soft px-2.5 py-0.5 text-xs font-medium text-accent-fg\">\n              Lumen Cash Flow\n            </span>\n            <p className=\"max-w-[18ch] text-3xl font-medium tracking-[-0.035em] text-balance text-fg\">\n              Ride the month, not the spreadsheet\n            </p>\n            <Button>\n              Forecast my runway\n              <ArrowRight />\n            </Button>\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport { WavyBackground }\nexport type { WavyBackgroundProps }\n"
    }
  ]
}
