{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "infinite-canvas",
  "type": "registry:ui",
  "title": "Infinite Canvas",
  "description": "Infinite draggable canvas with zoomable, repeating cards and touch/mouse controls.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["clsx", "tailwind-merge"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/infinite-canvas.tsx",
      "content": "'use client';\nimport React, {\n  useState,\n  useRef,\n  useEffect,\n  Children,\n  cloneElement,\n  ReactElement,\n  useCallback,\n  useMemo,\n} from 'react';\nimport { cn } from '@/lib/utils';\n\ninterface CardProps {\n  className?: string;\n  children: React.ReactNode;\n  x?: number;\n  y?: number;\n}\n\nexport const Card: React.FC<CardProps> = ({\n  className = '',\n  children,\n  x = 0,\n  y = 0,\n}) => {\n  return (\n    <div className={className} data-x={x} data-y={y}>\n      {children}\n    </div>\n  );\n};\n\ninterface InfiniteCanvasProps {\n  className?: string;\n  children: React.ReactNode;\n  cardWidth?: number;\n  cardHeight?: number;\n  spacing?: number;\n  showControls?: boolean;\n  showZoom?: boolean;\n  showStatus?: boolean;\n  showInstructions?: boolean;\n}\n\ninterface VisibleCard {\n  id: string;\n  x: number;\n  y: number;\n  childIndex: number;\n}\n\nconst InfiniteCanvas: React.FC<InfiniteCanvasProps> = ({\n  className = '',\n  children,\n  cardWidth = 280,\n  cardHeight = 220,\n  spacing = 30,\n  showControls = true,\n  showZoom = true,\n  showStatus = true,\n  showInstructions = true,\n}) => {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  const [zoom, setZoom] = useState(1);\n  const [isDragging, setIsDragging] = useState(false);\n  const [dragStart, setDragStart] = useState({ x: 0, y: 0 });\n  const [lastTouchDistance, setLastTouchDistance] = useState(0);\n  const [isActive, setIsActive] = useState(true);\n  const [visibleCards, setVisibleCards] = useState<VisibleCard[]>([]);\n  const canvasRef = useRef<HTMLDivElement>(null);\n  const rafRef = useRef<number | undefined>(undefined);\n\n  const cellWidth = cardWidth + spacing;\n  const cellHeight = cardHeight + spacing;\n\n  const childArray = useMemo(() => {\n    const array = Children.toArray(children);\n    return array;\n  }, [children]);\n\n  const childCount = childArray.length;\n\n  const getVisibleCards = useCallback(\n    (\n      currentPosition: { x: number; y: number },\n      currentZoom: number,\n      canvasElement: HTMLDivElement | null,\n    ): VisibleCard[] => {\n      const viewportWidth = canvasElement\n        ? canvasElement.offsetWidth\n        : window.innerWidth;\n      const viewportHeight = canvasElement\n        ? canvasElement.offsetHeight\n        : window.innerHeight;\n      const buffer = Math.ceil(3 / currentZoom);\n\n      const startCol =\n        Math.floor(\n          (-currentPosition.x / currentZoom - viewportWidth / 2) / cellWidth,\n        ) - buffer;\n      const endCol =\n        Math.ceil(\n          (-currentPosition.x / currentZoom + viewportWidth / 2) / cellWidth,\n        ) + buffer;\n      const startRow =\n        Math.floor(\n          (-currentPosition.y / currentZoom - viewportHeight / 2) / cellHeight,\n        ) - buffer;\n      const endRow =\n        Math.ceil(\n          (-currentPosition.y / currentZoom + viewportHeight / 2) / cellHeight,\n        ) + buffer;\n\n      const cards: VisibleCard[] = [];\n      for (let j = startRow; j <= endRow; j++) {\n        for (let i = startCol; i <= endCol; i++) {\n          const index = Math.abs(i * 7 + j * 13) % childCount;\n          cards.push({\n            id: `${i}-${j}`,\n            x: i * cellWidth + spacing / 2,\n            y: j * cellHeight + spacing / 2,\n            childIndex: index,\n          });\n        }\n      }\n      return cards;\n    },\n    [childCount, cellWidth, cellHeight, spacing],\n  );\n\n  useEffect(() => {\n    const updateVisibleCards = () => {\n      const cards = getVisibleCards(position, zoom, canvasRef.current);\n      setVisibleCards(cards);\n    };\n\n    updateVisibleCards();\n  }, [position, zoom, getVisibleCards]);\n\n  const handleMouseDown = useCallback(\n    (e: React.MouseEvent) => {\n      if (!isActive) return;\n      setIsDragging(true);\n      setDragStart({ x: e.clientX - position.x, y: e.clientY - position.y });\n    },\n    [isActive, position.x, position.y],\n  );\n\n  const handleMouseMove = useCallback(\n    (e: React.MouseEvent) => {\n      if (!isActive || !isDragging) return;\n\n      const newX = e.clientX - dragStart.x;\n      const newY = e.clientY - dragStart.y;\n\n      if (rafRef.current !== undefined) {\n        cancelAnimationFrame(rafRef.current);\n      }\n\n      rafRef.current = requestAnimationFrame(() => {\n        setPosition({ x: newX, y: newY });\n      });\n    },\n    [isActive, isDragging, dragStart.x, dragStart.y],\n  );\n\n  const handleMouseUp = useCallback(() => {\n    setIsDragging(false);\n  }, []);\n\n  const getTouchDistance = useCallback((touches: React.TouchList) => {\n    const dx = touches[0].clientX - touches[1].clientX;\n    const dy = touches[0].clientY - touches[1].clientY;\n    return Math.sqrt(dx * dx + dy * dy);\n  }, []);\n\n  const handleTouchStart = useCallback(\n    (e: React.TouchEvent) => {\n      if (!isActive) return;\n      if (e.touches.length === 2) {\n        setLastTouchDistance(getTouchDistance(e.touches));\n      } else if (e.touches.length === 1) {\n        setIsDragging(true);\n        setDragStart({\n          x: e.touches[0].clientX - position.x,\n          y: e.touches[0].clientY - position.y,\n        });\n      }\n    },\n    [isActive, position.x, position.y, getTouchDistance],\n  );\n\n  const handleTouchMove = useCallback(\n    (e: TouchEvent) => {\n      if (!isActive) return;\n      if (e.touches.length === 2) {\n        e.preventDefault();\n        const newDistance = getTouchDistance(\n          e.touches as unknown as React.TouchList,\n        );\n        if (lastTouchDistance > 0) {\n          const delta = newDistance - lastTouchDistance;\n          const zoomSpeed = 0.01;\n          const newZoom = Math.max(0.3, Math.min(3, zoom + delta * zoomSpeed));\n\n          if (rafRef.current !== undefined) {\n            cancelAnimationFrame(rafRef.current);\n          }\n\n          rafRef.current = requestAnimationFrame(() => {\n            setZoom(newZoom);\n          });\n        }\n        setLastTouchDistance(newDistance);\n      } else if (e.touches.length === 1 && isDragging) {\n        const newX = e.touches[0].clientX - dragStart.x;\n        const newY = e.touches[0].clientY - dragStart.y;\n\n        if (rafRef.current !== undefined) {\n          cancelAnimationFrame(rafRef.current);\n        }\n\n        rafRef.current = requestAnimationFrame(() => {\n          setPosition({ x: newX, y: newY });\n        });\n      }\n    },\n    [\n      isActive,\n      isDragging,\n      dragStart.x,\n      dragStart.y,\n      lastTouchDistance,\n      zoom,\n      getTouchDistance,\n    ],\n  );\n\n  const handleTouchEnd = useCallback(() => {\n    setIsDragging(false);\n    setLastTouchDistance(0);\n  }, []);\n\n  const handleWheel = useCallback(\n    (e: WheelEvent) => {\n      if (!isActive) return;\n      e.preventDefault();\n      const delta = e.deltaY;\n      const zoomSpeed = 0.001;\n      const newZoom = Math.max(0.3, Math.min(3, zoom - delta * zoomSpeed));\n\n      if (rafRef.current !== undefined) {\n        cancelAnimationFrame(rafRef.current);\n      }\n\n      rafRef.current = requestAnimationFrame(() => {\n        setZoom(newZoom);\n      });\n    },\n    [isActive, zoom],\n  );\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (canvas && isActive) {\n      canvas.addEventListener('wheel', handleWheel, { passive: false });\n      canvas.addEventListener('touchmove', handleTouchMove, { passive: false });\n      return () => {\n        canvas.removeEventListener('wheel', handleWheel);\n        canvas.removeEventListener('touchmove', handleTouchMove);\n      };\n    }\n  }, [handleWheel, handleTouchMove, isActive]);\n\n  useEffect(() => {\n    if (isActive) {\n      const originalOverflow = document.body.style.overflow;\n      const originalHtmlOverflow = document.documentElement.style.overflow;\n\n      document.body.style.overflow = 'hidden';\n      document.documentElement.style.overflow = 'hidden';\n\n      return () => {\n        document.body.style.overflow = originalOverflow;\n        document.documentElement.style.overflow = originalHtmlOverflow;\n      };\n    }\n  }, [isActive]);\n\n  const transformStyle = useMemo(\n    () => ({\n      transform: `translate(${position.x}px, ${position.y}px) scale(${zoom})`,\n      position: 'absolute' as const,\n      left: '50%',\n      top: '50%',\n      width: 0,\n      height: 0,\n      willChange: 'transform',\n    }),\n    [position.x, position.y, zoom],\n  );\n\n  return (\n    <div\n      ref={canvasRef}\n      className={cn('relative overflow-hidden select-none', className)}\n      style={{\n        cursor: isActive ? (isDragging ? 'grabbing' : 'grab') : 'default',\n      }}\n      onMouseDown={handleMouseDown}\n      onMouseMove={handleMouseMove}\n      onMouseUp={handleMouseUp}\n      onMouseLeave={handleMouseUp}\n      onTouchStart={handleTouchStart}\n      onTouchEnd={handleTouchEnd}\n    >\n      <div style={transformStyle}>\n        {visibleCards.map((card) => {\n          const child = childArray[card.childIndex];\n\n          return (\n            <div\n              key={card.id}\n              className='absolute'\n              style={{\n                transform: `translate(${card.x}px, ${card.y}px)`,\n                width: `${cardWidth}px`,\n                height: `${cardHeight}px`,\n                transformOrigin: 'center center',\n              }}\n            >\n              <div className='pointer-events-none w-full h-full flex items-start justify-start'>\n                {React.isValidElement(child)\n                  ? cloneElement(\n                      child as ReactElement<{ x?: number; y?: number }>,\n                      {\n                        x: card.x,\n                        y: card.y,\n                      },\n                    )\n                  : child}\n              </div>\n            </div>\n          );\n        })}\n      </div>\n\n      {showInstructions && (\n        <div className='absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-white/10 backdrop-blur-xs dark:text-white text-black px-4 py-2 rounded-full text-xs sm:text-sm pointer-events-none whitespace-nowrap'>\n          Drag to pan • Scroll to zoom\n        </div>\n      )}\n\n      {showZoom && (\n        <div className='absolute top-4 right-4 bg-white/10 backdrop-blur-xs dark:text-white text-black px-3 py-1.5 rounded-full text-xs sm:text-sm pointer-events-none'>\n          {Math.round(zoom * 100)}%\n        </div>\n      )}\n\n      {showStatus && (\n        <div className='absolute top-4 left-4 flex items-center gap-2 bg-white/10 backdrop-blur-xs dark:text-white text-black px-3 py-1.5 rounded-full text-xs sm:text-sm pointer-events-none'>\n          <div\n            className={`w-2 h-2 rounded-full ${\n              isActive ? 'bg-green-400 animate-pulse' : 'bg-red-400'\n            }`}\n          ></div>\n        </div>\n      )}\n\n      {showControls && (\n        <button\n          onClick={() => setIsActive(!isActive)}\n          className='absolute top-4 left-1/2 transform -translate-x-1/2 bg-white/20 hover:bg-white/30 backdrop-blur-xs dark:text-white text-black px-4 py-1.5 rounded-full text-xs sm:text-sm transition-colors pointer-events-auto whitespace-nowrap'\n        >\n          {isActive ? 'Disable' : 'Enable'}\n        </button>\n      )}\n    </div>\n  );\n};\n\nexport { InfiniteCanvas };\n"
    },
    {
      "type": "registry:lib",
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}"
    }
  ]
}
