{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"pixel-shock","type":"registry:ui","title":"Pixel Shock","description":"A dithered field of tiny pixels that breathes with noise and bursts into shockwave rings on click. WebGL.","dependencies":[],"registryDependencies":[],"files":[{"path":"registry/ui/pixel-shock.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nexport interface PixelShockProps {\n  color?: string;\n  shape?: \"square\" | \"circle\";\n  /** Cell size in CSS px. */\n  pixelSize?: number;\n  /** 0–1, how much of the field is lit. */\n  density?: number;\n  /** Animation speed multiplier. */\n  speed?: number;\n  /** Blast rings on click. */\n  interactive?: boolean;\n  /** Fire a blast at a random spot every few seconds. */\n  autoBlast?: boolean;\n  className?: string;\n}\n\nconst MAX_BLASTS = 8;\n\nconst FRAG = `precision highp float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform vec3 uColor;\nuniform float uPx;\nuniform float uDensity;\nuniform float uCircle;\nuniform vec4 uBlast[${MAX_BLASTS}];\n\nfloat hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }\nfloat noise(vec2 p){\n  vec2 i = floor(p), f = fract(p);\n  f = f * f * (3.0 - 2.0 * f);\n  return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y);\n}\nfloat fbm(vec2 p){ return noise(p) * 0.55 + noise(p * 2.1 + 5.3) * 0.3 + noise(p * 4.3 + 1.7) * 0.15; }\n// Ordered-dither threshold (8x8 Bayer), 0..1.\nfloat bayer2(vec2 a){ a = floor(a); return fract(dot(a, vec2(0.5, a.y * 0.75))); }\nfloat bayer8(vec2 a){ return (bayer2(0.25 * a) * 0.25 + bayer2(0.5 * a)) * 0.25 + bayer2(a); }\n\nvoid main(){\n  vec2 cell = floor(gl_FragCoord.xy / uPx);\n  vec2 local = fract(gl_FragCoord.xy / uPx) - 0.5;\n  vec2 c = (cell + 0.5) * uPx / uRes.y; // cell center, container-height units\n  float t = uTime;\n\n  float n = fbm(c * 3.0 + vec2(t * 0.07, -t * 0.05));\n  float v = smoothstep(0.62 - uDensity * 0.45, 0.95 - uDensity * 0.3, n);\n  v *= 0.75 + 0.25 * sin(t * 1.4 + n * 12.0); // breathing pulse\n\n  float ring = 0.0;\n  for (int i = 0; i < ${MAX_BLASTS}; i++) {\n    vec4 b = uBlast[i];\n    if (b.w > 0.001) {\n      float d = length(c - b.xy);\n      float front = b.z * 0.75;\n      float x = d - front;\n      float fade = exp(-b.z * 1.1) * b.w;\n      ring += (exp(-x * x * 300.0) * 1.3 + exp(-x * x * 30.0) * 0.4 * step(x, 0.0)) * fade;\n    }\n  }\n  v = clamp(v + ring, 0.0, 1.0);\n\n  float on = step(bayer8(cell) + 0.001, v);\n  float s = 0.42;\n  float aa = 1.0 / uPx;\n  float mask = uCircle > 0.5\n    ? 1.0 - smoothstep(s - aa, s + aa, length(local))\n    : (1.0 - smoothstep(s - aa, s + aa, abs(local.x))) * (1.0 - smoothstep(s - aa, s + aa, abs(local.y)));\n\n  vec3 col = mix(uColor * (0.55 + 0.45 * v), vec3(1.0), min(ring, 1.0) * 0.55) * on * mask;\n  gl_FragColor = vec4(col, on * mask);\n}`;\n\nfunction hexToRgb(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\ntype SetUniform = (name: string, v: number | number[], size?: number) => void;\n\n/** Full-screen-triangle WebGL runner: sizes to its host, pauses offscreen, one static frame under reduced motion. */\nfunction mountShader(host: HTMLElement, frag: string, frame: (set: SetUniform, dt: number) => 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, premultipliedAlpha: true });\n  if (!gl) return null;\n  const prog = gl.createProgram()!;\n  const stages: [number, string][] = [\n    [gl.VERTEX_SHADER, \"attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}\"],\n    [gl.FRAGMENT_SHADER, frag],\n  ];\n  for (const [type, src] of stages) {\n    const s = gl.createShader(type)!;\n    gl.shaderSource(s, src);\n    gl.compileShader(s);\n    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) console.warn(gl.getShaderInfoLog(s));\n    gl.attachShader(prog, s);\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 setters = [gl.uniform1fv, gl.uniform2fv, gl.uniform3fv, gl.uniform4fv];\n  const set: SetUniform = (name, v, size) => {\n    if (!locs.has(name)) locs.set(name, gl.getUniformLocation(prog, name));\n    const arr = typeof v === \"number\" ? [v] : v;\n    setters[(size ?? arr.length) - 1].call(gl, locs.get(name)!, arr);\n  };\n\n  const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  let raf = 0;\n  let last = 0;\n  const render = (dt: number) => {\n    set(\"uRes\", [canvas.width, canvas.height]);\n    frame(set, dt);\n    gl.drawArrays(gl.TRIANGLES, 0, 3);\n  };\n  const loop = (now: number) => {\n    render(last ? Math.min((now - last) / 1000, 0.1) : 0);\n    last = now;\n    raf = requestAnimationFrame(loop);\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    render(0);\n  };\n  const ro = new ResizeObserver(resize);\n  ro.observe(host);\n  const io = new IntersectionObserver(([e]) => {\n    cancelAnimationFrame(raf);\n    raf = 0;\n    last = 0;\n    if (e.isIntersecting && !reduced) raf = requestAnimationFrame(loop);\n  });\n  io.observe(host);\n\n  return {\n    redraw: () => {\n      if (!raf) render(0);\n    },\n    destroy: () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n    },\n  };\n}\n\nexport function PixelShock({\n  color = \"#a78bfa\",\n  shape = \"square\",\n  pixelSize = 6,\n  density = 0.5,\n  speed = 1,\n  interactive = true,\n  autoBlast = true,\n  className,\n}: PixelShockProps) {\n  const host = useRef<HTMLDivElement>(null);\n  const opts = useRef({ color: hexToRgb(color), shape, pixelSize, density, speed, interactive, autoBlast });\n  const redraw = useRef<() => void>(undefined);\n\n  useEffect(() => {\n    opts.current = { color: hexToRgb(color), shape, pixelSize, density, speed, interactive, autoBlast };\n    redraw.current?.();\n  }, [color, shape, pixelSize, density, speed, interactive, autoBlast]);\n\n  useEffect(() => {\n    const el = host.current!;\n    const area = el.parentElement ?? el;\n    // Blast ring buffer: x, y (container-height units, y up), age in seconds, strength.\n    const blasts = new Float32Array(MAX_BLASTS * 4);\n    let next = 0;\n    let t = 4;\n    let idle = 1.8;\n    const add = (x: number, y: number, s: number) => {\n      blasts.set([x, y, 0, s], next * 4);\n      next = (next + 1) % MAX_BLASTS;\n    };\n\n    const shader = mountShader(el, FRAG, (set, dt) => {\n      const o = opts.current;\n      t += dt * o.speed;\n      for (let i = 0; i < MAX_BLASTS; i++) {\n        blasts[i * 4 + 2] += dt * o.speed;\n        if (blasts[i * 4 + 2] > 4) blasts[i * 4 + 3] = 0;\n      }\n      idle += dt;\n      if (o.autoBlast && idle > 1.0) {\n        idle = 0;\n        const aspect = el.clientWidth / Math.max(el.clientHeight, 1);\n        add((0.15 + Math.random() * 0.7) * aspect, 0.15 + Math.random() * 0.7, 0.9);\n      }\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      set(\"uTime\", t);\n      set(\"uColor\", o.color);\n      set(\"uPx\", Math.max(2, o.pixelSize) * dpr);\n      set(\"uDensity\", Math.min(Math.max(o.density, 0), 1));\n      set(\"uCircle\", o.shape === \"circle\" ? 1 : 0);\n      set(\"uBlast\", Array.from(blasts), 4);\n    });\n    if (!shader) return;\n    redraw.current = shader.redraw;\n\n    const onDown = (e: PointerEvent) => {\n      if (!opts.current.interactive) return;\n      const r = el.getBoundingClientRect();\n      idle = 0;\n      add((e.clientX - r.left) / r.height, 1 - (e.clientY - r.top) / r.height, 1.4);\n    };\n    area.addEventListener(\"pointerdown\", onDown);\n    return () => {\n      redraw.current = undefined;\n      area.removeEventListener(\"pointerdown\", onDown);\n      shader.destroy();\n    };\n  }, []);\n\n  return (\n    <div\n      ref={host}\n      aria-hidden\n      className={className}\n      style={{ position: \"absolute\", inset: 0, overflow: \"hidden\", pointerEvents: \"none\" }}\n    />\n  );\n}\n"}]}