{
  "name": "noise-texture",
  "type": "registry:ui",
  "dependencies": [],
  "registryDependencies": [],
  "description": "An animated noise/grain texture overlay effect with customizable grain size and blend modes.",
  "files": [
    {
      "path": "components/ui/noise-texture.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef } from \"react\"\nimport { cn } from \"@/lib/utils\"\n\ninterface NoiseTextureProps {\n  className?: string\n  opacity?: number\n  speed?: number\n  grain?: \"fine\" | \"medium\" | \"coarse\"\n  blend?: \"overlay\" | \"soft-light\" | \"multiply\" | \"screen\" | \"normal\"\n  animate?: boolean\n}\n\nexport function NoiseTexture({\n  className,\n  opacity = 0.4,\n  speed = 10,\n  grain = \"medium\",\n  blend = \"normal\",\n  animate = true,\n}: NoiseTextureProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const animationRef = useRef<number | NodeJS.Timeout>(0)\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n    if (!canvas) return\n\n    const ctx = canvas.getContext(\"2d\")\n    if (!ctx) return\n\n    const grainSizes: Record<string, number> = {\n      fine: 1,\n      medium: 2,\n      coarse: 3,\n    }\n    const grainSize = grainSizes[grain] ?? 2\n\n    const resize = () => {\n      const rect = canvas.getBoundingClientRect()\n      const dpr = Math.min(window.devicePixelRatio || 1, 2)\n      canvas.width = Math.ceil(rect.width / grainSize) * dpr\n      canvas.height = Math.ceil(rect.height / grainSize) * dpr\n      canvas.style.width = `${rect.width}px`\n      canvas.style.height = `${rect.height}px`\n    }\n\n    resize()\n    window.addEventListener(\"resize\", resize)\n\n    const renderNoise = () => {\n      const { width, height } = canvas\n      if (width === 0 || height === 0) return\n      \n      const imageData = ctx.createImageData(width, height)\n      const data = imageData.data\n\n      for (let i = 0; i < data.length; i += 4) {\n        const value = Math.random() * 255\n        data[i] = value\n        data[i + 1] = value\n        data[i + 2] = value\n        data[i + 3] = 255\n      }\n\n      ctx.putImageData(imageData, 0, 0)\n    }\n\n    renderNoise()\n\n    if (animate) {\n      const animateNoise = () => {\n        renderNoise()\n        animationRef.current = setTimeout(() => {\n          requestAnimationFrame(animateNoise)\n        }, 1000 / speed)\n      }\n      animateNoise()\n    }\n\n    return () => {\n      window.removeEventListener(\"resize\", resize)\n      if (animationRef.current) {\n        clearTimeout(animationRef.current as NodeJS.Timeout)\n      }\n    }\n  }, [grain, speed, animate])\n\n  const grainSizes: Record<string, number> = {\n    fine: 1,\n    medium: 2,\n    coarse: 3,\n  }\n\n  return (\n    <canvas\n      ref={canvasRef}\n      className={cn(\"pointer-events-none absolute inset-0\", className)}\n      style={{\n        opacity,\n        mixBlendMode: blend,\n        imageRendering: \"pixelated\",\n        width: \"100%\",\n        height: \"100%\",\n        transform: `scale(${grainSizes[grain] ?? 2})`,\n        transformOrigin: \"top left\",\n      }}\n    />\n  )\n}\n",
      "type": "registry:ui"
    }
  ]
}
