{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "infinite-image-field",
  "type": "registry:ui",
  "title": "Infinite Image Field",
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "An endless, cursor-driven photo canvas. Images tile infinitely in all directions — move your cursor to pan the field, and it glides fluidly toward where you point.",
  "files": [
    {
      "path": "components/ui/infinite-image-field.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useCallback, useEffect, useRef } from \"react\";\n\nexport const INFINITE_IMAGE_FIELD_IMAGES: string[] = [\n  \"https://plus.unsplash.com/premium_photo-1665311515452-a9f54c4266c9?w=400&h=560&fit=crop&q=80\",\n  \"https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=560&fit=crop&q=80\",\n  \"https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=400&h=560&fit=crop&q=80\",\n  \"https://images.unsplash.com/photo-1433086966358-54859d0ed716?w=400&h=560&fit=crop&q=80\",\n  \"https://images.unsplash.com/photo-1501854140801-50d01698950b?w=400&h=560&fit=crop&q=80\",\n  \"https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=400&h=560&fit=crop&q=80\",\n  \"https://images.unsplash.com/photo-1500534314209-a25ddb2bd429?w=400&h=560&fit=crop&q=80\",\n  \"https://images.unsplash.com/photo-1440342359743-84fcb8c21f21?w=400&h=560&fit=crop&q=80\",\n  \"https://images.unsplash.com/photo-1511884642898-4c92249e20b6?w=400&h=560&fit=crop&q=80\",\n  \"https://images.unsplash.com/photo-1519681393784-d120267933ba?w=400&h=560&fit=crop&q=80\",\n  \"https://images.unsplash.com/photo-1448375240586-882707db888b?w=400&h=560&fit=crop&q=80\",\n  \"https://images.unsplash.com/photo-1542273917363-3b1817f69a2d?w=400&h=560&fit=crop&q=80\",\n];\n\nexport interface InfiniteImageFieldProps\n  extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  className?: string;\n  images?: string[];\n  imageWidth?: number;\n  imageHeight?: number;\n  gap?: number;\n  maxSpeed?: number;\n  smoothing?: number;\n  borderRadius?: number;\n}\n\nfunction drawRoundedRect(\n  ctx: CanvasRenderingContext2D,\n  x: number,\n  y: number,\n  w: number,\n  h: number,\n  r: number\n) {\n  const clampedR = Math.min(r, w / 2, h / 2);\n  ctx.beginPath();\n  ctx.moveTo(x + clampedR, y);\n  ctx.lineTo(x + w - clampedR, y);\n  ctx.quadraticCurveTo(x + w, y, x + w, y + clampedR);\n  ctx.lineTo(x + w, y + h - clampedR);\n  ctx.quadraticCurveTo(x + w, y + h, x + w - clampedR, y + h);\n  ctx.lineTo(x + clampedR, y + h);\n  ctx.quadraticCurveTo(x, y + h, x, y + h - clampedR);\n  ctx.lineTo(x, y + clampedR);\n  ctx.quadraticCurveTo(x, y, x + clampedR, y);\n  ctx.closePath();\n}\n\nexport function InfiniteImageField({\n  className,\n  images = INFINITE_IMAGE_FIELD_IMAGES,\n  imageWidth = 200,\n  imageHeight = 280,\n  gap = 28,\n  maxSpeed = 5,\n  smoothing = 0.07,\n  borderRadius = 0,\n  ...rest\n}: InfiniteImageFieldProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const loadedImagesRef = useRef<HTMLImageElement[]>([]);\n  const dimsRef = useRef({ w: 0, h: 0 });\n  const camRef = useRef({ x: 0, y: 0 });\n  const velRef = useRef({ x: 0, y: 0 });\n  const mouseRef = useRef({ x: 0.5, y: 0.5 });\n  const isInsideRef = useRef(false);\n  const rafRef = useRef<number>(0);\n\n  // Pre-load images\n  useEffect(() => {\n    const imgs = images.map((src) => {\n      const img = new Image();\n      img.crossOrigin = \"anonymous\";\n      img.src = src;\n      return img;\n    });\n    loadedImagesRef.current = imgs;\n  }, [images]);\n\n  const draw = useCallback(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const { w: W, h: H } = dimsRef.current;\n    if (W === 0 || H === 0) {\n      rafRef.current = requestAnimationFrame(draw);\n      return;\n    }\n\n    const dpr = window.devicePixelRatio || 1;\n    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n\n    const cellW = imageWidth + gap;\n    const cellH = imageHeight + gap;\n    const imgs = loadedImagesRef.current;\n    const numImages = imgs.length;\n\n    // Physics — cursor offset from center drives velocity\n    const tx = isInsideRef.current\n      ? (mouseRef.current.x - 0.5) * 2 * maxSpeed\n      : 0;\n    const ty = isInsideRef.current\n      ? (mouseRef.current.y - 0.5) * 2 * maxSpeed\n      : 0;\n\n    velRef.current.x += (tx - velRef.current.x) * smoothing;\n    velRef.current.y += (ty - velRef.current.y) * smoothing;\n\n    camRef.current.x += velRef.current.x;\n    camRef.current.y += velRef.current.y;\n\n    const camX = camRef.current.x;\n    const camY = camRef.current.y;\n\n    ctx.clearRect(0, 0, W, H);\n\n    // Compute visible cell range\n    const colMin = Math.floor((camX - W / 2) / cellW) - 1;\n    const colMax = Math.ceil((camX + W / 2) / cellW) + 1;\n    const rowMin = Math.floor((camY - H / 2) / cellH) - 1;\n    const rowMax = Math.ceil((camY + H / 2) / cellH) + 1;\n\n    for (let row = rowMin; row <= rowMax; row++) {\n      for (let col = colMin; col <= colMax; col++) {\n        // Top-left corner in screen space\n        const sx = col * cellW - camX + W / 2 - imageWidth / 2;\n        const sy = row * cellH - camY + H / 2 - imageHeight / 2;\n\n        // Deterministic image assignment — same cell always gets same image\n        const imgIdx =\n          Math.abs(col * 7 + row * 13 + ((col * row * 3) | 0)) % numImages;\n        const img = imgs[imgIdx];\n\n        ctx.save();\n        drawRoundedRect(ctx, sx, sy, imageWidth, imageHeight, borderRadius);\n        ctx.clip();\n\n        if (img && img.complete && img.naturalWidth > 0) {\n          ctx.drawImage(img, sx, sy, imageWidth, imageHeight);\n        } else {\n          // Placeholder while loading — semi-transparent so background shows\n          ctx.fillStyle = \"rgba(0,0,0,0.15)\";\n          ctx.fillRect(sx, sy, imageWidth, imageHeight);\n        }\n        ctx.restore();\n\n        // Subtle border overlay for glass-panel effect\n        ctx.save();\n        drawRoundedRect(ctx, sx, sy, imageWidth, imageHeight, borderRadius);\n        ctx.strokeStyle = \"rgba(255,255,255,0.06)\";\n        ctx.lineWidth = 1;\n        ctx.stroke();\n        ctx.restore();\n      }\n    }\n\n    rafRef.current = requestAnimationFrame(draw);\n  }, [imageWidth, imageHeight, gap, maxSpeed, smoothing, borderRadius]);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    const resize = () => {\n      const dpr = window.devicePixelRatio || 1;\n      const rect = canvas.getBoundingClientRect();\n      dimsRef.current = { w: rect.width, h: rect.height };\n      canvas.width = rect.width * dpr;\n      canvas.height = rect.height * dpr;\n    };\n\n    resize();\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(canvas);\n\n    const onMove = (e: MouseEvent) => {\n      const rect = canvas.getBoundingClientRect();\n      mouseRef.current = {\n        x: (e.clientX - rect.left) / rect.width,\n        y: (e.clientY - rect.top) / rect.height,\n      };\n    };\n\n    const onEnter = () => {\n      isInsideRef.current = true;\n    };\n    const onLeave = () => {\n      isInsideRef.current = false;\n    };\n\n    canvas.addEventListener(\"mousemove\", onMove);\n    canvas.addEventListener(\"mouseenter\", onEnter);\n    canvas.addEventListener(\"mouseleave\", onLeave);\n\n    rafRef.current = requestAnimationFrame(draw);\n\n    return () => {\n      cancelAnimationFrame(rafRef.current);\n      ro.disconnect();\n      canvas.removeEventListener(\"mousemove\", onMove);\n      canvas.removeEventListener(\"mouseenter\", onEnter);\n      canvas.removeEventListener(\"mouseleave\", onLeave);\n    };\n  }, [draw]);\n\n  return (\n    <div\n      {...rest}\n      className={cn(\"relative w-full h-full overflow-hidden\", className)}\n    >\n      <canvas ref={canvasRef} className=\"block w-full h-full bg-transparent\" />\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ]
}
