{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bend-mouse-follower",
  "description": "A pixel-art frame that smoothly chases the cursor and warps with an air-friction bend on a WebGL plane.",
  "dependencies": [
    "@react-three/fiber",
    "gsap",
    "three"
  ],
  "files": [
    {
      "path": "registry/new-york/mouse-follower/bend-mouse-follower.tsx",
      "content": "\"use client\";\n\n/**\n * A pixel-art frame that smoothly chases the cursor and warps with an air-friction bend on a WebGL plane.\n */\n\nimport { RefObject, useEffect, useMemo, useRef, useState } from \"react\";\nimport { Canvas, useFrame, useThree } from \"@react-three/fiber\";\nimport * as THREE from \"three\";\nimport gsap from \"gsap\";\n\nconst SRC = \"/bend-image-reveal.gif\";\nconst IMG_ASPECT = 540 / 304;\nconst lerp = (a: number, b: number, t: number) => a + (b - a) * t;\n\nconst vertexShader = /* glsl */ `\n  varying vec2 vUv;\n  uniform float uBendX;\n  uniform float uBendY;\n  void main() {\n    vUv = uv;\n    vec3 pos = position;\n    float px = uv.x * 2.0 - 1.0; // -1 (left)   .. +1 (right)\n    float py = uv.y * 2.0 - 1.0; // -1 (bottom) .. +1 (top)\n    pos.y += uBendY * px * px;\n    pos.x += uBendX * py * py;\n    gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);\n  }\n`;\n\nconst fragmentShader = /* glsl */ `\n  varying vec2 vUv;\n  uniform sampler2D uTex;\n  void main() {\n    gl_FragColor = texture2D(uTex, vUv);\n  }\n`;\n\nfunction MouseImage({ containerRef }: { containerRef: RefObject<HTMLDivElement | null> }) {\n  const meshRef = useRef<THREE.Mesh>(null);\n  const matRef = useRef<THREE.ShaderMaterial>(null);\n  const { size } = useThree();\n  const [texture, setTexture] = useState<THREE.Texture | null>(null);\n\n  const uniforms = useMemo(\n    () => ({\n      uTex: { value: null as THREE.Texture | null },\n      uBendX: { value: 0 },\n      uBendY: { value: 0 },\n    }),\n    [],\n  );\n\n  const planeH = Math.min(size.height * 0.4, (size.width * 0.55) / IMG_ASPECT);\n  const planeW = planeH * IMG_ASPECT;\n\n  useEffect(() => {\n    new THREE.TextureLoader().load(SRC, (t) => {\n      t.colorSpace = THREE.SRGBColorSpace;\n      t.magFilter = THREE.NearestFilter;\n      setTexture(t);\n    });\n  }, []);\n\n  useEffect(() => {\n    if (texture && matRef.current) matRef.current.uniforms.uTex.value = texture;\n  }, [texture]);\n\n  const target = useRef({ x: 0, y: 0 });\n  const st = useRef({ x: 0, y: 0, bx: 0, by: 0 });\n\n  useEffect(() => {\n    const el = containerRef.current;\n    if (!el) return;\n    const onMove = (e: PointerEvent) => {\n      const r = el.getBoundingClientRect();\n      const inside =\n        e.clientX >= r.left &&\n        e.clientX <= r.right &&\n        e.clientY >= r.top &&\n        e.clientY <= r.bottom;\n      if (inside) {\n        target.current.x = e.clientX - r.left - size.width / 2;\n        target.current.y = -(e.clientY - r.top - size.height / 2); // three.js: +y up\n      } else {\n        target.current.x = 0; // settle back to center when the cursor leaves the card\n        target.current.y = 0;\n      }\n    };\n    window.addEventListener(\"pointermove\", onMove);\n    return () => window.removeEventListener(\"pointermove\", onMove);\n  }, [containerRef, size.width, size.height]);\n\n  useFrame(() => {\n    const mesh = meshRef.current;\n    const mat = matRef.current;\n    if (!mesh || !mat) return;\n\n    const px = st.current.x;\n    const py = st.current.y;\n    st.current.x = lerp(st.current.x, target.current.x, 0.1);\n    st.current.y = lerp(st.current.y, target.current.y, 0.1);\n    mesh.position.set(st.current.x, st.current.y, 0);\n\n    const vx = st.current.x - px;\n    const vy = st.current.y - py;\n    st.current.bx = lerp(st.current.bx, gsap.utils.clamp(-45, 45, -vx * 1.5), 0.15);\n    st.current.by = lerp(st.current.by, gsap.utils.clamp(-45, 45, -vy * 1.5), 0.15);\n    mat.uniforms.uBendX.value = st.current.bx;\n    mat.uniforms.uBendY.value = st.current.by;\n  });\n\n  if (!texture) return null;\n  return (\n    <mesh ref={meshRef}>\n      <planeGeometry args={[planeW, planeH, 48, 48]} />\n      <shaderMaterial\n        ref={matRef}\n        vertexShader={vertexShader}\n        fragmentShader={fragmentShader}\n        uniforms={uniforms}\n      />\n    </mesh>\n  );\n}\n\nconst Page = () => {\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  return (\n    <div ref={containerRef} className=\"absolute inset-0\">\n      <Canvas\n        orthographic\n        camera={{ position: [0, 0, 100], zoom: 1, near: 1, far: 1000 }}\n        dpr={[1, 2]}\n        gl={{ antialias: true, alpha: true }}\n      >\n        <MouseImage containerRef={containerRef} />\n      </Canvas>\n    </div>\n  );\n};\n\nexport default Page;\n",
      "type": "registry:component",
      "target": "components/pixel-perfect/bend-mouse-follower.tsx"
    }
  ],
  "type": "registry:block"
}