{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "displacement-image",
  "title": "Displacement Image",
  "description": "Two images crossing by pushing their pixels through the same noise.",
  "registryDependencies": [
    "utils",
    "https://ui.tinkererslabs.com/r/render-surface.json"
  ],
  "files": [
    {
      "path": "registry/default/displacement-image/displacement-image.tsx",
      "content": "\"use client\"\n\n/* eslint-disable @next/next/no-img-element -- This must work outside Next.js. */\n\nimport * as React from \"react\"\nimport {\n  RenderSurface,\n  createQuadProgram,\n  type QuadProgram,\n} from \"@/registry/default/render-surface/render-surface\"\nimport { cn } from \"@/lib/utils\"\n\nexport type DisplacementImageProps = React.HTMLAttributes<HTMLDivElement> & {\n  from: string\n  to: string\n  alt: string\n  /** How far the pixels are pushed during the crossing, as a fraction of the box. */\n  intensity?: number\n  /** Seconds the crossing takes. */\n  duration?: number\n  /** Drives the crossing yourself instead of on pointer and focus. */\n  active?: boolean\n}\n\nconst FRAGMENT = `\nprecision highp float;\nvarying vec2 vUv;\nuniform sampler2D uFrom;\nuniform sampler2D uTo;\nuniform float uProgress;\nuniform float uIntensity;\nuniform float uBoxAspect;\nuniform float uFromAspect;\nuniform float uToAspect;\n\nfloat hash(vec2 p) {\n  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat noise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(\n    mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x),\n    mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x),\n    u.y\n  );\n}\n\nfloat fbm(vec2 p) {\n  float value = 0.0;\n  float amplitude = 0.5;\n  for (int i = 0; i < 4; i++) {\n    value += amplitude * noise(p);\n    p *= 2.05;\n    amplitude *= 0.5;\n  }\n  return value;\n}\n\nvec2 cover(vec2 uv, float imageAspect) {\n  if (uBoxAspect > imageAspect) {\n    uv.y = (uv.y - 0.5) * (imageAspect / uBoxAspect) + 0.5;\n  } else {\n    uv.x = (uv.x - 0.5) * (uBoxAspect / imageAspect) + 0.5;\n  }\n  return uv;\n}\n\nvoid main() {\n  vec2 uv = vec2(vUv.x, 1.0 - vUv.y);\n  float n = fbm(uv * 3.0);\n  vec2 push = vec2(n - 0.5, fbm(uv * 3.0 + 7.3) - 0.5) * uIntensity;\n  float p = smoothstep(0.0, 1.0, uProgress);\n\n  vec4 a = texture2D(uFrom, cover(uv + push * p, uFromAspect));\n  vec4 b = texture2D(uTo, cover(uv - push * (1.0 - p), uToAspect));\n\n  gl_FragColor = mix(a, b, p);\n}\n`\n\ntype Slot = { texture: WebGLTexture | null; aspect: number }\n\ntype Scene = {\n  quad: QuadProgram | null\n  from: Slot\n  to: Slot\n  progress: number\n  cancelled: boolean\n}\n\nfunction loadInto(\n  gl: WebGLRenderingContext,\n  slot: Slot,\n  src: string,\n  scene: Scene\n) {\n  const texture = gl.createTexture()\n  slot.texture = texture\n\n  gl.bindTexture(gl.TEXTURE_2D, texture)\n  gl.texImage2D(\n    gl.TEXTURE_2D,\n    0,\n    gl.RGBA,\n    1,\n    1,\n    0,\n    gl.RGBA,\n    gl.UNSIGNED_BYTE,\n    new Uint8Array([0, 0, 0, 0])\n  )\n\n  const image = new Image()\n  image.crossOrigin = \"anonymous\"\n  image.decoding = \"async\"\n\n  image.onload = () => {\n    if (scene.cancelled || !slot.texture) return\n\n    slot.aspect = image.naturalWidth / Math.max(image.naturalHeight, 1)\n    gl.bindTexture(gl.TEXTURE_2D, slot.texture)\n    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0)\n    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image)\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)\n  }\n\n  image.src = src\n}\n\n/**\n * Crosses between two images by pushing their pixels through the same noise in\n * opposite directions. The first image is also rendered as ordinary markup\n * underneath, so a reader whose browser refuses WebGL still sees a picture.\n */\nexport function DisplacementImage({\n  from,\n  to,\n  alt,\n  intensity = 0.35,\n  duration = 0.7,\n  active,\n  className,\n  children,\n  ...rootProps\n}: DisplacementImageProps) {\n  const [hovered, setHovered] = React.useState(false)\n  const target = active ?? hovered\n  const targetRef = React.useRef(target)\n\n  React.useEffect(() => {\n    targetRef.current = target\n  }, [target])\n\n  const setup = React.useCallback(\n    ({ context }: { context: WebGLRenderingContext }) => {\n      const scene: Scene = {\n        quad: createQuadProgram(context, FRAGMENT),\n        from: { texture: null, aspect: 1 },\n        to: { texture: null, aspect: 1 },\n        progress: targetRef.current ? 1 : 0,\n        cancelled: false,\n      }\n\n      loadInto(context, scene.from, from, scene)\n      loadInto(context, scene.to, to, scene)\n\n      return scene\n    },\n    [from, to]\n  )\n\n  const draw = React.useCallback(\n    ({\n      context,\n      size,\n      state,\n      delta,\n    }: {\n      context: WebGLRenderingContext\n      size: { width: number; height: number; dpr: number }\n      state: Scene\n      delta: number\n    }) => {\n      const quad = state.quad\n      if (!quad) return\n\n      const goal = targetRef.current ? 1 : 0\n      const step = duration > 0 ? delta / duration : 1\n      state.progress +=\n        Math.sign(goal - state.progress) *\n        Math.min(Math.abs(goal - state.progress), step)\n\n      const { width, height, dpr } = size\n      context.useProgram(quad.program)\n\n      context.activeTexture(context.TEXTURE0)\n      context.bindTexture(context.TEXTURE_2D, state.from.texture)\n      context.uniform1i(quad.uniform(\"uFrom\"), 0)\n\n      context.activeTexture(context.TEXTURE1)\n      context.bindTexture(context.TEXTURE_2D, state.to.texture)\n      context.uniform1i(quad.uniform(\"uTo\"), 1)\n\n      context.uniform1f(quad.uniform(\"uProgress\"), state.progress)\n      context.uniform1f(quad.uniform(\"uIntensity\"), intensity)\n      context.uniform1f(quad.uniform(\"uBoxAspect\"), width / Math.max(height, 1))\n      context.uniform1f(quad.uniform(\"uFromAspect\"), state.from.aspect)\n      context.uniform1f(quad.uniform(\"uToAspect\"), state.to.aspect)\n\n      quad.paint(width, height, dpr)\n    },\n    [duration, intensity]\n  )\n\n  const teardown = React.useCallback((state: Scene) => {\n    state.cancelled = true\n    state.quad?.dispose()\n  }, [])\n\n  return (\n    <div\n      data-slot=\"displacement-image\"\n      data-active={target ? \"\" : undefined}\n      className={cn(\n        \"bg-muted relative isolate overflow-hidden rounded-[var(--radius)]\",\n        className\n      )}\n      onPointerEnter={() => setHovered(true)}\n      onPointerLeave={() => setHovered(false)}\n      onFocusCapture={() => setHovered(true)}\n      onBlurCapture={() => setHovered(false)}\n      {...rootProps}\n    >\n      <img\n        src={from}\n        alt={alt}\n        className=\"absolute inset-0 size-full object-cover\"\n      />\n\n      <RenderSurface<Scene, \"webgl\">\n        contextType=\"webgl\"\n        setup={setup}\n        draw={draw}\n        teardown={teardown}\n        label={alt}\n        className=\"absolute inset-0\"\n      />\n\n      {children !== undefined && <div className=\"relative\">{children}</div>}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "scene",
    "media",
    "transition",
    "webgl"
  ],
  "type": "registry:ui"
}