{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "grain-overlay",
  "title": "Grain Overlay",
  "description": "Film grain for any panel, which also hides the banding in a wide gradient.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/grain-overlay/grain-overlay.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\nexport type GrainOverlayProps = React.HTMLAttributes<HTMLDivElement> & {\n  /** Higher is finer. Around 0.65 reads as film, 0.2 as coarse paper. */\n  frequency?: number\n  opacity?: number\n  blend?: \"overlay\" | \"soft-light\" | \"multiply\" | \"screen\" | \"normal\"\n  /** Shifts the grain a few times a second the way projected film does. */\n  animated?: boolean\n}\n\nfunction noiseUrl(frequency: number, seed: number) {\n  const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"160\" height=\"160\"><filter id=\"n\"><feTurbulence type=\"fractalNoise\" baseFrequency=\"${frequency}\" numOctaves=\"3\" seed=\"${seed}\" stitchTiles=\"stitch\"/><feColorMatrix type=\"saturate\" values=\"0\"/></filter><rect width=\"160\" height=\"160\" filter=\"url(#n)\"/></svg>`\n\n  return `url(\"data:image/svg+xml,${encodeURIComponent(svg)}\")`\n}\n\n/**\n * A film grain layer for any positioned box. It sits above the content and\n * ignores the pointer, so it can be dropped into a card or a hero without\n * changing anything underneath it.\n */\nexport function GrainOverlay({\n  frequency = 0.65,\n  opacity = 0.22,\n  blend = \"overlay\",\n  animated = false,\n  className,\n  style,\n  ...rootProps\n}: GrainOverlayProps) {\n  const frames = React.useMemo(\n    () => [2, 7, 13, 23].map((seed) => noiseUrl(frequency, seed)),\n    [frequency]\n  )\n\n  const [index, setIndex] = React.useState(0)\n\n  React.useEffect(() => {\n    if (!animated) return\n    if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return\n\n    const timer = window.setInterval(\n      () => setIndex((current) => (current + 1) % frames.length),\n      120\n    )\n\n    return () => window.clearInterval(timer)\n  }, [animated, frames.length])\n\n  return (\n    <div\n      data-slot=\"grain-overlay\"\n      aria-hidden=\"true\"\n      className={cn(\n        \"pointer-events-none absolute inset-0 bg-repeat\",\n        className\n      )}\n      style={{\n        backgroundImage: frames[index],\n        mixBlendMode: blend,\n        opacity,\n        ...style,\n      }}\n      {...rootProps}\n    />\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "scene",
    "texture",
    "overlay"
  ],
  "type": "registry:ui"
}