{
  "name": "3d-globe",
  "type": "registry:ui",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei"
  ],
  "devDependencies": [
    "@types/three"
  ],
  "files": [
    {
      "path": "components/ui/3d-globe.tsx",
      "content": "\"use client\";\nimport React, { useRef, useMemo, useState, useCallback, Suspense } from \"react\";\nimport { Canvas, useFrame, useThree } from \"@react-three/fiber\";\nimport { OrbitControls, Html, useTexture } from \"@react-three/drei\";\nimport * as THREE from \"three\";\nimport { cn } from \"@/lib/utils\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface GlobeMarker {\n  lat: number;\n  lng: number;\n  src: string;\n  label?: string;\n  size?: number;\n}\n\nexport interface Globe3DConfig {\n  /** Globe radius */\n  radius?: number;\n  /** Globe base color (used as fallback or tint) */\n  globeColor?: string;\n  /** URL to the Earth texture map */\n  textureUrl?: string;\n  /** URL to the bump/elevation map for terrain */\n  bumpMapUrl?: string;\n  /** Whether to show atmosphere glow */\n  showAtmosphere?: boolean;\n  /** Atmosphere color */\n  atmosphereColor?: string;\n  /** Atmosphere intensity */\n  atmosphereIntensity?: number;\n  /** Atmosphere blur/softness (higher = more diffuse, default 3) */\n  atmosphereBlur?: number;\n  /** Terrain bump scale (0 = flat, higher = more pronounced) */\n  bumpScale?: number;\n  /** Auto rotate speed (0 = disabled) */\n  autoRotateSpeed?: number;\n  /** Enable zoom */\n  enableZoom?: boolean;\n  /** Enable pan */\n  enablePan?: boolean;\n  /** Min zoom distance */\n  minDistance?: number;\n  /** Max zoom distance */\n  maxDistance?: number;\n  /** Initial rotation */\n  initialRotation?: { x: number; y: number };\n  /** Marker default size */\n  markerSize?: number;\n  /** Show wireframe overlay */\n  showWireframe?: boolean;\n  /** Wireframe color */\n  wireframeColor?: string;\n  /** Ambient light intensity */\n  ambientIntensity?: number;\n  /** Point light intensity */\n  pointLightIntensity?: number;\n  /** Background color (null for transparent) */\n  backgroundColor?: string | null;\n}\n\ninterface Globe3DProps {\n  /** Array of markers to display on the globe */\n  markers?: GlobeMarker[];\n  /** Globe configuration */\n  config?: Globe3DConfig;\n  /** Additional CSS classes */\n  className?: string;\n  /** Callback when a marker is clicked */\n  onMarkerClick?: (marker: GlobeMarker) => void;\n  /** Callback when a marker is hovered */\n  onMarkerHover?: (marker: GlobeMarker | null) => void;\n}\n\n// ============================================================================\n// Constants - Earth Texture URLs (NASA Blue Marble)\n// ============================================================================\n\nconst DEFAULT_EARTH_TEXTURE =\n  \"https://unpkg.com/three-globe@2.31.0/example/img/earth-blue-marble.jpg\";\nconst DEFAULT_BUMP_TEXTURE =\n  \"https://unpkg.com/three-globe@2.31.0/example/img/earth-topology.png\";\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\n/**\n * Convert latitude/longitude to 3D cartesian coordinates\n */\nfunction latLngToVector3(\n  lat: number,\n  lng: number,\n  radius: number,\n): THREE.Vector3 {\n  const phi = (90 - lat) * (Math.PI / 180);\n  const theta = (lng + 180) * (Math.PI / 180);\n\n  const x = -(radius * Math.sin(phi) * Math.cos(theta));\n  const z = radius * Math.sin(phi) * Math.sin(theta);\n  const y = radius * Math.cos(phi);\n\n  return new THREE.Vector3(x, y, z);\n}\n\n// ============================================================================\n// Marker Component (static - rotation handled by parent group)\n// ============================================================================\n\ninterface MarkerProps {\n  marker: GlobeMarker;\n  radius: number;\n  defaultSize: number;\n  onClick?: (marker: GlobeMarker) => void;\n  onHover?: (marker: GlobeMarker | null) => void;\n}\n\nfunction Marker({\n  marker,\n  radius,\n  defaultSize,\n  onClick,\n  onHover,\n}: MarkerProps) {\n  const [hovered, setHovered] = useState(false);\n  const [isVisible, setIsVisible] = useState(true);\n  const groupRef = useRef<THREE.Group>(null);\n  const imageGroupRef = useRef<THREE.Group>(null);\n  const { camera } = useThree();\n\n  // Surface position (where the line starts)\n  const surfacePosition = useMemo(() => {\n    return latLngToVector3(marker.lat, marker.lng, radius * 1.001);\n  }, [marker.lat, marker.lng, radius]);\n\n  // Top of the line (where the image is) - positioned further out to prevent going inside globe\n  const topPosition = useMemo(() => {\n    return latLngToVector3(marker.lat, marker.lng, radius * 1.18);\n  }, [marker.lat, marker.lng, radius]);\n\n  const lineHeight = topPosition.distanceTo(surfacePosition);\n\n  // Check if marker is facing the camera\n  useFrame(() => {\n    if (!imageGroupRef.current) return;\n\n    // Get the world position of the image (the positioned element)\n    const worldPos = new THREE.Vector3();\n    imageGroupRef.current.getWorldPosition(worldPos);\n\n    // Direction from globe center (0,0,0) to marker\n    const markerDirection = worldPos.clone().normalize();\n\n    // Direction from globe center to camera\n    const cameraDirection = camera.position.clone().normalize();\n\n    // Dot product: positive means facing camera, negative means behind\n    const dot = markerDirection.dot(cameraDirection);\n\n    // Show marker only if it's facing the camera (stricter threshold)\n    setIsVisible(dot > 0.1);\n  });\n\n  const handlePointerEnter = useCallback(() => {\n    setHovered(true);\n    onHover?.(marker);\n  }, [marker, onHover]);\n\n  const handlePointerLeave = useCallback(() => {\n    setHovered(false);\n    onHover?.(null);\n  }, [onHover]);\n\n  const handleClick = useCallback(() => {\n    onClick?.(marker);\n  }, [marker, onClick]);\n\n  // Calculate line center and orientation\n  const { lineCenter, lineQuaternion } = useMemo(() => {\n    const center = surfacePosition.clone().lerp(topPosition, 0.5);\n\n    // Calculate rotation to align cylinder with the direction from surface to top\n    const direction = topPosition.clone().sub(surfacePosition).normalize();\n    const quaternion = new THREE.Quaternion();\n    quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction);\n\n    return { lineCenter: center, lineQuaternion: quaternion };\n  }, [surfacePosition, topPosition]);\n\n  return (\n    <group ref={groupRef} visible={isVisible}>\n      {/* Pin line from surface to image - properly oriented */}\n      <mesh position={lineCenter} quaternion={lineQuaternion}>\n        <cylinderGeometry args={[0.003, 0.003, lineHeight, 8]} />\n        <meshBasicMaterial\n          color={hovered ? \"#ffffff\" : \"#94a3b8\"}\n          transparent\n          opacity={hovered ? 0.9 : 0.6}\n        />\n      </mesh>\n\n      {/* Pin point at the surface */}\n      <mesh position={surfacePosition} quaternion={lineQuaternion}>\n        <coneGeometry args={[0.015, 0.04, 8]} />\n        <meshBasicMaterial color={hovered ? \"#f97316\" : \"#ef4444\"} />\n      </mesh>\n\n      {/* Circular image at the top */}\n      <group ref={imageGroupRef} position={topPosition}>\n        <Html\n          transform\n          center\n          sprite\n          distanceFactor={10}\n          style={{\n            pointerEvents: isVisible ? \"auto\" : \"none\",\n            opacity: isVisible ? 1 : 0,\n            transition: \"opacity 0.15s ease-out\",\n          }}\n        >\n          <div\n            className={cn(\n              \"cursor-pointer overflow-hidden rounded-full bg-neutral-900 shadow-lg transition-transform duration-200\",\n              hovered && \"scale-125 shadow-xl ring-1 ring-white/50\",\n            )}\n            style={{\n              width: \"8px\",\n              height: \"8px\",\n            }}\n            onMouseEnter={handlePointerEnter}\n            onMouseLeave={handlePointerLeave}\n            onClick={handleClick}\n          >\n            <img\n              src={marker.src}\n              alt={marker.label || \"Marker\"}\n              className=\"h-full w-full object-cover\"\n              draggable={false}\n            />\n          </div>\n        </Html>\n      </group>\n    </group>\n  );\n}\n\n// ============================================================================\n// Rotating Globe with Markers (all rotate together)\n// ============================================================================\n\ninterface RotatingGlobeProps {\n  config: Required<Globe3DConfig>;\n  markers: GlobeMarker[];\n  onMarkerClick?: (marker: GlobeMarker) => void;\n  onMarkerHover?: (marker: GlobeMarker | null) => void;\n}\n\nfunction RotatingGlobe({\n  config,\n  markers,\n  onMarkerClick,\n  onMarkerHover,\n}: RotatingGlobeProps) {\n  const groupRef = useRef<THREE.Group>(null);\n\n  // Load Earth textures\n  const [earthTexture, bumpTexture] = useTexture([\n    config.textureUrl,\n    config.bumpMapUrl,\n  ]);\n\n  // Configure textures\n  useMemo(() => {\n    if (earthTexture) {\n      earthTexture.colorSpace = THREE.SRGBColorSpace;\n      earthTexture.anisotropy = 16;\n    }\n    if (bumpTexture) {\n      bumpTexture.anisotropy = 8;\n    }\n  }, [earthTexture, bumpTexture]);\n\n  // Create geometries\n  const geometry = useMemo(() => {\n    return new THREE.SphereGeometry(config.radius, 64, 64);\n  }, [config.radius]);\n\n  const wireframeGeometry = useMemo(() => {\n    return new THREE.SphereGeometry(config.radius * 1.002, 32, 16);\n  }, [config.radius]);\n\n  return (\n    <group ref={groupRef}>\n      {/* Main globe mesh with Earth texture */}\n      <mesh geometry={geometry}>\n        <meshStandardMaterial\n          map={earthTexture}\n          bumpMap={bumpTexture}\n          bumpScale={config.bumpScale * 0.05}\n          roughness={0.7}\n          metalness={0.0}\n        />\n      </mesh>\n\n      {/* Wireframe overlay */}\n      {config.showWireframe && (\n        <mesh geometry={wireframeGeometry}>\n          <meshBasicMaterial\n            color={config.wireframeColor}\n            wireframe\n            transparent\n            opacity={0.08}\n          />\n        </mesh>\n      )}\n\n      {/* Markers - now inside the rotating group */}\n      {markers.map((marker, index) => (\n        <Marker\n          key={`marker-${index}-${marker.lat}-${marker.lng}`}\n          marker={marker}\n          radius={config.radius}\n          defaultSize={config.markerSize}\n          onClick={onMarkerClick}\n          onHover={onMarkerHover}\n        />\n      ))}\n    </group>\n  );\n}\n\n// ============================================================================\n// Atmosphere Component (stays static - doesn't rotate)\n// ============================================================================\n\ninterface AtmosphereProps {\n  radius: number;\n  color: string;\n  intensity: number;\n  blur: number;\n}\n\nfunction Atmosphere({ radius, color, intensity, blur }: AtmosphereProps) {\n  // blur controls the fresnel exponent: lower = more diffuse, higher = sharper edge\n  // We invert it so higher blur value = more diffuse (lower exponent)\n  const fresnelPower = Math.max(0.5, 5 - blur);\n\n  const atmosphereMaterial = useMemo(() => {\n    return new THREE.ShaderMaterial({\n      uniforms: {\n        atmosphereColor: { value: new THREE.Color(color) },\n        intensity: { value: intensity },\n        fresnelPower: { value: fresnelPower },\n      },\n      vertexShader: `\n        varying vec3 vNormal;\n        varying vec3 vPosition;\n        void main() {\n          vNormal = normalize(normalMatrix * normal);\n          vPosition = (modelViewMatrix * vec4(position, 1.0)).xyz;\n          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n        }\n      `,\n      fragmentShader: `\n        uniform vec3 atmosphereColor;\n        uniform float intensity;\n        uniform float fresnelPower;\n        varying vec3 vNormal;\n        varying vec3 vPosition;\n        void main() {\n          float fresnel = pow(1.0 - abs(dot(vNormal, normalize(-vPosition))), fresnelPower);\n          gl_FragColor = vec4(atmosphereColor, fresnel * intensity);\n        }\n      `,\n      side: THREE.BackSide,\n      transparent: true,\n      depthWrite: false,\n    });\n  }, [color, intensity, fresnelPower]);\n\n  return (\n    <mesh scale={[1.12, 1.12, 1.12]}>\n      <sphereGeometry args={[radius, 64, 32]} />\n      <primitive object={atmosphereMaterial} attach=\"material\" />\n    </mesh>\n  );\n}\n\n// ============================================================================\n// Scene Component\n// ============================================================================\n\ninterface SceneProps {\n  markers: GlobeMarker[];\n  config: Required<Globe3DConfig>;\n  onMarkerClick?: (marker: GlobeMarker) => void;\n  onMarkerHover?: (marker: GlobeMarker | null) => void;\n}\n\nfunction Scene({ markers, config, onMarkerClick, onMarkerHover }: SceneProps) {\n  const { camera } = useThree();\n\n  // Set initial camera position (pulled back to accommodate markers)\n  React.useEffect(() => {\n    camera.position.set(0, 0, config.radius * 3.5);\n    camera.lookAt(0, 0, 0);\n  }, [camera, config.radius]);\n\n  return (\n    <>\n      {/* Lighting */}\n      <ambientLight intensity={config.ambientIntensity} />\n      <directionalLight\n        position={[config.radius * 5, config.radius * 2, config.radius * 5]}\n        intensity={config.pointLightIntensity}\n        color=\"#ffffff\"\n      />\n      <directionalLight\n        position={[-config.radius * 3, config.radius, -config.radius * 2]}\n        intensity={config.pointLightIntensity * 0.3}\n        color=\"#88ccff\"\n      />\n\n      {/* Rotating Globe with Markers */}\n      <RotatingGlobe\n        config={config}\n        markers={markers}\n        onMarkerClick={onMarkerClick}\n        onMarkerHover={onMarkerHover}\n      />\n\n      {/* Atmosphere (static) */}\n      {config.showAtmosphere && (\n        <Atmosphere\n          radius={config.radius}\n          color={config.atmosphereColor}\n          intensity={config.atmosphereIntensity}\n          blur={config.atmosphereBlur}\n        />\n      )}\n\n      {/* Controls */}\n      <OrbitControls\n        makeDefault\n        enablePan={config.enablePan}\n        enableZoom={config.enableZoom}\n        minDistance={config.minDistance}\n        maxDistance={config.maxDistance}\n        rotateSpeed={0.4}\n        autoRotate={config.autoRotateSpeed > 0}\n        autoRotateSpeed={config.autoRotateSpeed}\n        enableDamping\n        dampingFactor={0.1}\n      />\n    </>\n  );\n}\n\n// ============================================================================\n// Loading Fallback\n// ============================================================================\n\nfunction LoadingFallback() {\n  return (\n    <Html center>\n      <div className=\"flex shrink-0 flex-col items-center gap-3\">\n        <span className=\"inline-block shrink-0 text-sm text-neutral-400\">\n          Loading globe...\n        </span>\n      </div>\n    </Html>\n  );\n}\n\n// ============================================================================\n// Main Globe3D Component\n// ============================================================================\n\nconst defaultConfig: Required<Globe3DConfig> = {\n  radius: 2,\n  globeColor: \"#1a1a2e\",\n  textureUrl: DEFAULT_EARTH_TEXTURE,\n  bumpMapUrl: DEFAULT_BUMP_TEXTURE,\n  showAtmosphere: false,\n  atmosphereColor: \"#4da6ff\",\n  atmosphereIntensity: 0.5,\n  atmosphereBlur: 2,\n  bumpScale: 1,\n  autoRotateSpeed: 0.3,\n  enableZoom: false,\n  enablePan: false,\n  minDistance: 5,\n  maxDistance: 15,\n  initialRotation: { x: 0, y: 0 },\n  markerSize: 0.06,\n  showWireframe: false,\n  wireframeColor: \"#4a9eff\",\n  ambientIntensity: 0.6,\n  pointLightIntensity: 1.5,\n  backgroundColor: null,\n};\n\nexport function Globe3D({\n  markers = [],\n  config = {},\n  className,\n  onMarkerClick,\n  onMarkerHover,\n}: Globe3DProps) {\n  const mergedConfig = useMemo(\n    () => ({ ...defaultConfig, ...config }),\n    [config],\n  );\n\n  return (\n    <div className={cn(\"relative h-[500px] w-full\", className)}>\n      <Canvas\n        gl={{\n          antialias: true,\n          alpha: true,\n          powerPreference: \"high-performance\",\n        }}\n        dpr={[1, 2]}\n        camera={{\n          fov: 45,\n          near: 0.1,\n          far: 1000,\n          position: [0, 0, mergedConfig.radius * 3.5],\n        }}\n        style={{\n          background: mergedConfig.backgroundColor || \"transparent\",\n        }}\n      >\n        <Suspense fallback={<LoadingFallback />}>\n          <Scene\n            markers={markers}\n            config={mergedConfig}\n            onMarkerClick={onMarkerClick}\n            onMarkerHover={onMarkerHover}\n          />\n        </Suspense>\n      </Canvas>\n    </div>\n  );\n}\n\nexport default Globe3D;\n",
      "type": "registry:ui",
      "target": "components/ui/3d-globe.tsx"
    }
  ],
  "author": "Manu Arora <hi@manuarora.in>",
  "title": "3d Globe"
}