{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"block-dissolve","type":"registry:ui","title":"Block Dissolve","description":"On hover or focus, an image dissolves into a second one through a wave of pixel blocks spreading from the pointer.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/block-dissolve.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface BlockDissolveProps {\n  /** Image shown at rest (CORS-enabled URL). */\n  src: string;\n  /** Image revealed on hover / focus. */\n  hoverSrc: string;\n  alt?: string;\n  /** Pixel block size in px. */\n  blockSize?: number;\n  /** Transition length in ms. */\n  duration?: number;\n  /** Color of the sparkling front edge of the dissolve. */\n  accent?: string;\n  className?: string;\n}\n\nexport function BlockDissolve({\n  src,\n  hoverSrc,\n  alt = \"\",\n  blockSize = 18,\n  duration = 700,\n  accent = \"#c6ff3d\",\n  className,\n}: BlockDissolveProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const cv = useRef<HTMLCanvasElement>(null);\n\n  useEffect(() => {\n    const el = ref.current!;\n    const canvas = cv.current!;\n    const ctx = canvas.getContext(\"2d\")!;\n    const reduced = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const load = (u: string) => {\n      const i = new Image();\n      i.crossOrigin = \"anonymous\";\n      i.src = u;\n      return i;\n    };\n    const imgs = [load(src), load(hoverSrc)];\n    const layers = [document.createElement(\"canvas\"), document.createElement(\"canvas\")];\n    const tiny = document.createElement(\"canvas\"); // hover image at one pixel per block\n    let w = 0;\n    let h = 0;\n    let dpr = 1;\n    let cols = 0;\n    let rows = 0;\n    let order = new Float32Array(0);\n    let p = 0;\n    let target = 0;\n    let raf = 0;\n    let prev = 0;\n    const origin = { x: 0.5, y: 0.5 };\n    const band = 0.18;\n\n    const cover = (c: HTMLCanvasElement, img: HTMLImageElement, bw: number, bh: number) => {\n      c.width = bw;\n      c.height = bh;\n      if (!img.naturalWidth) return;\n      const s = Math.max(bw / img.naturalWidth, bh / img.naturalHeight);\n      const iw = img.naturalWidth * s;\n      const ih = img.naturalHeight * s;\n      c.getContext(\"2d\")!.drawImage(img, (bw - iw) / 2, (bh - ih) / 2, iw, ih);\n    };\n\n    const seed = () => {\n      // Blocks flip in a noisy wave spreading from where the pointer entered.\n      order = new Float32Array(cols * rows);\n      const max = Math.hypot(Math.max(origin.x, 1 - origin.x), Math.max(origin.y, 1 - origin.y));\n      for (let r = 0; r < rows; r++)\n        for (let c = 0; c < cols; c++) {\n          const d = Math.hypot((c + 0.5) / cols - origin.x, (r + 0.5) / rows - origin.y) / max;\n          order[r * cols + c] = d * 0.55 + Math.random() * 0.45;\n        }\n    };\n\n    const build = () => {\n      dpr = Math.min(window.devicePixelRatio || 1, 2);\n      w = canvas.offsetWidth;\n      h = canvas.offsetHeight;\n      canvas.width = Math.round(w * dpr);\n      canvas.height = Math.round(h * dpr);\n      cols = Math.ceil(w / blockSize);\n      rows = Math.ceil(h / blockSize);\n      imgs.forEach((img, i) => cover(layers[i], img, canvas.width, canvas.height));\n      tiny.width = cols;\n      tiny.height = rows;\n      const tctx = tiny.getContext(\"2d\")!;\n      tctx.drawImage(layers[1], 0, 0, cols, rows);\n      if (order.length !== cols * rows) seed();\n      draw();\n    };\n\n    const draw = () => {\n      ctx.imageSmoothingEnabled = true;\n      ctx.drawImage(layers[0], 0, 0);\n      if (p <= 0) return;\n      if (p >= 1) {\n        ctx.drawImage(layers[1], 0, 0);\n        return;\n      }\n      const bs = blockSize * dpr;\n      // Map progress so every block, including the latest, finishes exactly at p = 1.\n      const q = p * (1 + band);\n      ctx.fillStyle = accent;\n      for (let r = 0; r < rows; r++)\n        for (let c = 0; c < cols; c++) {\n          const o = order[r * cols + c];\n          const x = c * bs;\n          const y = r * bs;\n          if (q > o + band) {\n            ctx.drawImage(layers[1], x, y, bs, bs, x, y, bs, bs);\n          } else if (q > o) {\n            // Mid-flip: a flat block of the new image's average color, lit at the leading edge.\n            ctx.imageSmoothingEnabled = false;\n            ctx.drawImage(tiny, c, r, 1, 1, x, y, bs, bs);\n            const k = 1 - (q - o) / band;\n            if (k > 0.6) {\n              ctx.globalAlpha = (k - 0.6) * 2;\n              ctx.fillRect(x, y, bs, bs);\n              ctx.globalAlpha = 1;\n            }\n          }\n        }\n    };\n\n    const loop = (now: number) => {\n      const dt = prev ? now - prev : 16;\n      prev = now;\n      const step = reduced ? 1 : dt / duration;\n      p = target > p ? Math.min(target, p + step) : Math.max(target, p - step);\n      draw();\n      raf = p !== target ? requestAnimationFrame(loop) : 0;\n      if (!raf) prev = 0;\n    };\n    const go = (to: number, e?: PointerEvent | FocusEvent) => {\n      if (e && \"clientX\" in e && p <= 0) {\n        const r = el.getBoundingClientRect();\n        origin.x = (e.clientX - r.left) / r.width;\n        origin.y = (e.clientY - r.top) / r.height;\n        seed();\n      }\n      target = to;\n      if (!raf) raf = requestAnimationFrame(loop);\n    };\n    const enter = (e: PointerEvent) => go(1, e);\n    const leave = () => go(0);\n    const focus = () => go(1);\n\n    imgs.forEach((img) => (img.onload = build));\n    const ro = new ResizeObserver(build);\n    ro.observe(canvas);\n    el.addEventListener(\"pointerenter\", enter);\n    el.addEventListener(\"pointerleave\", leave);\n    el.addEventListener(\"focus\", focus);\n    el.addEventListener(\"blur\", leave);\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      imgs.forEach((img) => (img.onload = null));\n      el.removeEventListener(\"pointerenter\", enter);\n      el.removeEventListener(\"pointerleave\", leave);\n      el.removeEventListener(\"focus\", focus);\n      el.removeEventListener(\"blur\", leave);\n    };\n  }, [src, hoverSrc, blockSize, duration, accent]);\n\n  return (\n    <div\n      ref={ref}\n      tabIndex={0}\n      role=\"img\"\n      aria-label={alt}\n      className={cn(\"relative overflow-hidden outline-none focus-visible:ring-2 focus-visible:ring-ring\", className)}\n    >\n      <canvas ref={cv} aria-hidden style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\" }} />\n    </div>\n  );\n}\n"}]}