{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "noise-background",
  "title": "Noise Background",
  "description": "Drifting brand-gradient blobs under a static film-grain overlay; wraps its content.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/noise-background.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface NoiseBackgroundProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Film-grain opacity, 0–1 (0 hides the grain) */\n  grain?: number;\n  /** Drift speed multiplier */\n  speed?: number;\n  /** Pause the drift while hovered (focus inside always pauses it) */\n  pauseOnHover?: boolean;\n}\n\n// Static grain: SVG fractal noise, desaturated, tiled as a background image.\nconst NOISE = `url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")`;\n\nconst BLOBS = [\n  \"-top-1/2 -left-[10%] bg-brand-from\",\n  \"-top-1/4 -right-[15%] bg-brand-via\",\n  \"-bottom-2/3 left-1/4 bg-brand-to\",\n];\n\n/**\n * A soft brand-gradient wash — three blurred blobs drifting slowly — under a\n * static film-grain overlay. Wraps its children, which sit above both layers.\n */\nexport function NoiseBackground({\n  grain = 0.35,\n  speed = 1,\n  pauseOnHover = true,\n  className,\n  children,\n  ...props\n}: NoiseBackgroundProps) {\n  const ref = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const root = ref.current!;\n    const blobs = [...root.firstElementChild!.children] as HTMLElement[];\n    const mq = matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let t = 0;\n    let last = 0;\n    let raf = 0;\n    let seen = false;\n    let hover = false;\n    let focus = false;\n\n    const frame = (now: number) => {\n      t += (Math.min(now - last, 50) / 1000) * speed;\n      last = now;\n      blobs.forEach((b, i) => {\n        const p = i * 2.4;\n        const x = 22 * Math.sin(t * (0.21 + i * 0.05) + p);\n        const y = 16 * Math.cos(t * (0.17 + i * 0.04) + p);\n        b.style.transform = `translate(${x}%,${y}%) scale(${1 + 0.1 * Math.sin(t * 0.3 + p)})`;\n      });\n      raf = requestAnimationFrame(frame);\n    };\n    // Drift only on screen, in a visible tab, without reduced motion and\n    // while nobody is hovering or focused inside; otherwise hold still.\n    const sync = () => {\n      cancelAnimationFrame(raf);\n      if (!mq.matches && seen && !document.hidden && !focus && !(hover && pauseOnHover)) {\n        last = performance.now();\n        raf = requestAnimationFrame(frame);\n      }\n    };\n    const on = (e: Event) => {\n      if (e.type == \"pointerenter\") hover = true;\n      else if (e.type == \"pointerleave\") hover = false;\n      else focus = e.type == \"focusin\" || root.contains((e as FocusEvent).relatedTarget as Node);\n      sync();\n    };\n    const events = [\"pointerenter\", \"pointerleave\", \"focusin\", \"focusout\"];\n\n    const io = new IntersectionObserver(([e]) => {\n      seen = e.isIntersecting;\n      sync();\n    });\n    io.observe(root);\n    events.forEach((n) => root.addEventListener(n, on));\n    document.addEventListener(\"visibilitychange\", sync);\n    mq.addEventListener(\"change\", sync);\n    return () => {\n      cancelAnimationFrame(raf);\n      io.disconnect();\n      events.forEach((n) => root.removeEventListener(n, on));\n      document.removeEventListener(\"visibilitychange\", sync);\n      mq.removeEventListener(\"change\", sync);\n    };\n  }, [speed, pauseOnHover]);\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"noise-background\"\n      className={cn(\"relative isolate overflow-hidden bg-background\", className)}\n      {...props}\n    >\n      <div aria-hidden className=\"pointer-events-none absolute inset-0 -z-10 opacity-60 dark:opacity-50\">\n        {BLOBS.map((c) => (\n          <div\n            key={c}\n            className={cn(\"absolute aspect-square w-[65%] min-w-64 rounded-full blur-3xl will-change-transform\", c)}\n          />\n        ))}\n      </div>\n      <div\n        aria-hidden\n        className=\"pointer-events-none absolute inset-0 -z-10 mix-blend-overlay\"\n        style={{ backgroundImage: NOISE, opacity: grain }}\n      />\n      {children}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/noise-background.tsx"
    }
  ],
  "type": "registry:ui"
}