{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "icon-cloud",
	"title": "Icon Cloud",
	"type": "registry:block",
	"description": "An interactive 3D tag cloud component that displays icons or images in a sphere formation with smooth rotation and drag interactions.",
	"files": [
		{
			"content": "<script lang=\"ts\">\n\timport { onMount } from \"svelte\";\n\timport type { Component } from \"svelte\";\n\n\tinterface Icon {\n\t\tx: number;\n\t\ty: number;\n\t\tz: number;\n\t\tscale: number;\n\t\topacity: number;\n\t\tid: number;\n\t}\n\n\tinterface IconCloudProps {\n\t\ticons?: Component[];\n\t\timages?: string[];\n\t\tclass?: string;\n\t}\n\n\tlet { icons, images, class: className = \"\" }: IconCloudProps = $props();\n\n\tlet canvasRef: HTMLCanvasElement | null = $state(null);\n\tlet iconPositions = $state<Icon[]>([]);\n\tlet isDragging = $state(false);\n\tlet lastMousePos = $state({ x: 0, y: 0 });\n\tlet mousePos = $state({ x: 0, y: 0 });\n\tlet targetRotation = $state<{\n\t\tx: number;\n\t\ty: number;\n\t\tstartX: number;\n\t\tstartY: number;\n\t\tdistance: number;\n\t\tstartTime: number;\n\t\tduration: number;\n\t} | null>(null);\n\n\tlet animationFrameId = 0;\n\tlet rotationRef = { x: 0, y: 0 };\n\tlet iconCanvases: HTMLCanvasElement[] = [];\n\tlet imagesLoaded: boolean[] = [];\n\n\tfunction easeOutCubic(t: number): number {\n\t\treturn 1 - Math.pow(1 - t, 3);\n\t}\n\n\t// Create icon canvases\n\t$effect(() => {\n\t\tif (!icons && !images) return;\n\n\t\tconst items = icons || images || [];\n\t\timagesLoaded = new Array(items.length).fill(false);\n\n\t\tconst newIconCanvases = items.map((_item, index) => {\n\t\t\tconst offscreen = document.createElement(\"canvas\");\n\t\t\toffscreen.width = 40;\n\t\t\toffscreen.height = 40;\n\t\t\tconst offCtx = offscreen.getContext(\"2d\");\n\n\t\t\tif (offCtx) {\n\t\t\t\tif (images) {\n\t\t\t\t\tconst img = new Image();\n\t\t\t\t\timg.crossOrigin = \"anonymous\";\n\t\t\t\t\timg.src = items[index] as string;\n\t\t\t\t\timg.onload = () => {\n\t\t\t\t\t\toffCtx.clearRect(0, 0, offscreen.width, offscreen.height);\n\t\t\t\t\t\toffCtx.beginPath();\n\t\t\t\t\t\toffCtx.arc(20, 20, 20, 0, Math.PI * 2);\n\t\t\t\t\t\toffCtx.closePath();\n\t\t\t\t\t\toffCtx.clip();\n\t\t\t\t\t\toffCtx.drawImage(img, 0, 0, 40, 40);\n\t\t\t\t\t\timagesLoaded[index] = true;\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn offscreen;\n\t\t});\n\n\t\ticonCanvases = newIconCanvases;\n\t});\n\n\t// Generate initial icon positions on a sphere\n\t$effect(() => {\n\t\tconst items = icons || images || [];\n\t\tconst newIcons: Icon[] = [];\n\t\tconst numIcons = items.length || 20;\n\n\t\tconst offset = 2 / numIcons;\n\t\tconst increment = Math.PI * (3 - Math.sqrt(5));\n\n\t\tfor (let i = 0; i < numIcons; i++) {\n\t\t\tconst y = i * offset - 1 + offset / 2;\n\t\t\tconst r = Math.sqrt(1 - y * y);\n\t\t\tconst phi = i * increment;\n\n\t\t\tconst x = Math.cos(phi) * r;\n\t\t\tconst z = Math.sin(phi) * r;\n\n\t\t\tnewIcons.push({\n\t\t\t\tx: x * 100,\n\t\t\t\ty: y * 100,\n\t\t\t\tz: z * 100,\n\t\t\t\tscale: 1,\n\t\t\t\topacity: 1,\n\t\t\t\tid: i,\n\t\t\t});\n\t\t}\n\t\ticonPositions = newIcons;\n\t});\n\n\tfunction handleMouseDown(e: MouseEvent) {\n\t\tconst rect = canvasRef?.getBoundingClientRect();\n\t\tif (!rect || !canvasRef) return;\n\n\t\tconst x = e.clientX - rect.left;\n\t\tconst y = e.clientY - rect.top;\n\n\t\tconst ctx = canvasRef.getContext(\"2d\");\n\t\tif (!ctx) return;\n\n\t\tfor (const icon of iconPositions) {\n\t\t\tconst cosX = Math.cos(rotationRef.x);\n\t\t\tconst sinX = Math.sin(rotationRef.x);\n\t\t\tconst cosY = Math.cos(rotationRef.y);\n\t\t\tconst sinY = Math.sin(rotationRef.y);\n\n\t\t\tconst rotatedX = icon.x * cosY - icon.z * sinY;\n\t\t\tconst rotatedZ = icon.x * sinY + icon.z * cosY;\n\t\t\tconst rotatedY = icon.y * cosX + rotatedZ * sinX;\n\n\t\t\tconst screenX = canvasRef.width / 2 + rotatedX;\n\t\t\tconst screenY = canvasRef.height / 2 + rotatedY;\n\n\t\t\tconst scale = (rotatedZ + 200) / 300;\n\t\t\tconst radius = 20 * scale;\n\t\t\tconst dx = x - screenX;\n\t\t\tconst dy = y - screenY;\n\n\t\t\tif (dx * dx + dy * dy < radius * radius) {\n\t\t\t\tconst targetX = -Math.atan2(icon.y, Math.sqrt(icon.x * icon.x + icon.z * icon.z));\n\t\t\t\tconst targetY = Math.atan2(icon.x, icon.z);\n\n\t\t\t\tconst currentX = rotationRef.x;\n\t\t\t\tconst currentY = rotationRef.y;\n\t\t\t\tconst distance = Math.sqrt(\n\t\t\t\t\tMath.pow(targetX - currentX, 2) + Math.pow(targetY - currentY, 2)\n\t\t\t\t);\n\n\t\t\t\tconst duration = Math.min(2000, Math.max(800, distance * 1000));\n\n\t\t\t\ttargetRotation = {\n\t\t\t\t\tx: targetX,\n\t\t\t\t\ty: targetY,\n\t\t\t\t\tstartX: currentX,\n\t\t\t\t\tstartY: currentY,\n\t\t\t\t\tdistance,\n\t\t\t\t\tstartTime: performance.now(),\n\t\t\t\t\tduration,\n\t\t\t\t};\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\tisDragging = true;\n\t\tlastMousePos = { x: e.clientX, y: e.clientY };\n\t}\n\n\tfunction handleMouseMove(e: MouseEvent) {\n\t\tconst rect = canvasRef?.getBoundingClientRect();\n\t\tif (rect) {\n\t\t\tconst x = e.clientX - rect.left;\n\t\t\tconst y = e.clientY - rect.top;\n\t\t\tmousePos = { x, y };\n\t\t}\n\n\t\tif (isDragging) {\n\t\t\tconst deltaX = e.clientX - lastMousePos.x;\n\t\t\tconst deltaY = e.clientY - lastMousePos.y;\n\n\t\t\trotationRef = {\n\t\t\t\tx: rotationRef.x + deltaY * 0.002,\n\t\t\t\ty: rotationRef.y + deltaX * 0.002,\n\t\t\t};\n\n\t\t\tlastMousePos = { x: e.clientX, y: e.clientY };\n\t\t}\n\t}\n\n\tfunction handleMouseUp() {\n\t\tisDragging = false;\n\t}\n\n\t// Animation and rendering\n\tonMount(() => {\n\t\tconst canvas = canvasRef;\n\t\tconst ctx = canvas?.getContext(\"2d\");\n\t\tif (!canvas || !ctx) return;\n\n\t\tconst animate = () => {\n\t\t\tctx.clearRect(0, 0, canvas.width, canvas.height);\n\n\t\t\tconst centerX = canvas.width / 2;\n\t\t\tconst centerY = canvas.height / 2;\n\t\t\tconst maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);\n\t\t\tconst dx = mousePos.x - centerX;\n\t\t\tconst dy = mousePos.y - centerY;\n\t\t\tconst distance = Math.sqrt(dx * dx + dy * dy);\n\t\t\tconst speed = 0.003 + (distance / maxDistance) * 0.01;\n\n\t\t\tif (targetRotation) {\n\t\t\t\tconst elapsed = performance.now() - targetRotation.startTime;\n\t\t\t\tconst progress = Math.min(1, elapsed / targetRotation.duration);\n\t\t\t\tconst easedProgress = easeOutCubic(progress);\n\n\t\t\t\trotationRef = {\n\t\t\t\t\tx:\n\t\t\t\t\t\ttargetRotation.startX +\n\t\t\t\t\t\t(targetRotation.x - targetRotation.startX) * easedProgress,\n\t\t\t\t\ty:\n\t\t\t\t\t\ttargetRotation.startY +\n\t\t\t\t\t\t(targetRotation.y - targetRotation.startY) * easedProgress,\n\t\t\t\t};\n\n\t\t\t\tif (progress >= 1) {\n\t\t\t\t\ttargetRotation = null;\n\t\t\t\t}\n\t\t\t} else if (!isDragging) {\n\t\t\t\trotationRef = {\n\t\t\t\t\tx: rotationRef.x + (dy / canvas.height) * speed,\n\t\t\t\t\ty: rotationRef.y + (dx / canvas.width) * speed,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\ticonPositions.forEach((icon, index) => {\n\t\t\t\tconst cosX = Math.cos(rotationRef.x);\n\t\t\t\tconst sinX = Math.sin(rotationRef.x);\n\t\t\t\tconst cosY = Math.cos(rotationRef.y);\n\t\t\t\tconst sinY = Math.sin(rotationRef.y);\n\n\t\t\t\tconst rotatedX = icon.x * cosY - icon.z * sinY;\n\t\t\t\tconst rotatedZ = icon.x * sinY + icon.z * cosY;\n\t\t\t\tconst rotatedY = icon.y * cosX + rotatedZ * sinX;\n\n\t\t\t\tconst scale = (rotatedZ + 200) / 300;\n\t\t\t\tconst opacity = Math.max(0.2, Math.min(1, (rotatedZ + 150) / 200));\n\n\t\t\t\tctx.save();\n\t\t\t\tctx.translate(canvas.width / 2 + rotatedX, canvas.height / 2 + rotatedY);\n\t\t\t\tctx.scale(scale, scale);\n\t\t\t\tctx.globalAlpha = opacity;\n\n\t\t\t\tif (icons || images) {\n\t\t\t\t\tif (iconCanvases[index] && imagesLoaded[index]) {\n\t\t\t\t\t\tctx.drawImage(iconCanvases[index], -20, -20, 40, 40);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tctx.beginPath();\n\t\t\t\t\tctx.arc(0, 0, 20, 0, Math.PI * 2);\n\t\t\t\t\tctx.fillStyle = \"#4444ff\";\n\t\t\t\t\tctx.fill();\n\t\t\t\t\tctx.fillStyle = \"white\";\n\t\t\t\t\tctx.textAlign = \"center\";\n\t\t\t\t\tctx.textBaseline = \"middle\";\n\t\t\t\t\tctx.font = \"16px Arial\";\n\t\t\t\t\tctx.fillText(`${icon.id + 1}`, 0, 0);\n\t\t\t\t}\n\n\t\t\t\tctx.restore();\n\t\t\t});\n\n\t\t\tanimationFrameId = requestAnimationFrame(animate);\n\t\t};\n\n\t\tanimate();\n\n\t\treturn () => {\n\t\t\tif (animationFrameId) {\n\t\t\t\tcancelAnimationFrame(animationFrameId);\n\t\t\t}\n\t\t};\n\t});\n</script>\n\n<canvas\n\tbind:this={canvasRef}\n\twidth={400}\n\theight={400}\n\tonmousedown={handleMouseDown}\n\tonmousemove={handleMouseMove}\n\tonmouseup={handleMouseUp}\n\tonmouseleave={handleMouseUp}\n\tclass={`rounded-lg ${className}`}\n\taria-label=\"Interactive 3D Icon Cloud\"\n></canvas>\n",
			"type": "registry:component",
			"target": "magic/icon-cloud/icon-cloud.svelte"
		},
		{
			"content": "export { default as IconCloud } from \"./icon-cloud.svelte\";\n",
			"type": "registry:file",
			"target": "magic/icon-cloud/index.ts"
		}
	]
}