{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"strip-gradient","type":"registry:ui","title":"Strip Gradient","description":"A flowing wave gradient cut into horizontal strips that glide sideways independently. WebGL.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/strip-gradient.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface StripGradientProps {\n  /** First palette color. */\n  colorA?: string;\n  /** Second palette color. */\n  colorB?: string;\n  /** Third palette color. */\n  colorC?: string;\n  /** Animation speed multiplier. */\n  speed?: number;\n  /** Number of horizontal strips. */\n  slices?: number;\n  /** How far strips slide sideways. */\n  shift?: number;\n  /** Wave zoom; higher means tighter waves. */\n  scale?: number;\n  className?: string;\n}\n\nconst FRAG = `\nprecision highp float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform vec3 uA;\nuniform vec3 uB;\nuniform vec3 uC;\nuniform float uSlices;\nuniform float uShift;\nuniform float uScale;\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\nvec3 pal(float x) {\n  x = fract(x) * 3.0;\n  vec3 c = mix(uA, uB, smoothstep(0.0, 1.0, x));\n  c = mix(c, uC, smoothstep(1.0, 2.0, x));\n  return mix(c, uA, smoothstep(2.0, 3.0, x));\n}\n\nvoid main() {\n  float asp = uRes.x / uRes.y;\n  vec2 uv = gl_FragCoord.xy / uRes;\n  float t = uTime;\n\n  float s = uv.y * uSlices;\n  float id = floor(s);\n  float f = fract(s);\n\n  // each strip glides between random offsets on its own schedule\n  float tt = t * 0.3 + hash(vec2(id, 3.7)) * 10.0;\n  float k = floor(tt);\n  float e = smoothstep(0.0, 1.0, fract(tt));\n  e = e * e * (3.0 - 2.0 * e);\n  float off = (mix(hash(vec2(id, k)), hash(vec2(id, k + 1.0)), e) - 0.5) * uShift * 0.8;\n\n  vec2 p = vec2(uv.x * asp + off, uv.y) * 2.0 * uScale;\n  float v = p.x * 0.3\n    + 0.24 * sin(p.x * 2.1 + t * 0.55)\n    + 0.18 * sin(p.y * 3.3 - t * 0.45 + p.x * 0.8)\n    + 0.1 * sin((p.x - p.y) * 4.2 + t * 0.8);\n\n  vec3 col = pal(v * 1.1 + t * 0.02);\n  float lines = 0.5 + 0.5 * sin(v * 42.0);\n  col *= 0.88 + 0.12 * smoothstep(0.2, 0.9, lines);\n\n  // strip shading: lit top edge, soft shadow and a hairline gap at the bottom\n  float px = uSlices / uRes.y;\n  col *= 0.72 + 0.28 * smoothstep(0.0, 0.8, f);\n  col *= smoothstep(0.0, 1.5 * px, f);\n  col += smoothstep(1.0 - 3.0 * px, 1.0 - 1.0 * px, f) * 0.12;\n\n  vec2 q = uv - 0.5;\n  col *= 1.0 - dot(q, q) * 0.8;\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 StripGradient({\n  colorA = \"#0ea5e9\",\n  colorB = \"#8b5cf6\",\n  colorC = \"#f43f5e\",\n  speed = 1,\n  slices = 14,\n  shift = 1,\n  scale = 1,\n  className,\n}: StripGradientProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const opts = useRef({ colorA, colorB, colorC, speed, slices, shift, scale });\n  const redraw = useRef<(() => void) | undefined>(undefined);\n\n  useEffect(() => {\n    opts.current = { colorA, colorB, colorC, speed, slices, shift, scale };\n    redraw.current?.();\n  }, [colorA, colorB, colorC, speed, slices, shift, scale]);\n\n  useEffect(() => {\n    const r = runShader(\n      ref.current!,\n      FRAG,\n      () => opts.current.speed,\n      (gl, u) => {\n        const o = opts.current;\n        gl.uniform3fv(u(\"uA\"), rgb(o.colorA));\n        gl.uniform3fv(u(\"uB\"), rgb(o.colorB));\n        gl.uniform3fv(u(\"uC\"), rgb(o.colorC));\n        gl.uniform1f(u(\"uSlices\"), Math.max(1, Math.round(o.slices)));\n        gl.uniform1f(u(\"uShift\"), o.shift);\n        gl.uniform1f(u(\"uScale\"), Math.max(0.1, o.scale));\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"}]}