{
  "name": "pixelated-canvas",
  "type": "registry:ui",
  "files": [
    {
      "path": "components/ui/pixelated-canvas.tsx",
      "content": "\"use client\";\nimport React from \"react\";\n\ntype PixelatedCanvasProps = {\n  src: string;\n  width?: number;\n  height?: number;\n  /** Size of each cell (in CSS pixels) used for sampling and spacing. */\n  cellSize?: number;\n  /** Dot size as a fraction of cell size (0..1). */\n  dotScale?: number;\n  /** Shape of the dot drawn for each sample. */\n  shape?: \"circle\" | \"square\";\n  /** Optional background color to clear the canvas with before drawing. */\n  backgroundColor?: string;\n  /** Convert to grayscale before drawing. */\n  grayscale?: boolean;\n  className?: string;\n  /** Redraw on window resize using the provided width/height. */\n  responsive?: boolean;\n  /** 0..1. Higher value removes more dots in low-contrast regions. */\n  dropoutStrength?: number;\n  /** Enable interactive mouse distortion animation. */\n  interactive?: boolean;\n  /** Max per-dot offset (px) due to distortion. */\n  distortionStrength?: number;\n  /** Radius (px) around pointer influencing distortion. */\n  distortionRadius?: number;\n  /** How pixels move near the pointer. */\n  distortionMode?: \"repel\" | \"attract\" | \"swirl\";\n  /** 0..1 smoothing factor for pointer follow. */\n  followSpeed?: number;\n  /** Average multiple samples per cell instead of single center sample. */\n  sampleAverage?: boolean;\n  /** Apply a color tint (e.g., \"#0ea5e9\" or \"rgb(14,165,233)\"). */\n  tintColor?: string;\n  /** 0..1 tint mix amount with original colors. */\n  tintStrength?: number;\n  /** Cap animation frame rate to improve perf on large canvases. */\n  maxFps?: number;\n  /** Object-fit behavior for the source image within the canvas. */\n  objectFit?: \"cover\" | \"contain\" | \"fill\" | \"none\";\n  /** Random motion amplitude for dots near the pointer. */\n  jitterStrength?: number;\n  /** Speed factor for the random motion. */\n  jitterSpeed?: number;\n  /** Smoothly fade the distortion when the pointer leaves. */\n  fadeOnLeave?: boolean;\n  /** 0..1 smoothing factor for leave fade. Higher = faster fade. */\n  fadeSpeed?: number;\n};\n\nexport const PixelatedCanvas: React.FC<PixelatedCanvasProps> = ({\n  src,\n  width = 400,\n  height = 500,\n  cellSize = 3,\n  dotScale = 0.9,\n  shape = \"square\",\n  backgroundColor = \"#000000\",\n  grayscale = false,\n  className,\n  responsive = false,\n  dropoutStrength = 0.4,\n  interactive = true,\n  distortionStrength = 3,\n  distortionRadius = 80,\n  distortionMode = \"swirl\",\n  followSpeed = 0.2,\n  sampleAverage = true,\n  tintColor = \"#FFFFFF\",\n  tintStrength = 0.2,\n  maxFps = 60,\n  objectFit = \"cover\",\n  jitterStrength = 4,\n  jitterSpeed = 4,\n  fadeOnLeave = true,\n  fadeSpeed = 0.1,\n}) => {\n  const canvasRef = React.useRef<HTMLCanvasElement | null>(null);\n  const samplesRef = React.useRef<\n    Array<{\n      x: number;\n      y: number;\n      r: number;\n      g: number;\n      b: number;\n      a: number;\n      drop: boolean;\n      seed: number;\n    }>\n  >([]);\n  const dimsRef = React.useRef<{\n    width: number;\n    height: number;\n    dot: number;\n  } | null>(null);\n  const targetMouseRef = React.useRef<{ x: number; y: number }>({\n    x: -9999,\n    y: -9999,\n  });\n  const animMouseRef = React.useRef<{ x: number; y: number }>({\n    x: -9999,\n    y: -9999,\n  });\n  const rafRef = React.useRef<number | null>(null);\n  const lastFrameRef = React.useRef<number>(0);\n  const pointerInsideRef = React.useRef<boolean>(false);\n  const activityRef = React.useRef<number>(0);\n  const activityTargetRef = React.useRef<number>(0);\n\n  React.useEffect(() => {\n    let isCancelled = false;\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    const img = new Image();\n    img.crossOrigin = \"anonymous\";\n    img.src = src;\n\n    const compute = () => {\n      if (!canvas) return;\n      const dpr =\n        typeof window !== \"undefined\" ? window.devicePixelRatio || 1 : 1;\n\n      const displayWidth = width ?? img.naturalWidth;\n      const displayHeight = height ?? img.naturalHeight;\n\n      canvas.width = Math.max(1, Math.floor(displayWidth * dpr));\n      canvas.height = Math.max(1, Math.floor(displayHeight * dpr));\n      canvas.style.width = `${displayWidth}px`;\n      canvas.style.height = `${displayHeight}px`;\n\n      const ctx = canvas.getContext(\"2d\");\n      if (!ctx) return;\n      ctx.resetTransform();\n      ctx.scale(dpr, dpr);\n\n      if (backgroundColor) {\n        ctx.fillStyle = backgroundColor;\n        ctx.fillRect(0, 0, displayWidth, displayHeight);\n      } else {\n        ctx.clearRect(0, 0, displayWidth, displayHeight);\n      }\n\n      const offscreen = document.createElement(\"canvas\");\n      offscreen.width = Math.max(1, Math.floor(displayWidth));\n      offscreen.height = Math.max(1, Math.floor(displayHeight));\n      const off = offscreen.getContext(\"2d\");\n      if (!off) return;\n\n      const iw = img.naturalWidth || displayWidth;\n      const ih = img.naturalHeight || displayHeight;\n      let dw = displayWidth;\n      let dh = displayHeight;\n      let dx = 0;\n      let dy = 0;\n      if (objectFit === \"cover\") {\n        const scale = Math.max(displayWidth / iw, displayHeight / ih);\n        dw = Math.ceil(iw * scale);\n        dh = Math.ceil(ih * scale);\n        dx = Math.floor((displayWidth - dw) / 2);\n        dy = Math.floor((displayHeight - dh) / 2);\n      } else if (objectFit === \"contain\") {\n        const scale = Math.min(displayWidth / iw, displayHeight / ih);\n        dw = Math.ceil(iw * scale);\n        dh = Math.ceil(ih * scale);\n        dx = Math.floor((displayWidth - dw) / 2);\n        dy = Math.floor((displayHeight - dh) / 2);\n      } else if (objectFit === \"fill\") {\n        dw = displayWidth;\n        dh = displayHeight;\n      } else {\n        dw = iw;\n        dh = ih;\n        dx = Math.floor((displayWidth - dw) / 2);\n        dy = Math.floor((displayHeight - dh) / 2);\n      }\n      off.drawImage(img, dx, dy, dw, dh);\n\n      let imageData: ImageData;\n      try {\n        imageData = off.getImageData(0, 0, offscreen.width, offscreen.height);\n      } catch {\n        ctx.drawImage(img, 0, 0, displayWidth, displayHeight);\n        return;\n      }\n\n      const data = imageData.data;\n      const stride = offscreen.width * 4;\n      const effectiveDotSize = Math.max(1, Math.floor(cellSize * dotScale));\n      dimsRef.current = {\n        width: displayWidth,\n        height: displayHeight,\n        dot: effectiveDotSize,\n      };\n\n      const luminanceAt = (px: number, py: number) => {\n        const ix = Math.max(0, Math.min(offscreen.width - 1, px));\n        const iy = Math.max(0, Math.min(offscreen.height - 1, py));\n        const i = iy * stride + ix * 4;\n        const rr = data[i];\n        const gg = data[i + 1];\n        const bb = data[i + 2];\n        return 0.2126 * rr + 0.7152 * gg + 0.0722 * bb;\n      };\n\n      const hash2D = (ix: number, iy: number) => {\n        const s = Math.sin(ix * 12.9898 + iy * 78.233) * 43758.5453123;\n        return s - Math.floor(s);\n      };\n\n      const samples: Array<{\n        x: number;\n        y: number;\n        r: number;\n        g: number;\n        b: number;\n        a: number;\n        drop: boolean;\n        seed: number;\n      }> = [];\n\n      let tintRGB: [number, number, number] | null = null;\n      if (tintColor && tintStrength > 0) {\n        const parse = (c: string): [number, number, number] | null => {\n          if (c.startsWith(\"#\")) {\n            const hex = c.slice(1);\n            if (hex.length === 3) {\n              const r = parseInt(hex[0] + hex[0], 16);\n              const g = parseInt(hex[1] + hex[1], 16);\n              const b = parseInt(hex[2] + hex[2], 16);\n              return [r, g, b];\n            }\n            const r = parseInt(hex.slice(0, 2), 16);\n            const g = parseInt(hex.slice(2, 4), 16);\n            const b = parseInt(hex.slice(4, 6), 16);\n            return [r, g, b];\n          }\n          const m = c.match(/rgb\\((\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\)/i);\n          if (m)\n            return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];\n          return null;\n        };\n        tintRGB = parse(tintColor) as any;\n      }\n\n      for (let y = 0; y < offscreen.height; y += cellSize) {\n        const cy = Math.min(offscreen.height - 1, y + Math.floor(cellSize / 2));\n        for (let x = 0; x < offscreen.width; x += cellSize) {\n          const cx = Math.min(\n            offscreen.width - 1,\n            x + Math.floor(cellSize / 2),\n          );\n          let r = 0;\n          let g = 0;\n          let b = 0;\n          let a = 0;\n          if (!sampleAverage) {\n            const idx = cy * stride + cx * 4;\n            r = data[idx];\n            g = data[idx + 1];\n            b = data[idx + 2];\n            a = data[idx + 3] / 255;\n          } else {\n            let count = 0;\n            for (let oy = -1; oy <= 1; oy++) {\n              for (let ox = -1; ox <= 1; ox++) {\n                const sx = Math.max(0, Math.min(offscreen.width - 1, cx + ox));\n                const sy = Math.max(0, Math.min(offscreen.height - 1, cy + oy));\n                const sIdx = sy * stride + sx * 4;\n                r += data[sIdx];\n                g += data[sIdx + 1];\n                b += data[sIdx + 2];\n                a += data[sIdx + 3] / 255;\n                count++;\n              }\n            }\n            r = Math.round(r / count);\n            g = Math.round(g / count);\n            b = Math.round(b / count);\n            a = a / count;\n          }\n\n          if (grayscale) {\n            const L = Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b);\n            r = L;\n            g = L;\n            b = L;\n          } else if (tintRGB && tintStrength > 0) {\n            const k = Math.max(0, Math.min(1, tintStrength));\n            r = Math.round(r * (1 - k) + tintRGB[0] * k);\n            g = Math.round(g * (1 - k) + tintRGB[1] * k);\n            b = Math.round(b * (1 - k) + tintRGB[2] * k);\n          }\n\n          const Lc = luminanceAt(cx, cy);\n          const Lx1 = luminanceAt(cx - 1, cy);\n          const Lx2 = luminanceAt(cx + 1, cy);\n          const Ly1 = luminanceAt(cx, cy - 1);\n          const Ly2 = luminanceAt(cx, cy + 1);\n          const grad =\n            Math.abs(Lx2 - Lx1) +\n            Math.abs(Ly2 - Ly1) +\n            Math.abs(Lc - (Lx1 + Lx2 + Ly1 + Ly2) / 4);\n          const gradientNorm = Math.max(0, Math.min(1, grad / 255));\n          const dropoutProb = Math.max(\n            0,\n            Math.min(1, (1 - gradientNorm) * dropoutStrength),\n          );\n          const drop = hash2D(cx, cy) < dropoutProb;\n          const seed = hash2D(cx, cy);\n\n          samples.push({ x, y, r, g, b, a, drop, seed });\n        }\n      }\n\n      samplesRef.current = samples;\n    };\n\n    img.onload = () => {\n      if (isCancelled) return;\n      compute();\n      const canvasEl = canvasRef.current;\n      if (!canvasEl) return;\n\n      if (!interactive) {\n        const ctx = canvasEl.getContext(\"2d\");\n        const dims = dimsRef.current;\n        const samples = samplesRef.current;\n        if (!ctx || !dims || !samples) return;\n        if (backgroundColor) {\n          ctx.fillStyle = backgroundColor;\n          ctx.fillRect(0, 0, dims.width, dims.height);\n        } else {\n          ctx.clearRect(0, 0, dims.width, dims.height);\n        }\n        for (const s of samples) {\n          if (s.drop || s.a <= 0) continue;\n          ctx.globalAlpha = s.a;\n          ctx.fillStyle = `rgb(${s.r}, ${s.g}, ${s.b})`;\n          if (shape === \"circle\") {\n            const radius = dims.dot / 2;\n            ctx.beginPath();\n            ctx.arc(\n              s.x + cellSize / 2,\n              s.y + cellSize / 2,\n              radius,\n              0,\n              Math.PI * 2,\n            );\n            ctx.fill();\n          } else {\n            ctx.fillRect(\n              s.x + cellSize / 2 - dims.dot / 2,\n              s.y + cellSize / 2 - dims.dot / 2,\n              dims.dot,\n              dims.dot,\n            );\n          }\n        }\n        ctx.globalAlpha = 1;\n        return;\n      }\n\n      const onPointerMove = (e: PointerEvent) => {\n        const rect = canvasEl.getBoundingClientRect();\n        targetMouseRef.current.x = e.clientX - rect.left;\n        targetMouseRef.current.y = e.clientY - rect.top;\n        pointerInsideRef.current = true;\n        activityTargetRef.current = 1;\n      };\n      const onPointerEnter = () => {\n        pointerInsideRef.current = true;\n        activityTargetRef.current = 1;\n      };\n      const onPointerLeave = () => {\n        pointerInsideRef.current = false;\n        if (fadeOnLeave) {\n          activityTargetRef.current = 0;\n        } else {\n          targetMouseRef.current.x = -9999;\n          targetMouseRef.current.y = -9999;\n        }\n      };\n      canvasEl.addEventListener(\"pointermove\", onPointerMove);\n      canvasEl.addEventListener(\"pointerenter\", onPointerEnter);\n      canvasEl.addEventListener(\"pointerleave\", onPointerLeave);\n\n      const animate = () => {\n        const now = performance.now();\n        const minDelta = 1000 / Math.max(1, maxFps);\n        if (now - lastFrameRef.current < minDelta) {\n          rafRef.current = requestAnimationFrame(animate);\n          return;\n        }\n        lastFrameRef.current = now;\n        const ctx = canvasEl.getContext(\"2d\");\n        const dims = dimsRef.current;\n        const samples = samplesRef.current;\n        if (!ctx || !dims || !samples) {\n          rafRef.current = requestAnimationFrame(animate);\n          return;\n        }\n\n        animMouseRef.current.x =\n          animMouseRef.current.x +\n          (targetMouseRef.current.x - animMouseRef.current.x) * followSpeed;\n        animMouseRef.current.y =\n          animMouseRef.current.y +\n          (targetMouseRef.current.y - animMouseRef.current.y) * followSpeed;\n\n        if (fadeOnLeave) {\n          activityRef.current =\n            activityRef.current +\n            (activityTargetRef.current - activityRef.current) * fadeSpeed;\n        } else {\n          activityRef.current = pointerInsideRef.current ? 1 : 0;\n        }\n\n        if (backgroundColor) {\n          ctx.fillStyle = backgroundColor;\n          ctx.fillRect(0, 0, dims.width, dims.height);\n        } else {\n          ctx.clearRect(0, 0, dims.width, dims.height);\n        }\n\n        const mx = animMouseRef.current.x;\n        const my = animMouseRef.current.y;\n        const sigma = Math.max(1, distortionRadius * 0.5);\n        const t = now * 0.001 * jitterSpeed;\n        const activity = Math.max(0, Math.min(1, activityRef.current));\n\n        for (const s of samples) {\n          if (s.drop || s.a <= 0) continue;\n          let drawX = s.x + cellSize / 2;\n          let drawY = s.y + cellSize / 2;\n          const dx = drawX - mx;\n          const dy = drawY - my;\n          const dist2 = dx * dx + dy * dy;\n          const falloff = Math.exp(-dist2 / (2 * sigma * sigma));\n          const influence = falloff * activity;\n          if (influence > 0.0005) {\n            if (distortionMode === \"repel\") {\n              const dist = Math.sqrt(dist2) + 0.0001;\n              drawX += (dx / dist) * distortionStrength * influence;\n              drawY += (dy / dist) * distortionStrength * influence;\n            } else if (distortionMode === \"attract\") {\n              const dist = Math.sqrt(dist2) + 0.0001;\n              drawX -= (dx / dist) * distortionStrength * influence;\n              drawY -= (dy / dist) * distortionStrength * influence;\n            } else if (distortionMode === \"swirl\") {\n              const angle = distortionStrength * 0.05 * influence;\n              const cosA = Math.cos(angle);\n              const sinA = Math.sin(angle);\n              const rx = cosA * dx - sinA * dy;\n              const ry = sinA * dx + cosA * dy;\n              drawX = mx + rx;\n              drawY = my + ry;\n            }\n\n            if (jitterStrength > 0) {\n              const k = s.seed * 43758.5453;\n              const jx = Math.sin(t + k) * jitterStrength * influence;\n              const jy = Math.cos(t + k * 1.13) * jitterStrength * influence;\n              drawX += jx;\n              drawY += jy;\n            }\n          }\n\n          ctx.globalAlpha = s.a;\n          ctx.fillStyle = `rgb(${s.r}, ${s.g}, ${s.b})`;\n          if (shape === \"circle\") {\n            const radius = dims.dot / 2;\n            ctx.beginPath();\n            ctx.arc(drawX, drawY, radius, 0, Math.PI * 2);\n            ctx.fill();\n          } else {\n            ctx.fillRect(\n              drawX - dims.dot / 2,\n              drawY - dims.dot / 2,\n              dims.dot,\n              dims.dot,\n            );\n          }\n        }\n        ctx.globalAlpha = 1;\n\n        rafRef.current = requestAnimationFrame(animate);\n      };\n\n      if (rafRef.current) cancelAnimationFrame(rafRef.current);\n      rafRef.current = requestAnimationFrame(animate);\n\n      const cleanup = () => {\n        canvasEl.removeEventListener(\"pointermove\", onPointerMove);\n        canvasEl.removeEventListener(\"pointerenter\", onPointerEnter);\n        canvasEl.removeEventListener(\"pointerleave\", onPointerLeave);\n        if (rafRef.current) cancelAnimationFrame(rafRef.current);\n      };\n      (img as any)._cleanup = cleanup;\n    };\n\n    img.onerror = () => {\n      console.error(\"Failed to load image for PixelatedCanvas:\", src);\n    };\n\n    if (responsive) {\n      const onResize = () => {\n        if (img.complete && img.naturalWidth) {\n          compute();\n        }\n      };\n      window.addEventListener(\"resize\", onResize);\n      return () => {\n        isCancelled = true;\n        window.removeEventListener(\"resize\", onResize);\n        if ((img as any)._cleanup) (img as any)._cleanup();\n      };\n    }\n\n    return () => {\n      isCancelled = true;\n      if ((img as any)._cleanup) (img as any)._cleanup();\n    };\n  }, [\n    src,\n    width,\n    height,\n    cellSize,\n    dotScale,\n    shape,\n    backgroundColor,\n    grayscale,\n    responsive,\n    dropoutStrength,\n    interactive,\n    distortionStrength,\n    distortionRadius,\n    distortionMode,\n    followSpeed,\n    sampleAverage,\n    tintColor,\n    tintStrength,\n    maxFps,\n    objectFit,\n    jitterStrength,\n    jitterSpeed,\n    fadeOnLeave,\n    fadeSpeed,\n  ]);\n\n  return (\n    <canvas\n      ref={canvasRef}\n      className={className}\n      aria-label=\"Pixelated rendering of source image\"\n      role=\"img\"\n    />\n  );\n};\n",
      "type": "registry:ui",
      "target": "components/ui/pixelated-canvas.tsx"
    }
  ],
  "author": "Manu Arora <hi@manuarora.in>",
  "title": "Pixelated Canvas"
}