{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "globe",
  "type": "registry:ui",
  "title": "Globe",
  "description": "The Globe component creates beautiful 5kb WebGL Globe",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["@react-spring/web", "cobe"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/globe.tsx",
      "content": "'use client';\nimport { useEffect, useRef, useState } from 'react';\nimport { useSpring } from '@react-spring/web';\nimport createGlobe from 'cobe';\n\ninterface Marker {\n  location: [number, number];\n  size: number;\n}\n\ninterface GlobeProps {\n  baseColor?: [number, number, number];\n  markerColor?: [number, number, number];\n  glowColor?: [number, number, number];\n  markers?: Marker[];\n  scale?: number;\n  className?: string;\n  rotateToLocation?: string | [number, number];\n  autoRotate?: boolean;\n  rotateCities?: string[];\n  rotationSpeed?: number;\n}\n\nconst cityCoordinates: Record<string, [number, number]> = {\n  'san francisco': [37.7749, -122.4194],\n  'new york': [40.7128, -74.006],\n  london: [51.5074, -0.1278],\n  tokyo: [35.6762, 139.6503],\n  paris: [48.8566, 2.3522],\n  moscow: [55.7558, 37.6176],\n  dubai: [25.2048, 55.2708],\n  singapore: [1.3521, 103.8198],\n};\n\nconst locationToAngles = (lat: number, long: number): [number, number] => [\n  Math.PI - ((long * Math.PI) / 180 - Math.PI / 2),\n  (lat * Math.PI) / 180,\n];\n\ninterface GlobeRenderer {\n  destroy: () => void;\n}\n\nexport default function Globe({\n  baseColor = [0.3, 0.3, 0.3],\n  markerColor = [0.1, 0.8, 1],\n  glowColor = [1, 1, 1],\n  markers = [\n    { location: [37.7595, -122.4367], size: 0.03 },\n    { location: [40.7128, -74.006], size: 0.1 },\n  ],\n  scale = 1,\n  className = 'aspect-square w-full max-w-150',\n  rotateToLocation,\n  autoRotate = true,\n  rotateCities = [],\n  rotationSpeed = 3000,\n}: GlobeProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const pointerInteracting = useRef<number | null>(null);\n  const pointerInteractionMovement = useRef(0);\n  const focusRef = useRef<[number, number] | null>(null);\n  const phiRef = useRef(0);\n  const rotationInterval = useRef<NodeJS.Timeout | null>(null);\n  const [currentCityIndex, setCurrentCityIndex] = useState(0);\n  const globeRef = useRef<GlobeRenderer | null>(null);\n  const [isVisible, setIsVisible] = useState(false);\n\n  const [{ r }, api] = useSpring(() => ({\n    r: 0,\n    config: {\n      mass: 1,\n      tension: 280,\n      friction: 40,\n      precision: 0.001,\n    },\n  }));\n\n  useEffect(() => {\n    if (!containerRef.current) return;\n\n    const container = containerRef.current;\n\n    const observer = new IntersectionObserver(\n      (entries) => {\n        const [entry] = entries;\n        setIsVisible(entry.isIntersecting);\n      },\n      { threshold: 0.1 },\n    );\n\n    observer.observe(container);\n\n    return () => {\n      observer.unobserve(container);\n    };\n  }, []);\n\n  useEffect(() => {\n    if (rotateCities.length === 0) return;\n\n    const rotateToNextCity = () => {\n      const nextIndex = (currentCityIndex + 1) % rotateCities.length;\n      const city = rotateCities[nextIndex].toLowerCase();\n      const coordinates = cityCoordinates[city];\n\n      if (coordinates) {\n        focusRef.current = locationToAngles(...coordinates);\n        setCurrentCityIndex(nextIndex);\n      }\n    };\n\n    if (isVisible) {\n      const city = rotateCities[currentCityIndex].toLowerCase();\n      const coordinates = cityCoordinates[city];\n      if (coordinates) {\n        focusRef.current = locationToAngles(...coordinates);\n      }\n\n      rotationInterval.current = setInterval(rotateToNextCity, rotationSpeed);\n    }\n\n    return () => {\n      if (rotationInterval.current) {\n        clearInterval(rotationInterval.current);\n      }\n    };\n  }, [rotateCities, currentCityIndex, rotationSpeed, isVisible]);\n\n  useEffect(() => {\n    if (!rotateToLocation) {\n      focusRef.current = null;\n      return;\n    }\n\n    let coordinates: [number, number];\n    if (typeof rotateToLocation === 'string') {\n      const city = rotateToLocation.toLowerCase();\n      coordinates = cityCoordinates[city] || [0, 0];\n    } else {\n      coordinates = rotateToLocation;\n    }\n\n    focusRef.current = locationToAngles(...coordinates);\n  }, [rotateToLocation]);\n\n  useEffect(() => {\n    if (!isVisible || !canvasRef.current) return;\n\n    let width = canvasRef.current.offsetWidth || 300;\n    const doublePi = Math.PI * 2;\n    let currentPhi = 0;\n    let currentTheta = 0;\n    const animationFrame: number | null = null;\n\n    const onResize = () => {\n      if (canvasRef.current) {\n        width = canvasRef.current.offsetWidth || 300;\n      }\n    };\n\n    window.addEventListener('resize', onResize);\n\n    try {\n      globeRef.current = createGlobe(canvasRef.current, {\n        devicePixelRatio: 2,\n        width: width * 2,\n        height: width * 2,\n        phi: 0,\n        theta: 0,\n        dark: 1,\n        diffuse: 1.2,\n        mapSamples: 16000,\n        mapBrightness: 6,\n        baseColor: baseColor || [0.3, 0.3, 0.3],\n        markerColor: markerColor || [0.1, 0.8, 1],\n        glowColor: glowColor || [1, 1, 1],\n        markers: markers || [],\n        scale: scale || 1,\n        onRender: (state) => {\n          if (!state) return;\n\n          if (autoRotate && !pointerInteracting.current && !focusRef.current) {\n            phiRef.current += 0.01;\n          }\n\n          if (focusRef.current) {\n            const [focusPhi, focusTheta] = focusRef.current;\n            const distPositive = (focusPhi - currentPhi + doublePi) % doublePi;\n            const distNegative = (currentPhi - focusPhi + doublePi) % doublePi;\n\n            currentPhi +=\n              distPositive < distNegative\n                ? distPositive * 0.08\n                : -distNegative * 0.08;\n            currentTheta = currentTheta * 0.92 + focusTheta * 0.08;\n          } else {\n            currentPhi = phiRef.current + r.get();\n          }\n\n          state.phi = currentPhi;\n          state.theta = focusRef.current ? currentTheta : 0;\n          state.width = width * 2;\n          state.height = width * 2;\n        },\n      });\n\n      if (canvasRef.current) {\n        setTimeout(() => {\n          if (canvasRef.current) canvasRef.current.style.opacity = '1';\n        }, 100);\n      }\n    } catch (error) {\n      console.error('Error creating globe:', error);\n    }\n\n    return () => {\n      if (globeRef.current) {\n        globeRef.current.destroy();\n        globeRef.current = null;\n      }\n      window.removeEventListener('resize', onResize);\n      if (animationFrame) {\n        cancelAnimationFrame(animationFrame);\n      }\n    };\n  }, [\n    baseColor,\n    markerColor,\n    glowColor,\n    markers,\n    scale,\n    r,\n    autoRotate,\n    isVisible,\n  ]);\n\n  return (\n    <div ref={containerRef} className={`relative ${className}`}>\n      <canvas\n        ref={canvasRef}\n        onPointerDown={(e) => {\n          pointerInteracting.current =\n            e.clientX - pointerInteractionMovement.current;\n          canvasRef.current?.style?.setProperty('cursor', 'grabbing');\n        }}\n        onPointerUp={() => {\n          pointerInteracting.current = null;\n          canvasRef.current?.style?.setProperty('cursor', 'grab');\n        }}\n        onPointerOut={() => {\n          pointerInteracting.current = null;\n          canvasRef.current?.style?.setProperty('cursor', 'grab');\n        }}\n        onMouseMove={(e) => {\n          if (pointerInteracting.current !== null) {\n            const delta = e.clientX - pointerInteracting.current;\n            pointerInteractionMovement.current = delta;\n            api.start({ r: delta / 200 });\n          }\n        }}\n        onTouchMove={(e) => {\n          if (pointerInteracting.current !== null && e.touches[0]) {\n            const delta = e.touches[0].clientX - pointerInteracting.current;\n            pointerInteractionMovement.current = delta;\n            api.start({ r: delta / 100 });\n          }\n        }}\n        className='w-full h-full cursor-grab opacity-0 transition-opacity duration-1000'\n        style={{ contain: 'layout paint size' }}\n      />\n    </div>\n  );\n}\n"
    }
  ]
}
