{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"grain-gradient","type":"registry:ui","title":"Grain Gradient","description":"A slowly morphing mesh gradient of four colors under a layer of moving film grain.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/grain-gradient.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface GrainGradientProps {\n  /** Three or four mesh colors. */\n  colors?: string[];\n  /** Morph speed multiplier. */\n  speed?: number;\n  /** Film grain strength, 0-1. */\n  grain?: number;\n  /** How much the color fields swirl into each other. */\n  warp?: number;\n  className?: string;\n}\n\nconst FRAG = `\nprecision highp float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform vec3 uC0;\nuniform vec3 uC1;\nuniform vec3 uC2;\nuniform vec3 uC3;\nuniform float uGrain;\nuniform float uWarp;\nuniform float uSeed;\n\nfloat hash(vec2 p) {\n  vec3 p3 = fract(vec3(p.xyx) * 0.1031);\n  p3 += dot(p3, p3.yzx + 33.33);\n  return fract((p3.x + p3.y) * p3.z);\n}\n\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / uRes;\n  float asp = uRes.x / uRes.y;\n  vec2 p = (uv - 0.5) * vec2(asp, 1.0);\n  float t = uTime * 0.18;\n\n  vec2 q = p + uWarp * 0.22 * vec2(\n    sin(p.y * 2.1 + t * 1.3) + sin(p.y * 3.7 - t * 0.9 + p.x),\n    cos(p.x * 1.8 - t * 1.1) + sin(p.x * 3.1 + t * 0.7 - p.y));\n\n  // four color points drifting on slow Lissajous paths, blended by inverse distance\n  vec2 h = vec2(asp * 0.5, 0.5);\n  vec2 P0 = vec2(sin(t * 0.9) * 0.7, cos(t * 0.7) * 0.6) * h + vec2(-0.3, 0.2) * h;\n  vec2 P1 = vec2(cos(t * 0.6 + 1.0) * 0.7, sin(t * 0.8 + 2.0) * 0.6) * h + vec2(0.35, 0.25) * h;\n  vec2 P2 = vec2(sin(t * 0.5 + 4.0) * 0.7, cos(t * 0.9 + 1.0) * 0.6) * h + vec2(0.3, -0.3) * h;\n  vec2 P3 = vec2(cos(t * 0.8 + 3.0) * 0.7, sin(t * 0.55 + 5.0) * 0.6) * h + vec2(-0.35, -0.25) * h;\n  vec4 w = 1.0 / (vec4(dot(q - P0, q - P0), dot(q - P1, q - P1), dot(q - P2, q - P2), dot(q - P3, q - P3)) + 0.02);\n  w = w * w;\n  vec3 col = (uC0 * w.x + uC1 * w.y + uC2 * w.z + uC3 * w.w) / (w.x + w.y + w.z + w.w);\n\n  // soft light pooling and vignette\n  col *= 0.9 + 0.2 * smoothstep(0.9, 0.0, length(p - P1));\n  col *= 1.0 - 0.25 * dot(p, p);\n\n  float g = hash(gl_FragCoord.xy + uSeed * 97.0) - 0.5;\n  col += g * uGrain * 0.22 * (0.5 + 0.8 * (1.0 - dot(col, vec3(0.3333))));\n  gl_FragColor = vec4(col, 1.0);\n}\n`;\n\nconst VERT = \"attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}\";\n\ntype Ptr = { x: number; y: number; hover: number };\ntype Uni = (name: string) => WebGLUniformLocation | null;\n\n/** Full-screen shader canvas inside `host`: DPR-capped resize, offscreen pause, reduced motion, eased pointer. */\nfunction runShader(host: HTMLElement, frag: string, speed: () => number, update: (gl: WebGLRenderingContext, u: Uni) => void) {\n  const canvas = document.createElement(\"canvas\");\n  canvas.style.cssText = \"position:absolute;inset:0;width:100%;height:100%;display:block\";\n  const gl = canvas.getContext(\"webgl\", { antialias: false });\n  if (!gl) return null;\n  const prog = gl.createProgram()!;\n  for (const [type, src] of [\n    [gl.VERTEX_SHADER, VERT],\n    [gl.FRAGMENT_SHADER, frag],\n  ] as const) {\n    const sh = gl.createShader(type)!;\n    gl.shaderSource(sh, src);\n    gl.compileShader(sh);\n    if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) console.warn(gl.getShaderInfoLog(sh));\n    gl.attachShader(prog, sh);\n  }\n  gl.bindAttribLocation(prog, 0, \"p\");\n  gl.linkProgram(prog);\n  if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {\n    gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n    return null;\n  }\n  gl.useProgram(prog);\n  gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());\n  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n  gl.enableVertexAttribArray(0);\n  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);\n  host.appendChild(canvas);\n\n  const locs = new Map<string, WebGLUniformLocation | null>();\n  const u: Uni = (n) => {\n    if (!locs.has(n)) locs.set(n, gl.getUniformLocation(prog, n));\n    return locs.get(n) ?? null;\n  };\n  const zone = host.parentElement ?? host;\n  const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  const ptr: Ptr = { x: 0.5, y: 0.5, hover: 0 };\n  const goal: Ptr = { ...ptr };\n  let raf = 0;\n  let last = 0;\n  let time = 0;\n\n  const draw = () => {\n    gl.uniform2f(u(\"uRes\"), canvas.width, canvas.height);\n    gl.uniform1f(u(\"uTime\"), time);\n    gl.uniform2f(u(\"uMouse\"), ptr.x, ptr.y);\n    gl.uniform1f(u(\"uHover\"), ptr.hover);\n    update(gl, u);\n    gl.drawArrays(gl.TRIANGLES, 0, 3);\n  };\n  const loop = (now: number) => {\n    const dt = Math.min((now - last) / 1000, 0.1);\n    last = now;\n    const k = 1 - Math.exp(-dt * 4);\n    for (const key of [\"x\", \"y\", \"hover\"] as const) ptr[key] += (goal[key] - ptr[key]) * k;\n    time += dt * speed();\n    draw();\n    raf = requestAnimationFrame(loop);\n  };\n  const run = (on: boolean) => {\n    if (on && !raf && !reduced) {\n      last = performance.now();\n      raf = requestAnimationFrame(loop);\n    } else if (!on && raf) {\n      cancelAnimationFrame(raf);\n      raf = 0;\n    }\n  };\n  const resize = () => {\n    const dpr = Math.min(window.devicePixelRatio || 1, 2);\n    canvas.width = Math.max(1, Math.round(host.clientWidth * dpr));\n    canvas.height = Math.max(1, Math.round(host.clientHeight * dpr));\n    gl.viewport(0, 0, canvas.width, canvas.height);\n    draw();\n  };\n  const move = (e: PointerEvent) => {\n    const r = host.getBoundingClientRect();\n    goal.x = (e.clientX - r.left) / r.width;\n    goal.y = 1 - (e.clientY - r.top) / r.height;\n    goal.hover = 1;\n  };\n  const leave = () => {\n    goal.hover = 0;\n  };\n  const ro = new ResizeObserver(resize);\n  const io = new IntersectionObserver(([e]) => run(e.isIntersecting));\n  ro.observe(host);\n  io.observe(host);\n  zone.addEventListener(\"pointermove\", move);\n  zone.addEventListener(\"pointerleave\", leave);\n  return {\n    redraw: () => {\n      if (!raf) draw();\n    },\n    dispose: () => {\n      run(false);\n      ro.disconnect();\n      io.disconnect();\n      zone.removeEventListener(\"pointermove\", move);\n      zone.removeEventListener(\"pointerleave\", leave);\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n    },\n  };\n}\n\nconst rgb = (hex: string) => {\n  const n = parseInt(hex.replace(\"#\", \"\").padEnd(6, \"0\").slice(0, 6), 16);\n  return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n};\n\nexport function GrainGradient({\n  colors = [\"#ff6a3d\", \"#c026d3\", \"#4338ca\", \"#0c0a1f\"],\n  speed = 1,\n  grain = 0.5,\n  warp = 1,\n  className,\n}: GrainGradientProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const opts = useRef({ colors, speed, grain, warp });\n  const redraw = useRef<(() => void) | undefined>(undefined);\n\n  useEffect(() => {\n    opts.current = { colors, speed, grain, warp };\n    redraw.current?.();\n  }, [colors, speed, grain, warp]);\n\n  useEffect(() => {\n    let frame = 0;\n    const r = runShader(\n      ref.current!,\n      FRAG,\n      () => opts.current.speed,\n      (gl, u) => {\n        const o = opts.current;\n        const c = o.colors.length ? o.colors : [\"#000000\"];\n        for (let i = 0; i < 4; i++) gl.uniform3fv(u(`uC${i}`), rgb(c[i % c.length]));\n        gl.uniform1f(u(\"uGrain\"), o.grain);\n        gl.uniform1f(u(\"uWarp\"), o.warp);\n        // new grain pattern every other frame, independent of speed\n        gl.uniform1f(u(\"uSeed\"), Math.floor(frame++ / 2) % 64);\n      },\n    );\n    redraw.current = r?.redraw;\n    return () => r?.dispose();\n  }, []);\n\n  return (\n    <div\n      ref={ref}\n      aria-hidden\n      className={className}\n      style={{ position: \"absolute\", inset: 0, overflow: \"hidden\", pointerEvents: \"none\" }}\n    />\n  );\n}\n"}]}