{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "globe",
	"title": "Globe",
	"type": "registry:block",
	"description": "An autorotating, interactive, and highly performant globe made using WebGL.",
	"dependencies": [
		"cobe"
	],
	"files": [
		{
			"content": "<script lang=\"ts\">\n\timport type { COBEOptions, Globe } from \"cobe\";\n\timport { Spring } from \"svelte/motion\";\n\timport { onDestroy, onMount } from \"svelte\";\n\timport { cn } from \"$UTILS$\";\n\n\tinterface Props {\n\t\tclass?: string;\n\t\tconfig?: Partial<COBEOptions>;\n\t}\n\n\tlet { class: className, config = {} }: Props = $props();\n\n\tlet size = $derived(config.width ?? 300);\n\n\tconst baseTheta = 0.2;\n\tconst thetaOffsetMin = -0.4;\n\tconst thetaOffsetMax = 0.4;\n\n\tlet canvas: HTMLCanvasElement | null = null;\n\tlet globe: Globe | null = null;\n\tlet observer: IntersectionObserver | null = null;\n\tlet createGlobePromise: Promise<typeof import(\"cobe\").default> | null = null;\n\tlet isVisible = false;\n\tlet isDragging = $state(false);\n\n\tlet frame = 0;\n\tlet autoPhi = 0;\n\tlet dragStart: { x: number; y: number; phi: number; theta: number } | null = null;\n\tlet lastPointer: { x: number; y: number; t: number } | null = null;\n\tlet releaseVelocity = { phi: 0, theta: 0 };\n\n\tconst initialPhi = $derived(config.phi ?? 0);\n\tconst initialTheta = $derived(config.theta ?? baseTheta);\n\n\tconst phiOffset = new Spring(0, {\n\t\tstiffness: 0.12,\n\t\tdamping: 0.7,\n\t\tprecision: 0.0001,\n\t});\n\n\tconst thetaOffset = new Spring(0, {\n\t\tstiffness: 0.12,\n\t\tdamping: 0.7,\n\t\tprecision: 0.0001,\n\t});\n\n\tfunction clamp(value: number, min: number, max: number) {\n\t\treturn Math.min(max, Math.max(min, value));\n\t}\n\n\tfunction handlePointerDown(event: PointerEvent) {\n\t\tdragStart = {\n\t\t\tx: event.clientX,\n\t\t\ty: event.clientY,\n\t\t\tphi: phiOffset.target,\n\t\t\ttheta: thetaOffset.target,\n\t\t};\n\t\tlastPointer = { x: event.clientX, y: event.clientY, t: performance.now() };\n\t\treleaseVelocity = { phi: 0, theta: 0 };\n\t\tisDragging = true;\n\t\tcanvas?.setPointerCapture?.(event.pointerId);\n\t}\n\n\tfunction handlePointerMove(event: PointerEvent) {\n\t\tif (!dragStart) return;\n\n\t\tconst nextPhi = dragStart.phi + (event.clientX - dragStart.x) / 300;\n\t\tconst nextTheta = clamp(\n\t\t\tdragStart.theta + (event.clientY - dragStart.y) / 1000,\n\t\t\tthetaOffsetMin,\n\t\t\tthetaOffsetMax\n\t\t);\n\n\t\tvoid phiOffset.set(nextPhi, { instant: true });\n\t\tvoid thetaOffset.set(nextTheta, { instant: true });\n\n\t\tconst now = performance.now();\n\t\tif (lastPointer) {\n\t\t\tconst dt = Math.max(now - lastPointer.t, 1);\n\t\t\tconst maxVelocity = 0.15;\n\n\t\t\treleaseVelocity = {\n\t\t\t\tphi: clamp(((event.clientX - lastPointer.x) / dt) * 0.3, -maxVelocity, maxVelocity),\n\t\t\t\ttheta: clamp(\n\t\t\t\t\t((event.clientY - lastPointer.y) / dt) * 0.08,\n\t\t\t\t\t-maxVelocity,\n\t\t\t\t\tmaxVelocity\n\t\t\t\t),\n\t\t\t};\n\t\t}\n\n\t\tlastPointer = { x: event.clientX, y: event.clientY, t: now };\n\t}\n\n\tfunction handlePointerUp(event?: PointerEvent) {\n\t\tif (!dragStart) return;\n\n\t\tisDragging = false;\n\t\tdragStart = null;\n\t\tlastPointer = null;\n\n\t\tif (event && canvas?.hasPointerCapture(event.pointerId)) {\n\t\t\tcanvas.releasePointerCapture(event.pointerId);\n\t\t}\n\n\t\tvoid phiOffset.set(phiOffset.target + releaseVelocity.phi * 18);\n\t\tvoid thetaOffset.set(\n\t\t\tclamp(thetaOffset.target + releaseVelocity.theta * 12, thetaOffsetMin, thetaOffsetMax)\n\t\t);\n\n\t\treleaseVelocity = { phi: 0, theta: 0 };\n\t}\n\n\tfunction stopAnimation() {\n\t\tif (!frame) return;\n\t\tcancelAnimationFrame(frame);\n\t\tframe = 0;\n\t}\n\n\tfunction destroyGlobe() {\n\t\tstopAnimation();\n\t\tglobe?.destroy();\n\t\tglobe = null;\n\t}\n\n\tasync function getCreateGlobe() {\n\t\tcreateGlobePromise ??= import(\"cobe\").then((module) => module.default);\n\t\treturn createGlobePromise;\n\t}\n\n\tasync function startGlobe() {\n\t\tif (!canvas) return;\n\n\t\tif (globe) {\n\t\t\tif (!frame) animate();\n\t\t\treturn;\n\t\t}\n\n\t\tconst createGlobe = await getCreateGlobe();\n\n\t\tif (!canvas || globe || !isVisible) return;\n\n\t\tglobe = createGlobe(canvas, {\n\t\t\tdevicePixelRatio: Math.min(window.devicePixelRatio, 2),\n\t\t\twidth: size,\n\t\t\theight: size,\n\t\t\tphi: 0,\n\t\t\ttheta: baseTheta,\n\t\t\tdark: 0,\n\t\t\tdiffuse: 1.7,\n\t\t\tmapSamples: 13000,\n\t\t\tmapBrightness: 6,\n\t\t\tbaseColor: [1, 1, 1],\n\t\t\tmarkerColor: [0.9, 0.4, 0.1],\n\t\t\tglowColor: [1, 1, 1],\n\t\t\tmarkers: [\n\t\t\t\t{ location: [37.78, -122.44], size: 0.026, id: \"sf\" },\n\t\t\t\t{ location: [40.71, -74.01], size: 0.026, id: \"nyc\" },\n\t\t\t],\n\t\t\tmarkerElevation: 0,\n\t\t\t...config,\n\t\t} satisfies COBEOptions);\n\n\t\tanimate();\n\t}\n\n\tlet onWindowPointerMove: ((event: PointerEvent) => void) | null = null;\n\tlet onWindowPointerUp: ((event: PointerEvent) => void) | null = null;\n\n\tfunction animate() {\n\t\tif (!globe) {\n\t\t\tframe = 0;\n\t\t\treturn;\n\t\t}\n\n\t\tif (!isDragging) {\n\t\t\tautoPhi += 0.005;\n\t\t}\n\n\t\tglobe.update({\n\t\t\tphi: initialPhi + autoPhi + phiOffset.current,\n\t\t\ttheta: initialTheta + thetaOffset.current,\n\t\t});\n\n\t\tframe = requestAnimationFrame(animate);\n\t}\n\n\tonMount(() => {\n\t\tif (!canvas) return;\n\n\t\tonWindowPointerMove = (event: PointerEvent) => handlePointerMove(event);\n\t\tonWindowPointerUp = (event: PointerEvent) => handlePointerUp(event);\n\n\t\twindow.addEventListener(\"pointermove\", onWindowPointerMove, { passive: true });\n\t\twindow.addEventListener(\"pointerup\", onWindowPointerUp, { passive: true });\n\t\twindow.addEventListener(\"pointercancel\", onWindowPointerUp, { passive: true });\n\n\t\tobserver = new IntersectionObserver(\n\t\t\t([entry]) => {\n\t\t\t\tif (!entry) return;\n\t\t\t\tisVisible = entry.isIntersecting;\n\t\t\t\tif (isVisible) {\n\t\t\t\t\tvoid startGlobe();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tstopAnimation();\n\t\t\t},\n\t\t\t{ threshold: 0.1 }\n\t\t);\n\n\t\tobserver.observe(canvas);\n\t});\n\n\tonDestroy(() => {\n\t\tobserver?.disconnect();\n\t\tobserver = null;\n\t\tisVisible = false;\n\n\t\tif (onWindowPointerMove) {\n\t\t\twindow.removeEventListener(\"pointermove\", onWindowPointerMove);\n\t\t\tonWindowPointerMove = null;\n\t\t}\n\n\t\tif (onWindowPointerUp) {\n\t\t\twindow.removeEventListener(\"pointerup\", onWindowPointerUp);\n\t\t\twindow.removeEventListener(\"pointercancel\", onWindowPointerUp);\n\t\t\tonWindowPointerUp = null;\n\t\t}\n\n\t\thandlePointerUp();\n\t\tdestroyGlobe();\n\t\tcanvas = null;\n\t});\n</script>\n\n<div\n\tclass={cn(\"globe-container absolute inset-0 mx-auto aspect-square w-full max-w-150\", className)}\n>\n\t<canvas\n\t\tbind:this={canvas}\n\t\tclass={cn(\n\t\t\t\"block size-full cursor-grab touch-none contain-[layout_paint_size] select-none\",\n\t\t\tisDragging && \"cursor-grabbing\"\n\t\t)}\n\t\tonpointerdown={handlePointerDown}\n\t></canvas>\n</div>\n",
			"type": "registry:component",
			"target": "magic/globe/globe.svelte"
		},
		{
			"content": "import Globe from \"./globe.svelte\";\nexport { Globe };\n",
			"type": "registry:file",
			"target": "magic/globe/index.ts"
		}
	]
}