{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cobe-globe",
  "title": "cobe-globe",
  "description": "A cobe globe component.",
  "dependencies": [
    "react-spring",
    "cobe",
    "react"
  ],
  "registryDependencies": [
    "button",
    "utils"
  ],
  "files": [
    {
      "path": "registry/eldoraui/cobe-globe.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\nimport createGlobe from \"cobe\"\nimport { useSpring } from \"react-spring\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\n\ntype CobeVariant =\n  | \"default\"\n  | \"draggable\"\n  | \"auto-draggable\"\n  | \"auto-rotation\"\n  | \"rotate-to-location\"\n  | \"scaled\"\n\ninterface Location {\n  name: string\n  lat?: number\n  long?: number\n  emoji?: string\n}\n\ninterface GeocodeResult {\n  lat: number\n  lng: number\n  display_name: string\n}\n\ninterface CobeProps {\n  variant?: CobeVariant\n  className?: string\n  style?: React.CSSProperties\n  locations?: Location[]\n  // Globe configuration settings\n  phi?: number\n  theta?: number\n  mapSamples?: number\n  mapBrightness?: number\n  mapBaseBrightness?: number\n  diffuse?: number\n  dark?: number\n  baseColor?: string\n  markerColor?: string\n  markerSize?: number\n  glowColor?: string\n  scale?: number\n  offsetX?: number\n  offsetY?: number\n  opacity?: number\n}\n\ntype CobeState = Record<string, unknown>\n\nexport function Cobe({\n  variant = \"default\",\n  className,\n  style,\n  locations = [\n    { name: \"San Francisco\", emoji: \"📍\" },\n    { name: \"Berlin\", emoji: \"📍\" },\n    { name: \"Tokyo\", emoji: \"📍\" },\n    { name: \"Buenos Aires\", emoji: \"📍\" },\n  ],\n  // Default values based on the original JSX version\n  phi = 0,\n  theta = 0.2,\n  mapSamples = 16000,\n  mapBrightness = 1.8,\n  mapBaseBrightness = 0.05,\n  diffuse = 3,\n  dark = 1.0,\n  baseColor = \"#ffffff\",\n  markerColor = \"#fb6415\",\n  markerSize = 0.05,\n  glowColor = \"#ffffff\",\n  scale = 1.0,\n  offsetX = 0.0,\n  offsetY = 0.0,\n  opacity = 0.7,\n}: CobeProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const pointerInteracting = useRef<number | null>(null)\n  const pointerInteractionMovement = useRef<number>(0)\n  const focusRef = useRef<[number, number]>([0, 0])\n  const [customLocations, setCustomLocations] = useState<Location[]>([])\n  const [isInitializing, setIsInitializing] = useState(true)\n\n  const [{ r }, api] = useSpring<{ r: number }>(() => ({\n    r: 0,\n    config: {\n      mass: 1,\n      tension: 280,\n      friction: 40,\n      precision: 0.001,\n    },\n  }))\n\n  const locationToAngles = (lat: number, long: number): [number, number] => {\n    return [\n      Math.PI - ((long * Math.PI) / 180 - Math.PI / 2),\n      (lat * Math.PI) / 180,\n    ] as [number, number]\n  }\n\n  const hexToRgb = (hex: string): [number, number, number] => {\n    const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex)\n    return result\n      ? [\n          parseInt(result[1], 16) / 255,\n          parseInt(result[2], 16) / 255,\n          parseInt(result[3], 16) / 255,\n        ]\n      : [0, 0, 0]\n  }\n\n  const geocodeLocation = async (\n    query: string\n  ): Promise<GeocodeResult | null> => {\n    try {\n      const response = await fetch(\n        `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=1`\n      )\n      const data = await response.json()\n\n      if (data && data.length > 0) {\n        return {\n          lat: parseFloat(data[0].lat),\n          lng: parseFloat(data[0].lon),\n          display_name: data[0].display_name,\n        }\n      }\n      return null\n    } catch (error) {\n      console.error(\"Geocoding error:\", error)\n      return null\n    }\n  }\n\n  const geocodeLocationList = useCallback(async (locationList: Location[]) => {\n    const geocodedLocations: Location[] = []\n\n    for (const location of locationList) {\n      if (location.lat && location.long) {\n        // Already has coordinates\n        geocodedLocations.push(location)\n      } else {\n        // Need to geocode\n        const result = await geocodeLocation(location.name)\n        if (result) {\n          geocodedLocations.push({\n            ...location,\n            lat: result.lat,\n            long: result.lng,\n          })\n        }\n      }\n    }\n\n    return geocodedLocations\n  }, [])\n\n  // Initialize locations on component mount\n  useEffect(() => {\n    const initializeLocations = async () => {\n      if (variant === \"rotate-to-location\" && locations.length > 0) {\n        setIsInitializing(true)\n        const geocoded = await geocodeLocationList(locations)\n        setCustomLocations(geocoded)\n        setIsInitializing(false)\n      }\n    }\n\n    initializeLocations()\n  }, [variant, locations, geocodeLocationList])\n\n  useEffect(() => {\n    let phi = 0\n    let width = 0\n    let currentPhi = 0\n    let currentTheta = 0\n    const doublePi = Math.PI * 2\n\n    const onResize = () => {\n      if (canvasRef.current) {\n        width = canvasRef.current.offsetWidth\n      }\n    }\n\n    window.addEventListener(\"resize\", onResize)\n    onResize()\n\n    if (!canvasRef.current) return\n\n    const globe = createGlobe(canvasRef.current, {\n      devicePixelRatio: 2,\n      width: width * 2,\n      height: variant === \"scaled\" ? width * 2 * 0.4 : width * 2,\n      phi: phi,\n      theta: theta,\n      dark: dark,\n      diffuse: diffuse,\n      mapSamples: mapSamples,\n      mapBrightness: mapBrightness,\n      mapBaseBrightness: mapBaseBrightness,\n      baseColor: hexToRgb(baseColor),\n      markerColor: hexToRgb(markerColor),\n      glowColor: hexToRgb(glowColor),\n      markers:\n        variant === \"default\" ||\n        variant === \"draggable\" ||\n        variant === \"auto-draggable\" ||\n        variant === \"auto-rotation\" ||\n        variant === \"scaled\"\n          ? [\n              // San Francisco, default color\n              { location: [37.7595, -122.4367], size: markerSize },\n              // New York, red color\n              {\n                location: [40.7128, -74.006],\n                size: markerSize,\n                color: [1, 0, 0],\n              },\n              // Tokyo, blue color\n              {\n                location: [35.6895, 139.6917],\n                size: markerSize,\n                color: [0, 0.5, 1],\n              },\n              // Sydney, green color\n              {\n                location: [-33.8688, 151.2093],\n                size: markerSize,\n                color: [0, 1, 0],\n              },\n              // Rio de Janeiro, purple color\n              {\n                location: [-22.9068, -43.1729],\n                size: markerSize,\n                color: [0.8, 0, 0.8],\n              },\n              // Paris, yellow color\n              {\n                location: [48.8566, 2.3522],\n                size: markerSize,\n                color: [1, 1, 0],\n              },\n              // Porto, orange color\n              {\n                location: [41.1579, -8.6291],\n                size: markerSize,\n                color: [1, 0.5, 0],\n              },\n              // Athens, pink color\n              {\n                location: [37.9838, 23.7275],\n                size: markerSize,\n                color: [1, 0.5, 1],\n              },\n              // Rome, brown color\n              {\n                location: [41.9028, 12.4964],\n                size: markerSize,\n                color: [0.5, 0.3, 0],\n              },\n              // Kathmandu, blue color\n              {\n                location: [27.7172, 85.324],\n                size: markerSize,\n                color: [0, 0.5, 1],\n              },\n              // Tarbes, green color\n              {\n                location: [43.4643, -0.5167],\n                size: markerSize,\n                color: [0, 1, 0],\n              },\n              // Bamako, yellow color\n              {\n                location: [12.6683, -8.0076],\n                size: markerSize,\n                color: [1, 1, 0],\n              },\n              // Djibouti, purple color\n              {\n                location: [11.55, 43.1667],\n                size: markerSize,\n                color: [0.8, 0, 0.8],\n              },\n            ]\n          : variant === \"rotate-to-location\"\n            ? customLocations\n                .filter((loc) => loc.lat && loc.long)\n                .map((loc) => ({\n                  location: [loc.lat!, loc.long!],\n                  size: markerSize,\n                }))\n            : [],\n      scale: variant === \"scaled\" ? 2.5 : undefined,\n      offset: variant === \"scaled\" ? [0, width * 2 * 0.4 * 0.6] : undefined,\n      opacity: opacity,\n      onRender: (state: CobeState) => {\n        switch (variant) {\n          case \"default\":\n            state.phi = phi + r.get()\n            phi += 0.005\n            break\n          case \"draggable\":\n            state.phi = r.get()\n            break\n          case \"auto-draggable\":\n            if (!pointerInteracting.current) {\n              phi += 0.005\n            }\n            state.phi = phi + r.get()\n            break\n          case \"auto-rotation\":\n            state.phi = phi\n            phi += 0.005\n            break\n          case \"rotate-to-location\":\n            state.phi = currentPhi\n            state.theta = currentTheta\n            const [focusPhi, focusTheta] = focusRef.current\n            const distPositive = (focusPhi - currentPhi + doublePi) % doublePi\n            const distNegative = (currentPhi - focusPhi + doublePi) % doublePi\n            if (distPositive < distNegative) {\n              currentPhi += distPositive * 0.08\n            } else {\n              currentPhi -= distNegative * 0.08\n            }\n            currentTheta = currentTheta * 0.92 + focusTheta * 0.08\n            break\n          case \"scaled\":\n            // No rotation for scaled variant\n            break\n        }\n\n        state.width = width * 2\n        state.height = variant === \"scaled\" ? width * 2 * 0.4 : width * 2\n      },\n    })\n\n    if (canvasRef.current) {\n      setTimeout(() => {\n        if (canvasRef.current) {\n          canvasRef.current.style.opacity = opacity.toString()\n        }\n      })\n    }\n\n    return () => {\n      globe.destroy()\n      window.removeEventListener(\"resize\", onResize)\n    }\n  }, [\n    variant,\n    r,\n    customLocations,\n    phi,\n    theta,\n    mapSamples,\n    mapBrightness,\n    mapBaseBrightness,\n    diffuse,\n    dark,\n    baseColor,\n    markerColor,\n    markerSize,\n    glowColor,\n    scale,\n    offsetX,\n    offsetY,\n    opacity,\n  ])\n\n  const handlePointerDown = (e: React.PointerEvent) => {\n    if (\n      variant === \"draggable\" ||\n      variant === \"auto-draggable\" ||\n      variant === \"default\"\n    ) {\n      pointerInteracting.current =\n        e.clientX - pointerInteractionMovement.current\n      if (canvasRef.current) canvasRef.current.style.cursor = \"grabbing\"\n    }\n  }\n\n  const handlePointerUp = () => {\n    if (\n      variant === \"draggable\" ||\n      variant === \"auto-draggable\" ||\n      variant === \"default\"\n    ) {\n      pointerInteracting.current = null\n      if (canvasRef.current) canvasRef.current.style.cursor = \"grab\"\n    }\n  }\n\n  const handlePointerOut = () => {\n    if (\n      variant === \"draggable\" ||\n      variant === \"auto-draggable\" ||\n      variant === \"default\"\n    ) {\n      pointerInteracting.current = null\n      if (canvasRef.current) canvasRef.current.style.cursor = \"grab\"\n    }\n  }\n\n  const handleMouseMove = (e: React.MouseEvent) => {\n    if (\n      (variant === \"draggable\" ||\n        variant === \"auto-draggable\" ||\n        variant === \"default\") &&\n      pointerInteracting.current !== null\n    ) {\n      const delta = e.clientX - pointerInteracting.current\n      pointerInteractionMovement.current = delta\n      api.start({\n        r: delta / 200,\n      })\n    }\n  }\n\n  const handleTouchMove = (e: React.TouchEvent) => {\n    if (\n      (variant === \"draggable\" ||\n        variant === \"auto-draggable\" ||\n        variant === \"default\") &&\n      pointerInteracting.current !== null &&\n      e.touches[0]\n    ) {\n      const delta = e.touches[0].clientX - pointerInteracting.current\n      pointerInteractionMovement.current = delta\n      api.start({\n        r: delta / 100,\n      })\n    }\n  }\n\n  const handleLocationClick = (lat: number, long: number) => {\n    if (variant === \"rotate-to-location\") {\n      focusRef.current = locationToAngles(lat, long)\n    }\n  }\n\n  const containerStyle = {\n    width: \"100%\",\n    maxWidth: variant === \"scaled\" ? 800 : 600,\n    aspectRatio: variant === \"scaled\" ? 2.5 : 1,\n    margin: \"auto\",\n    position: \"relative\" as const,\n    ...style,\n  }\n\n  const canvasStyle = {\n    width: \"100%\",\n    height: \"100%\",\n    contain: \"layout paint size\" as const,\n    opacity: 0,\n    transition: \"opacity 1s ease\",\n    cursor:\n      variant === \"draggable\" ||\n      variant === \"auto-draggable\" ||\n      variant === \"default\"\n        ? \"grab\"\n        : undefined,\n    borderRadius:\n      variant === \"default\" ||\n      variant === \"draggable\" ||\n      variant === \"auto-draggable\" ||\n      variant === \"auto-rotation\"\n        ? \"50%\"\n        : variant === \"scaled\"\n          ? \"8px\"\n          : undefined,\n  }\n\n  return (\n    <div className={cn(\"\", className)} style={containerStyle}>\n      <canvas\n        ref={canvasRef}\n        onPointerDown={handlePointerDown}\n        onPointerUp={handlePointerUp}\n        onPointerOut={handlePointerOut}\n        onMouseMove={handleMouseMove}\n        onTouchMove={handleTouchMove}\n        style={canvasStyle}\n      />\n      {variant === \"rotate-to-location\" && (\n        <>\n          <div\n            className=\"control-buttons flex flex-col items-center justify-center md:flex-row\"\n            style={{ gap: \".5rem\" }}\n          >\n            {isInitializing ? \"Loading locations...\" : \"\"}\n            {customLocations\n              .filter((loc) => loc.lat && loc.long)\n              .map((location, index) => (\n                <Button\n                  key={index}\n                  onClick={() =>\n                    handleLocationClick(location.lat!, location.long!)\n                  }\n                  className=\"bg-background/80 text-foreground hover:bg-background/90 border-border transition-all duration-200 hover:scale-105\"\n                >\n                  {location.emoji || \"📍\"} {location.name}\n                </Button>\n              ))}\n          </div>\n        </>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}