{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "before-after",
  "description": "Drag the handle to wipe between two images with a liquid seam.",
  "dependencies": [
    "three"
  ],
  "registryDependencies": [
    "https://www.pixel-perfect.space/r/animated-texture.json"
  ],
  "files": [
    {
      "path": "registry/new-york/shaders/before-after.tsx",
      "content": "\"use client\";\n\n/**\n * Drag the handle to wipe between two images with a liquid seam.\n */\n\n\nimport { useEffect, useRef } from \"react\";\nimport * as THREE from \"three\";\nimport { createAnimatedTexture } from \"./animated-texture\";\n\nconst VERTEX = /* glsl */ `\n  varying vec2 vUv;\n  void main() {\n    vUv = uv;\n    gl_Position = vec4(position.xy, 0.0, 1.0);\n  }\n`;\n\nconst FRAGMENT = /* glsl */ `\n  precision highp float;\n  uniform sampler2D uA;\n  uniform sampler2D uB;\n  uniform vec2 uResA;\n  uniform vec2 uResB;\n  uniform vec2 resolution;\n  uniform float uSplit;\n  varying vec2 vUv;\n\n  vec2 coverUV(vec2 uv, vec2 res) {\n    float sa = resolution.x / resolution.y;\n    float ia = res.x / res.y;\n    vec2 s = vec2(1.0);\n    if (sa > ia) s.y = ia / sa; else s.x = sa / ia;\n    return (uv - 0.5) * s + 0.5;\n  }\n\n  void main() {\n    float dx = vUv.x - uSplit;\n    // liquid warp: pull pixels toward the seam, strongest right at it\n    float warp = smoothstep(0.07, 0.0, abs(dx)) * 0.05;\n    vec2 off = vec2(-sign(dx) * warp, 0.0);\n    vec3 a = texture2D(uA, coverUV(vUv + off, uResA)).rgb;\n    vec3 b = texture2D(uB, coverUV(vUv + off, uResB)).rgb;\n    vec3 col = dx < 0.0 ? a : b;\n    gl_FragColor = vec4(col, 1.0);\n  }\n`;\n\nconst BeforeAfter = ({\n  image,\n  imageB,\n  className,\n  dpr = 2,\n  interactive = false,\n}: {\n  image: string;\n  imageB: string;\n  className?: string;\n  dpr?: number;\n  interactive?: boolean;\n}) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const handleRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: \"low-power\" });\n    renderer.setPixelRatio(Math.min(window.devicePixelRatio, dpr));\n    container.appendChild(renderer.domElement);\n    const canvas = renderer.domElement;\n    canvas.style.display = \"block\";\n    const onContextLost = (e: Event) => e.preventDefault();\n    canvas.addEventListener(\"webglcontextlost\", onContextLost);\n\n    const camera = new THREE.Camera();\n    const scene = new THREE.Scene();\n    const geometry = new THREE.PlaneGeometry(2, 2);\n\n    const a = createAnimatedTexture(image);\n    const b = createAnimatedTexture(imageB);\n\n    const uniforms = {\n      uA: { value: a.texture },\n      uB: { value: b.texture },\n      uResA: { value: new THREE.Vector2(1, 1) },\n      uResB: { value: new THREE.Vector2(1, 1) },\n      resolution: { value: new THREE.Vector2() },\n      uSplit: { value: 0.5 },\n    };\n    const material = new THREE.ShaderMaterial({ vertexShader: VERTEX, fragmentShader: FRAGMENT, uniforms });\n    const mesh = new THREE.Mesh(geometry, material);\n    scene.add(mesh);\n\n    const split = { value: 0.5, target: 0.5 };\n    const onMove = (e: PointerEvent) => {\n      const r = canvas.getBoundingClientRect();\n      if (!r.width) return;\n      split.target = Math.min(Math.max((e.clientX - r.left) / r.width, 0.02), 0.98);\n    };\n    if (interactive) canvas.addEventListener(\"pointermove\", onMove);\n\n    let shaderTime = 0;\n    let last = performance.now();\n    const render = () => {\n      const now = performance.now();\n      const dt = Math.min((now - last) / 1000, 0.05);\n      last = now;\n      shaderTime += dt;\n\n      a.update(shaderTime * 1000);\n      b.update(shaderTime * 1000);\n      const ia = a.texture.image as { width: number; height: number };\n      const ib = b.texture.image as { width: number; height: number };\n      if (ia && ia.width > 1) uniforms.uResA.value.set(ia.width, ia.height);\n      if (ib && ib.width > 1) uniforms.uResB.value.set(ib.width, ib.height);\n\n      if (!interactive) split.target = 0.5 + 0.42 * Math.sin(shaderTime * 0.6);\n      split.value += (split.target - split.value) * Math.min(dt * 12, 1); // ease\n      uniforms.uSplit.value = split.value;\n      if (handleRef.current) handleRef.current.style.left = `${split.value * 100}%`;\n\n      renderer.render(scene, camera);\n    };\n\n    const resize = () => {\n      const w = container.clientWidth, h = container.clientHeight;\n      if (!w || !h) return;\n      renderer.setSize(w, h);\n      uniforms.resolution.value.set(renderer.domElement.width, renderer.domElement.height);\n      render();\n    };\n    resize();\n    const ro = new ResizeObserver(resize);\n    ro.observe(container);\n\n    let visible = true;\n    const io = new IntersectionObserver(([e]) => { visible = e.isIntersecting; last = performance.now(); }, { rootMargin: \"150px\", threshold: 0 });\n    io.observe(container);\n\n    let raf = 0;\n    const animate = () => { raf = requestAnimationFrame(animate); if (visible) render(); };\n    animate();\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      canvas.removeEventListener(\"webglcontextlost\", onContextLost);\n      canvas.removeEventListener(\"pointermove\", onMove);\n      if (canvas.parentNode === container) container.removeChild(canvas);\n      a.dispose();\n      b.dispose();\n      geometry.dispose();\n      material.dispose();\n      renderer.forceContextLoss();\n      renderer.dispose();\n    };\n  }, [image, imageB, dpr, interactive]);\n\n  return (\n    <div ref={containerRef} className={className} style={{ position: \"relative\", overflow: \"hidden\" }}>\n      <div\n        ref={handleRef}\n        aria-hidden\n        style={{\n          position: \"absolute\",\n          top: 0,\n          bottom: 0,\n          left: \"50%\",\n          width: 0,\n          zIndex: 10,\n          pointerEvents: \"none\",\n          transform: \"translateX(-1px)\",\n        }}\n      >\n        <div style={{ position: \"absolute\", top: 0, bottom: 0, left: 0, width: 2, background: \"rgba(255,255,255,0.85)\", boxShadow: \"0 0 8px rgba(0,0,0,0.4)\" }} />\n        <div\n          style={{\n            position: \"absolute\",\n            top: \"50%\",\n            left: 0,\n            transform: \"translate(-50%, -50%)\",\n            width: 30,\n            height: 30,\n            borderRadius: \"9999px\",\n            background: \"rgba(255,255,255,0.92)\",\n            color: \"#111\",\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            fontSize: 12,\n            boxShadow: \"0 1px 6px rgba(0,0,0,0.5)\",\n          }}\n        >\n          ‹›\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default BeforeAfter;\n",
      "type": "registry:component",
      "target": "components/pixel-perfect/before-after.tsx"
    }
  ],
  "type": "registry:block"
}