{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shader-hero",
  "type": "registry:ui",
  "title": "Shader hero",
  "description": "Uses a low-power WebGL field that reshapes around pointer intent.",
  "dependencies": [
    "motion"
  ],
  "categories": [
    "hero-story"
  ],
  "docs": "https://motion-lexicon.pages.dev/en/components/shader-hero/",
  "meta": {
    "engines": [
      "motion",
      "webgl"
    ],
    "runtimeCost": "heavy",
    "signature": "Pointer input reshapes the focal wave field",
    "sceneFamily": "spatial-dark",
    "motionRole": "ambient",
    "primaryState": "Still dark light field with legible heading",
    "assetProvenance": "generated"
  },
  "files": [
    {
      "path": "src/registry/components/shader-hero.tsx",
      "type": "registry:ui",
      "target": "components/motion-lexicon/shader-hero.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { useReducedMotion } from \"motion/react\";\n\nexport type ShaderHeroProps = {\n  eyebrow?: string;\n  title?: string;\n  description?: string;\n  actionLabel?: string;\n  onAction?: () => void;\n  className?: string;\n};\n\nconst vertexSource = `attribute vec2 position; void main() { gl_Position = vec4(position, 0.0, 1.0); }`;\nconst fragmentSource = `precision mediump float;\nuniform vec2 resolution; uniform vec2 pointer; uniform float time; uniform float motion;\nfloat hash(vec2 p) { return fract(sin(dot(p, vec2(41.31, 289.23))) * 43758.5); }\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / resolution.xy; uv.x *= resolution.x / resolution.y;\n  vec2 p = uv - vec2(.82, .54); p.x -= .28;\n  float distanceToPointer = length(uv - pointer);\n  float wave = sin((uv.x * 7. + uv.y * 4. + time * .45) + distanceToPointer * 16.) * .055 * motion;\n  float arc = smoothstep(.61 + wave, .12 + wave, length(p + vec2(sin(time*.18)*.035, 0.0)));\n  float rim = smoothstep(.13, .0, abs(length(p) - .32 - wave));\n  float grain = (hash(gl_FragCoord.xy) - .5) * .035;\n  vec3 base = vec3(.025, .035, .06);\n  vec3 bloom = vec3(.18, .75, .82) * arc + vec3(.85, .2, .47) * rim;\n  vec3 color = base + bloom * (1. - distanceToPointer * .32) + grain;\n  gl_FragColor = vec4(color, 1.0);\n}`;\n\nfunction compile(gl: WebGLRenderingContext, type: number, source: string) {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { gl.deleteShader(shader); return null; }\n  return shader;\n}\n\nexport function ShaderHero({\n  eyebrow = \"Spatial interface\",\n  title = \"Make the atmosphere part of the product.\",\n  description = \"A responsive field that holds its shape before interaction and bends gently around intent.\",\n  actionLabel = \"Open field notes\",\n  onAction,\n  className = \"\",\n}: ShaderHeroProps) {\n  const reduced = useReducedMotion() === true;\n  const headingId = useId();\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const visibleRef = useRef(true);\n  const pointerRef = useRef({ x: 0.76, y: 0.46 });\n  const frameRef = useRef<number | null>(null);\n  const [supported, setSupported] = useState(true);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const gl = canvas.getContext(\"webgl\", { alpha: false, antialias: false, powerPreference: \"low-power\" });\n    if (!gl) { setSupported(false); return; }\n    const vertex = compile(gl, gl.VERTEX_SHADER, vertexSource);\n    const fragment = compile(gl, gl.FRAGMENT_SHADER, fragmentSource);\n    if (!vertex || !fragment) {\n      setSupported(false);\n      if (vertex) gl.deleteShader(vertex);\n      if (fragment) gl.deleteShader(fragment);\n      return;\n    }\n    const program = gl.createProgram();\n    if (!program) { setSupported(false); return; }\n    gl.attachShader(program, vertex); gl.attachShader(program, fragment); gl.linkProgram(program);\n    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { setSupported(false); gl.deleteProgram(program); return; }\n    const buffer = gl.createBuffer();\n    if (!buffer) { setSupported(false); return; }\n    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);\n    const position = gl.getAttribLocation(program, \"position\");\n    const resolution = gl.getUniformLocation(program, \"resolution\");\n    const pointer = gl.getUniformLocation(program, \"pointer\");\n    const time = gl.getUniformLocation(program, \"time\");\n    const motion = gl.getUniformLocation(program, \"motion\");\n    const start = performance.now();\n    const resize = () => {\n      const bounds = canvas.getBoundingClientRect();\n      const ratio = Math.min(window.devicePixelRatio || 1, 1.5);\n      canvas.width = Math.max(1, Math.floor(bounds.width * ratio));\n      canvas.height = Math.max(1, Math.floor(bounds.height * ratio));\n      gl.viewport(0, 0, canvas.width, canvas.height);\n    };\n    const draw = (now: number) => {\n      frameRef.current = null;\n      gl.useProgram(program);\n      gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n      gl.enableVertexAttribArray(position);\n      gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n      gl.uniform2f(resolution, canvas.width, canvas.height);\n      gl.uniform2f(pointer, pointerRef.current.x, pointerRef.current.y);\n      gl.uniform1f(time, reduced ? 0 : (now - start) / 1000);\n      gl.uniform1f(motion, reduced ? 0 : 1);\n      gl.drawArrays(gl.TRIANGLES, 0, 6);\n      if (!reduced && visibleRef.current) frameRef.current = requestAnimationFrame(draw);\n    };\n    const observer = new IntersectionObserver(([entry]) => {\n      visibleRef.current = entry?.isIntersecting ?? false;\n      if (visibleRef.current && frameRef.current === null) frameRef.current = requestAnimationFrame(draw);\n    }, { threshold: 0.08 });\n    const resizeObserver = new ResizeObserver(() => { resize(); if (frameRef.current === null) frameRef.current = requestAnimationFrame(draw); });\n    resize(); observer.observe(canvas); resizeObserver.observe(canvas);\n    frameRef.current = requestAnimationFrame(draw);\n    return () => {\n      observer.disconnect(); resizeObserver.disconnect();\n      if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);\n      gl.deleteBuffer(buffer); gl.deleteProgram(program); gl.deleteShader(vertex); gl.deleteShader(fragment);\n    };\n  }, [reduced]);\n\n  return (\n    <section aria-labelledby={headingId} className={`relative isolate min-h-[390px] overflow-hidden rounded-[18px] bg-[#070a12] px-5 py-6 text-white shadow-[0_20px_65px_-26px_rgba(15,30,72,.78)] sm:px-8 sm:py-9 ${className}`}>\n      <canvas\n        ref={canvasRef}\n        aria-hidden\n        onPointerMove={(event) => {\n          if (reduced || event.pointerType !== \"mouse\") return;\n          const bounds = event.currentTarget.getBoundingClientRect();\n          pointerRef.current = { x: (event.clientX - bounds.left) / bounds.width, y: 1 - (event.clientY - bounds.top) / bounds.height };\n        }}\n        className=\"absolute inset-0 size-full\"\n      />\n      {!supported ? <div aria-hidden className=\"absolute inset-0 bg-[radial-gradient(circle_at_75%_50%,#248c9a,transparent_28%),radial-gradient(circle_at_82%_48%,#a82d64,transparent_13%),#070a12]\" /> : null}\n      <div className=\"relative z-10 flex min-h-[342px] max-w-md flex-col justify-end\">\n        <span className=\"font-mono text-[10px] uppercase tracking-[.2em] text-cyan-100/68\">{eyebrow}</span>\n        <h3 id={headingId} className=\"mt-3 max-w-[11ch] text-4xl font-medium leading-[.94] tracking-[-.06em] sm:text-5xl\">{title}</h3>\n        <p className=\"mt-4 max-w-sm text-[13px] leading-relaxed text-white/72\">{description}</p>\n        <button type=\"button\" onClick={onAction} className=\"mt-6 min-h-11 w-fit rounded-full border border-white/22 bg-white/[.1] px-4 text-[12px] font-medium outline-none backdrop-blur-md transition hover:bg-white/[.17] focus-visible:ring-2 focus-visible:ring-cyan-200 focus-visible:ring-offset-2 focus-visible:ring-offset-[#070a12]\">{actionLabel}</button>\n      </div>\n      <span className=\"absolute right-5 top-5 rounded-full border border-white/15 bg-black/15 px-2.5 py-1 font-mono text-[9px] uppercase tracking-[.16em] text-white/60\">{reduced ? \"Still\" : \"WebGL\"}</span>\n    </section>\n  );\n}\n"
    }
  ]
}
