{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "liquid-metal",
  "title": "Liquid metal",
  "description": "A surface of flowing chrome from a tiny WebGL shader, with still metal bands as fallback.",
  "dependencies": [],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/liquid-metal/liquid-metal.tsx",
      "content": "import { useEffect, useRef, type ReactNode } from 'react'\nimport { useReducedMotion } from '@/registry/manniche/hooks/use-reduced-motion'\nimport { cn } from '@/lib/utils'\n\nexport type LiquidMetalProps = {\n  /** Flow speed; 1 is slow and heavy. */\n  speed?: number\n  /** Content shown on top of the metal. */\n  children?: ReactNode\n  className?: string\n}\n\nconst VERTEX = `attribute vec2 a;void main(){gl_Position=vec4(a,0.,1.);}`\n\n// The plane is folded a few times with sine waves, then read through a chrome palette of dark and bright bands.\nconst FRAGMENT = `precision mediump float;\nuniform vec2 r;uniform float t;\nvoid main(){\n  vec2 p=(gl_FragCoord.xy*2.-r)/min(r.x,r.y);\n  for(int i=1;i<5;i++){\n    float f=float(i);\n    p+=vec2(.6/f*sin(f*p.y+t+.3*f),.4/f*sin(f*p.x+t*.8+.3*f));\n  }\n  float v=.5+.5*sin(p.x*2.2+p.y*1.4);\n  float band=pow(abs(sin(v*9.42)),.6);\n  vec3 c=mix(vec3(.16,.17,.2),vec3(.96,.97,.99),band);\n  c+=vec3(.05,.02,.08)*sin(v*12.);\n  gl_FragColor=vec4(c,1.);\n}`\n\n/** A surface of flowing chrome, drawn with a tiny WebGL shader. Without WebGL it falls back to still metal bands. */\nexport function LiquidMetal({ speed = 1, children, className }: LiquidMetalProps) {\n  const canvas = useRef<HTMLCanvasElement>(null)\n  const reduce = useReducedMotion()\n\n  useEffect(() => {\n    const el = canvas.current\n    const gl = el?.getContext('webgl', { antialias: false, premultipliedAlpha: false })\n    if (!el || !gl) return\n\n    const shader = (type: number, src: string) => {\n      const s = gl.createShader(type)!\n      gl.shaderSource(s, src)\n      gl.compileShader(s)\n      return s\n    }\n    const program = gl.createProgram()!\n    gl.attachShader(program, shader(gl.VERTEX_SHADER, VERTEX))\n    gl.attachShader(program, shader(gl.FRAGMENT_SHADER, FRAGMENT))\n    gl.linkProgram(program)\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return\n    gl.useProgram(program)\n\n    // One triangle that covers the whole canvas.\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    const uRes = gl.getUniformLocation(program, 'r')\n    const uTime = gl.getUniformLocation(program, 't')\n\n    const draw = (now: number) => {\n      gl.uniform1f(uTime, (now / 1000) * 0.35 * speed)\n      gl.drawArrays(gl.TRIANGLES, 0, 3)\n    }\n\n    const resize = () => {\n      const dpr = Math.min(1.5, window.devicePixelRatio || 1)\n      el.width = Math.max(1, Math.round(el.clientWidth * dpr))\n      el.height = Math.max(1, Math.round(el.clientHeight * dpr))\n      gl.viewport(0, 0, el.width, el.height)\n      gl.uniform2f(uRes, el.width, el.height)\n      draw(reduce ? 4000 : performance.now())\n    }\n    const ro = new ResizeObserver(resize)\n    ro.observe(el)\n    el.dataset.ready = ''\n\n    let raf = 0\n    let visible = false\n    const loop = (now: number) => {\n      draw(now)\n      raf = requestAnimationFrame(loop)\n    }\n    // Only animate while on screen.\n    const io = new IntersectionObserver(([entry]) => {\n      if (reduce || entry.isIntersecting === visible) return\n      visible = entry.isIntersecting\n      if (visible) raf = requestAnimationFrame(loop)\n      else cancelAnimationFrame(raf)\n    })\n    io.observe(el)\n\n    return () => {\n      ro.disconnect()\n      io.disconnect()\n      cancelAnimationFrame(raf)\n      delete el.dataset.ready\n      gl.getExtension('WEBGL_lose_context')?.loseContext()\n    }\n  }, [speed, reduce])\n\n  return (\n    <div\n      className={cn(\n        'relative isolate overflow-hidden',\n        // Fallback until (or if) WebGL starts: still chrome bands.\n        'bg-[linear-gradient(115deg,#2a2c33_0%,#f4f5f8_22%,#6b6e78_40%,#eef0f4_58%,#30323a_78%,#d9dbe1_100%)]',\n        className,\n      )}\n    >\n      <canvas ref={canvas} aria-hidden className=\"absolute inset-0 -z-10 size-full opacity-0 transition-opacity duration-500 data-ready:opacity-100\" />\n      {children}\n    </div>\n  )\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/manniche/hooks/use-reduced-motion.ts",
      "content": "import { useSyncExternalStore } from 'react'\n\nconst QUERY = '(prefers-reduced-motion: reduce)'\n\nfunction subscribe(onChange: () => void) {\n  const mq = window.matchMedia(QUERY)\n  mq.addEventListener('change', onChange)\n  return () => mq.removeEventListener('change', onChange)\n}\n\n/** True when the visitor has asked the system for less motion. */\nexport function useReducedMotion() {\n  return useSyncExternalStore(\n    subscribe,\n    () => window.matchMedia(QUERY).matches,\n    () => false,\n  )\n}\n",
      "type": "registry:hook"
    }
  ],
  "categories": [
    "surfaces"
  ],
  "type": "registry:component"
}