{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "icon-cloud",
  "type": "registry:ui",
  "title": "Icon Cloud",
  "description": "An interactive 3D tag cloud component",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/magicui/icon-cloud.tsx",
      "content": "\"use client\"\n\nimport React, { useEffect, useRef, useState } from \"react\"\nimport { Pause, Play } from \"lucide-react\"\nimport { renderToString } from \"react-dom/server\"\n\nimport { Button } from \"@/components/ui/button\"\n\ninterface Icon {\n  x: number\n  y: number\n  z: number\n  scale: number\n  opacity: number\n  id: number\n}\n\ninterface IconCloudProps {\n  icons?: React.ReactNode[]\n  images?: string[]\n  showControl?: boolean\n}\n\nfunction easeOutCubic(t: number): number {\n  return 1 - Math.pow(1 - t, 3)\n}\n\nexport function IconCloud({\n  icons,\n  images,\n  showControl = true,\n}: IconCloudProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const [iconPositions, setIconPositions] = useState<Icon[]>([])\n  const [isDragging, setIsDragging] = useState(false)\n  const [isPaused, setIsPaused] = useState(false)\n  const [lastMousePos, setLastMousePos] = useState({ x: 0, y: 0 })\n  const [mousePos, setMousePos] = useState({ x: 0, y: 0 })\n  const [targetRotation, setTargetRotation] = useState<{\n    x: number\n    y: number\n    startX: number\n    startY: number\n    distance: number\n    startTime: number\n    duration: number\n  } | null>(null)\n  const animationFrameRef = useRef<number>(0)\n  const rotationRef = useRef({ x: 0, y: 0 })\n  const iconCanvasesRef = useRef<HTMLCanvasElement[]>([])\n  const imagesLoadedRef = useRef<boolean[]>([])\n\n  // Pause animation if user prefers reduced motion\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n    if (mediaQuery.matches) {\n      setIsPaused(true)\n    }\n\n    const handleChange = (e: MediaQueryListEvent) => {\n      setIsPaused(e.matches)\n    }\n\n    mediaQuery.addEventListener(\"change\", handleChange)\n    return () => mediaQuery.removeEventListener(\"change\", handleChange)\n  }, [])\n\n  // Create icon canvases once when icons/images change\n  useEffect(() => {\n    if (!icons && !images) return\n\n    const items = icons ?? images ?? []\n    imagesLoadedRef.current = new Array(items.length).fill(false)\n\n    const newIconCanvases = items.map((item, index) => {\n      const offscreen = document.createElement(\"canvas\")\n      offscreen.width = 40\n      offscreen.height = 40\n      const offCtx = offscreen.getContext(\"2d\")\n\n      if (offCtx) {\n        if (images) {\n          // Handle image URLs directly\n          const img = new Image()\n          img.crossOrigin = \"anonymous\"\n          img.src = items[index] as string\n          img.onload = () => {\n            offCtx.clearRect(0, 0, offscreen.width, offscreen.height)\n\n            // Create circular clipping path\n            offCtx.beginPath()\n            offCtx.arc(20, 20, 20, 0, Math.PI * 2)\n            offCtx.closePath()\n            offCtx.clip()\n\n            // Draw the image\n            offCtx.drawImage(img, 0, 0, 40, 40)\n\n            imagesLoadedRef.current[index] = true\n          }\n        } else {\n          // Handle SVG icons\n          offCtx.scale(0.4, 0.4)\n          const svgString = renderToString(item as React.ReactElement)\n          const img = new Image()\n          img.src = \"data:image/svg+xml;base64,\" + btoa(svgString)\n          img.onload = () => {\n            offCtx.clearRect(0, 0, offscreen.width, offscreen.height)\n            offCtx.drawImage(img, 0, 0)\n            imagesLoadedRef.current[index] = true\n          }\n        }\n      }\n      return offscreen\n    })\n\n    iconCanvasesRef.current = newIconCanvases\n  }, [icons, images])\n\n  // Generate initial icon positions on a sphere\n  useEffect(() => {\n    const items = icons ?? images ?? []\n    const newIcons: Icon[] = []\n    const numIcons = items.length || 20\n\n    // Fibonacci sphere parameters\n    const offset = 2 / numIcons\n    const increment = Math.PI * (3 - Math.sqrt(5))\n\n    for (let i = 0; i < numIcons; i++) {\n      const y = i * offset - 1 + offset / 2\n      const r = Math.sqrt(1 - y * y)\n      const phi = i * increment\n\n      const x = Math.cos(phi) * r\n      const z = Math.sin(phi) * r\n\n      newIcons.push({\n        x: x * 100,\n        y: y * 100,\n        z: z * 100,\n        scale: 1,\n        opacity: 1,\n        id: i,\n      })\n    }\n    setIconPositions(newIcons)\n  }, [icons, images])\n\n  // Handle mouse events\n  const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {\n    const rect = canvasRef.current?.getBoundingClientRect()\n    if (!rect || !canvasRef.current) return\n\n    const x = e.clientX - rect.left\n    const y = e.clientY - rect.top\n\n    const ctx = canvasRef.current.getContext(\"2d\")\n    if (!ctx) return\n\n    iconPositions.forEach((icon) => {\n      const cosX = Math.cos(rotationRef.current.x)\n      const sinX = Math.sin(rotationRef.current.x)\n      const cosY = Math.cos(rotationRef.current.y)\n      const sinY = Math.sin(rotationRef.current.y)\n\n      const rotatedX = icon.x * cosY - icon.z * sinY\n      const rotatedZ = icon.x * sinY + icon.z * cosY\n      const rotatedY = icon.y * cosX + rotatedZ * sinX\n\n      const screenX = canvasRef.current!.width / 2 + rotatedX\n      const screenY = canvasRef.current!.height / 2 + rotatedY\n\n      const scale = (rotatedZ + 200) / 300\n      const radius = 20 * scale\n      const dx = x - screenX\n      const dy = y - screenY\n\n      if (dx * dx + dy * dy < radius * radius) {\n        const targetX = -Math.atan2(\n          icon.y,\n          Math.sqrt(icon.x * icon.x + icon.z * icon.z)\n        )\n        const targetY = Math.atan2(icon.x, icon.z)\n\n        const currentX = rotationRef.current.x\n        const currentY = rotationRef.current.y\n        const distance = Math.sqrt(\n          Math.pow(targetX - currentX, 2) + Math.pow(targetY - currentY, 2)\n        )\n\n        const duration = Math.min(2000, Math.max(800, distance * 1000))\n\n        setTargetRotation({\n          x: targetX,\n          y: targetY,\n          startX: currentX,\n          startY: currentY,\n          distance,\n          startTime: performance.now(),\n          duration,\n        })\n        return\n      }\n    })\n\n    setIsDragging(true)\n    setLastMousePos({ x: e.clientX, y: e.clientY })\n  }\n\n  const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {\n    const rect = canvasRef.current?.getBoundingClientRect()\n    if (rect) {\n      const x = e.clientX - rect.left\n      const y = e.clientY - rect.top\n      setMousePos({ x, y })\n    }\n\n    if (isDragging) {\n      const deltaX = e.clientX - lastMousePos.x\n      const deltaY = e.clientY - lastMousePos.y\n\n      rotationRef.current = {\n        x: rotationRef.current.x + deltaY * 0.002,\n        y: rotationRef.current.y + deltaX * 0.002,\n      }\n\n      setLastMousePos({ x: e.clientX, y: e.clientY })\n    }\n  }\n\n  const handleMouseUp = () => {\n    setIsDragging(false)\n  }\n\n  // Animation and rendering\n  useEffect(() => {\n    const canvas = canvasRef.current\n    const ctx = canvas?.getContext(\"2d\")\n    if (canvas && ctx) {\n      const animate = () => {\n        ctx.clearRect(0, 0, canvas.width, canvas.height)\n\n        const centerX = canvas.width / 2\n        const centerY = canvas.height / 2\n        const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY)\n        const dx = mousePos.x - centerX\n        const dy = mousePos.y - centerY\n        const distance = Math.sqrt(dx * dx + dy * dy)\n        const speed = 0.003 + (distance / maxDistance) * 0.01\n\n        if (targetRotation) {\n          const elapsed = performance.now() - targetRotation.startTime\n          const progress = Math.min(1, elapsed / targetRotation.duration)\n          const easedProgress = easeOutCubic(progress)\n\n          rotationRef.current = {\n            x:\n              targetRotation.startX +\n              (targetRotation.x - targetRotation.startX) * easedProgress,\n            y:\n              targetRotation.startY +\n              (targetRotation.y - targetRotation.startY) * easedProgress,\n          }\n\n          if (progress >= 1) {\n            setTargetRotation(null)\n          }\n        } else if (!isDragging && !isPaused) {\n          rotationRef.current = {\n            x: rotationRef.current.x + (dy / canvas.height) * speed,\n            y: rotationRef.current.y + (dx / canvas.width) * speed,\n          }\n        }\n\n        iconPositions.forEach((icon, index) => {\n          const cosX = Math.cos(rotationRef.current.x)\n          const sinX = Math.sin(rotationRef.current.x)\n          const cosY = Math.cos(rotationRef.current.y)\n          const sinY = Math.sin(rotationRef.current.y)\n\n          const rotatedX = icon.x * cosY - icon.z * sinY\n          const rotatedZ = icon.x * sinY + icon.z * cosY\n          const rotatedY = icon.y * cosX + rotatedZ * sinX\n\n          const scale = (rotatedZ + 200) / 300\n          const opacity = Math.max(0.2, Math.min(1, (rotatedZ + 150) / 200))\n\n          ctx.save()\n          ctx.translate(\n            canvas.width / 2 + rotatedX,\n            canvas.height / 2 + rotatedY\n          )\n          ctx.scale(scale, scale)\n          ctx.globalAlpha = opacity\n\n          if (icons || images) {\n            // Only try to render icons/images if they exist\n            if (\n              iconCanvasesRef.current[index] &&\n              imagesLoadedRef.current[index]\n            ) {\n              ctx.drawImage(iconCanvasesRef.current[index], -20, -20, 40, 40)\n            }\n          } else {\n            // Show numbered circles if no icons/images are provided\n            ctx.beginPath()\n            ctx.arc(0, 0, 20, 0, Math.PI * 2)\n            ctx.fillStyle = \"#4444ff\"\n            ctx.fill()\n            ctx.fillStyle = \"white\"\n            ctx.textAlign = \"center\"\n            ctx.textBaseline = \"middle\"\n            ctx.font = \"16px Arial\"\n            ctx.fillText(`${icon.id + 1}`, 0, 0)\n          }\n\n          ctx.restore()\n        })\n\n        const hasPendingAssets =\n          Boolean(icons || images) &&\n          !imagesLoadedRef.current.every((loaded) => loaded)\n        const shouldContinue =\n          !isPaused || isDragging || targetRotation !== null || hasPendingAssets\n\n        if (shouldContinue) {\n          animationFrameRef.current = requestAnimationFrame(animate)\n        }\n      }\n\n      animate()\n    }\n\n    return () => {\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current)\n      }\n    }\n  }, [\n    icons,\n    images,\n    iconPositions,\n    isDragging,\n    isPaused,\n    mousePos,\n    targetRotation,\n  ])\n\n  return (\n    <div className=\"relative inline-block\">\n      <canvas\n        ref={canvasRef}\n        width={400}\n        height={400}\n        onMouseDown={handleMouseDown}\n        onMouseMove={handleMouseMove}\n        onMouseUp={handleMouseUp}\n        onMouseLeave={handleMouseUp}\n        className=\"rounded-lg\"\n        aria-label=\"Interactive 3D Icon Cloud\"\n        role=\"img\"\n      />\n      {showControl && (\n        <Button\n          variant=\"outline\"\n          size=\"icon\"\n          onClick={() => setIsPaused(!isPaused)}\n          aria-label={isPaused ? \"Play Animation\" : \"Pause Animation\"}\n          className=\"absolute top-2 right-2\"\n        >\n          {isPaused ? <Play size={16} /> : <Pause size={16} />}\n        </Button>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ]
}