{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "globe-wireframe",
  "type": "registry:ui",
  "title": "Globe Wireframe",
  "description": "The Globe Wireframe component creates beautiful globe with smooth city rotations, responsive design, and premium motion",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": [
    "d3",
    "topojson-client",
    "@types/d3",
    "@types/topojson-client",
    "@types/topojson-specification"
  ],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/globe-wireframe.tsx",
      "content": "'use client';\nimport type React from 'react';\nimport { useEffect, useRef, useState, useCallback } from 'react';\nimport * as d3 from 'd3';\nimport { feature } from 'topojson-client';\nimport type {\n  Topology,\n  GeometryCollection,\n  GeometryObject,\n} from 'topojson-specification';\nimport type { GeoPermissibleObjects } from 'd3';\n\ninterface GlobeWireframeProps {\n  width?: number;\n  height?: number;\n  className?: string;\n  strokeColor?: string;\n  strokeWidth?: number;\n  graticuleColor?: string;\n  graticuleOpacity?: number;\n  sphereOutlineColor?: string;\n  sphereOutlineWidth?: number;\n  autoRotate?: boolean;\n  autoRotateSpeed?: number;\n  rotateToLocation?: string | [number, number];\n  rotateCities?: string[];\n  rotationSpeed?: number;\n  initialRotation?: [number, number];\n  enableInteraction?: boolean;\n  showGraticule?: boolean;\n  animationDuration?: number;\n  startAsGlobe?: boolean;\n  countryFillColor?: string;\n  countryHoverColor?: string;\n  variant?: 'wireframe' | 'wireframesolid' | 'solid';\n  scale?: number;\n  backgroundColor?: string;\n}\n\ninterface GeoFeature {\n  type: string;\n  geometry: GeometryObject;\n  properties: Record<string, unknown>;\n}\n\ninterface WorldAtlasTopology extends Topology {\n  objects: {\n    countries: GeometryCollection;\n  };\n}\n\ninterface CustomProjection extends d3.GeoProjection {\n  alpha(value: number): CustomProjection;\n  alpha(): 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  sydney: [-33.8688, 151.2093],\n  mumbai: [19.076, 72.8777],\n  'los angeles': [34.0522, -118.2437],\n  chicago: [41.8781, -87.6298],\n};\n\nfunction orthographicRaw(x: number, y: number): [number, number] {\n  const cosy = Math.cos(y);\n  return [cosy * Math.sin(x), Math.sin(y)];\n}\n\nfunction equirectangularRaw(lambda: number, phi: number): [number, number] {\n  return [lambda, phi];\n}\n\nfunction interpolateProjection(\n  raw0: (lambda: number, phi: number) => [number, number],\n  raw1: (lambda: number, phi: number) => [number, number],\n): CustomProjection {\n  let t = 0;\n\n  const createRawProjection = (\n    alpha: number,\n  ): ((lambda: number, phi: number) => [number, number]) => {\n    return (lambda: number, phi: number): [number, number] => {\n      const [x0, y0] = raw0(lambda, phi);\n      const [x1, y1] = raw1(lambda, phi);\n      return [x0 + alpha * (x1 - x0), y0 + alpha * (y1 - y0)];\n    };\n  };\n\n  const projection = d3.geoProjection(\n    createRawProjection(t),\n  ) as unknown as CustomProjection;\n\n  const alphaMethod = ((value?: number): CustomProjection | number => {\n    if (value !== undefined) {\n      t = +value;\n      const newProjection = d3.geoProjection(\n        createRawProjection(t),\n      ) as unknown as CustomProjection;\n\n      if (projection.scale()) newProjection.scale(projection.scale());\n      if (projection.translate())\n        newProjection.translate(projection.translate());\n      if (projection.rotate()) newProjection.rotate(projection.rotate());\n      if (projection.precision())\n        newProjection.precision(projection.precision());\n\n      newProjection.alpha = alphaMethod as CustomProjection['alpha'];\n\n      return newProjection;\n    }\n    return t;\n  }) as CustomProjection['alpha'];\n\n  projection.alpha = alphaMethod;\n\n  return projection;\n}\n\nexport default function GlobeWireframe({\n  width,\n  height,\n  className = 'aspect-square w-full max-w-150',\n  strokeColor = 'currentColor',\n  strokeWidth = 1.0,\n  graticuleColor = 'currentColor',\n  graticuleOpacity = 0.2,\n  sphereOutlineColor = 'currentColor',\n  sphereOutlineWidth = 1,\n  autoRotate = true,\n  autoRotateSpeed = 0.5,\n  rotateToLocation,\n  rotateCities = [],\n  rotationSpeed = 3000,\n  initialRotation = [0, 0],\n  enableInteraction = true,\n  showGraticule = true,\n  animationDuration = 2000,\n  startAsGlobe = true,\n  countryFillColor,\n  countryHoverColor,\n  variant = 'wireframe',\n  scale = 1,\n  backgroundColor,\n}: GlobeWireframeProps) {\n  const svgRef = useRef<SVGSVGElement>(null);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [isAnimating, setIsAnimating] = useState(false);\n  const [progress, setProgress] = useState(startAsGlobe ? 0 : 100);\n  const [worldData, setWorldData] = useState<GeoFeature[]>([]);\n  const [rotation, setRotation] = useState<[number, number]>(initialRotation);\n  const [isDragging, setIsDragging] = useState(false);\n  const [lastMouse, setLastMouse] = useState([0, 0]);\n  const [isVisible, setIsVisible] = useState(false);\n  const rotationInterval = useRef<NodeJS.Timeout | null>(null);\n  const rotationAnimFrame = useRef<number | null>(null);\n  const rotationStartTime = useRef<number | null>(null);\n  const rotationFrom = useRef<[number, number]>([0, 0]);\n  const rotationTo = useRef<[number, number]>([0, 0]);\n  const animationFrame = useRef<number | null>(null);\n  const [currentCityIndex, setCurrentCityIndex] = useState(0);\n  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });\n  const resizeObserver = useRef<ResizeObserver | null>(null);\n  const rotationRef = useRef(rotation);\n\n  useEffect(() => {\n    rotationRef.current = rotation;\n  }, [rotation]);\n\n  const useResponsive = !width && !height;\n  const finalWidth = useResponsive ? dimensions.width : width || 800;\n  const finalHeight = useResponsive ? dimensions.height : height || 500;\n\n  const defaultStrokeColor = strokeColor || 'currentColor';\n  const defaultGraticuleColor = graticuleColor || 'currentColor';\n  const defaultSphereOutlineColor = sphereOutlineColor || 'currentColor';\n  const defaultCountryFillColor =\n    countryFillColor || (variant === 'solid' ? 'currentColor' : 'none');\n  const defaultBackgroundColor = backgroundColor || 'transparent';\n\n  useEffect(() => {\n    if (!containerRef.current) return;\n\n    const container = containerRef.current;\n\n    const updateDimensions = () => {\n      if (container && useResponsive) {\n        const width = container.offsetWidth || 300;\n        setDimensions({ width, height: width });\n      }\n    };\n\n    updateDimensions();\n\n    if (useResponsive) {\n      resizeObserver.current = new ResizeObserver(updateDimensions);\n      resizeObserver.current.observe(container);\n    }\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      if (resizeObserver.current) {\n        resizeObserver.current.disconnect();\n      }\n      observer.unobserve(container);\n    };\n  }, [useResponsive]);\n\n  useEffect(() => {\n    const loadWorldData = async () => {\n      try {\n        const response = await fetch(\n          'https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json',\n        );\n        const world = (await response.json()) as WorldAtlasTopology;\n        const countries = feature(world, world.objects.countries)\n          .features as GeoFeature[];\n        setWorldData(countries);\n      } catch (error) {\n        console.error('Error loading world data:', error);\n        const fallbackData: GeoFeature[] = [\n          {\n            type: 'Feature',\n            geometry: {\n              type: 'Polygon',\n              coordinates: [\n                [\n                  [-180, -90],\n                  [180, -90],\n                  [180, 90],\n                  [-180, 90],\n                  [-180, -90],\n                ],\n              ],\n            } as unknown as GeometryObject,\n            properties: {},\n          },\n        ];\n        setWorldData(fallbackData);\n      }\n    };\n    loadWorldData();\n  }, []);\n\n  useEffect(() => {\n    if (!autoRotate || !isVisible || isDragging || rotateCities.length > 0) {\n      if (animationFrame.current) {\n        cancelAnimationFrame(animationFrame.current);\n        animationFrame.current = null;\n      }\n      return;\n    }\n\n    const rotate = () => {\n      setRotation((prev) => [(prev[0] + autoRotateSpeed) % 360, prev[1]]);\n      animationFrame.current = requestAnimationFrame(rotate);\n    };\n\n    animationFrame.current = requestAnimationFrame(rotate);\n\n    return () => {\n      if (animationFrame.current) {\n        cancelAnimationFrame(animationFrame.current);\n      }\n    };\n  }, [autoRotate, autoRotateSpeed, isVisible, isDragging, rotateCities.length]);\n\n  const animateRotationTo = useCallback(\n    (target: [number, number], duration = 1200) => {\n      if (rotationAnimFrame.current) {\n        cancelAnimationFrame(rotationAnimFrame.current);\n      }\n\n      rotationFrom.current = rotationRef.current;\n      rotationTo.current = target;\n      rotationStartTime.current = performance.now();\n\n      const animate = (time: number) => {\n        const elapsed = time - (rotationStartTime.current || 0);\n        const t = Math.min(elapsed / duration, 1);\n\n        const eased = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;\n\n        const lon =\n          rotationFrom.current[0] +\n          (rotationTo.current[0] - rotationFrom.current[0]) * eased;\n\n        const lat =\n          rotationFrom.current[1] +\n          (rotationTo.current[1] - rotationFrom.current[1]) * eased;\n\n        setRotation([lon, lat]);\n\n        if (t < 1) {\n          rotationAnimFrame.current = requestAnimationFrame(animate);\n        }\n      };\n\n      rotationAnimFrame.current = requestAnimationFrame(animate);\n    },\n    [],\n  );\n\n  useEffect(() => {\n    if (rotateCities.length === 0 || !isVisible) 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        animateRotationTo(\n          [-coordinates[1], -coordinates[0]],\n          rotationSpeed * 0.6,\n        );\n        setCurrentCityIndex(nextIndex);\n      }\n    };\n\n    const city = rotateCities[currentCityIndex].toLowerCase();\n    const coordinates = cityCoordinates[city];\n\n    if (coordinates) {\n      animateRotationTo(\n        [-coordinates[1], -coordinates[0]],\n        rotationSpeed * 0.6,\n      );\n    }\n\n    rotationInterval.current = setInterval(rotateToNextCity, rotationSpeed);\n\n    return () => {\n      if (rotationInterval.current) clearInterval(rotationInterval.current);\n    };\n  }, [\n    rotateCities,\n    currentCityIndex,\n    rotationSpeed,\n    isVisible,\n    animateRotationTo,\n  ]);\n\n  useEffect(() => {\n    if (!rotateToLocation) return;\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    setRotation([-coordinates[1], -coordinates[0]]);\n  }, [rotateToLocation]);\n\n  const handleMouseDown = (event: React.MouseEvent) => {\n    if (!enableInteraction) return;\n    setIsDragging(true);\n    const rect = svgRef.current?.getBoundingClientRect();\n    if (rect) {\n      setLastMouse([event.clientX - rect.left, event.clientY - rect.top]);\n    }\n  };\n\n  const handleMouseMove = (event: React.MouseEvent) => {\n    if (!isDragging || !enableInteraction) return;\n    const rect = svgRef.current?.getBoundingClientRect();\n    if (!rect) return;\n\n    const currentMouse = [event.clientX - rect.left, event.clientY - rect.top];\n    const dx = currentMouse[0] - lastMouse[0];\n    const dy = currentMouse[1] - lastMouse[1];\n\n    const t = progress / 100;\n    let sensitivity: number;\n\n    if (variant === 'wireframe') {\n      sensitivity = t < 0.5 ? 0.5 : 0.25;\n    } else {\n      sensitivity = 0.5;\n    }\n\n    setRotation((prev) => [\n      prev[0] + dx * sensitivity,\n      Math.max(-90, Math.min(90, prev[1] - dy * sensitivity)),\n    ]);\n\n    setLastMouse(currentMouse);\n  };\n\n  const handleMouseUp = () => {\n    setIsDragging(false);\n  };\n\n  const handleMouseLeave = () => {\n    setIsDragging(false);\n  };\n\n  useEffect(() => {\n    if (!svgRef.current || worldData.length === 0 || !isVisible) return;\n    if (useResponsive && dimensions.width === 0) return;\n\n    const svg = d3.select(svgRef.current);\n    svg.selectAll('*').remove();\n\n    let finalCountryFill = 'none';\n    let finalStrokeWidth = strokeWidth;\n    let finalOpacity = 1.0;\n    let renderGraticule = showGraticule;\n    let finalGraticuleOpacity = graticuleOpacity;\n    let finalSphereOutlineWidth = sphereOutlineWidth;\n\n    if (variant === 'wireframe') {\n      finalCountryFill = 'none';\n      finalStrokeWidth = strokeWidth;\n      finalOpacity = 1.0;\n      renderGraticule = showGraticule;\n      finalGraticuleOpacity = graticuleOpacity;\n      finalSphereOutlineWidth = sphereOutlineWidth;\n    } else if (variant === 'wireframesolid') {\n      finalCountryFill = 'none';\n      finalStrokeWidth = strokeWidth;\n      finalOpacity = 1.0;\n      renderGraticule = false;\n      finalGraticuleOpacity = 0;\n      finalSphereOutlineWidth = 1.5;\n    } else if (variant === 'solid') {\n      finalCountryFill = defaultCountryFillColor;\n      finalStrokeWidth = strokeWidth * 0.5;\n      finalOpacity = 0.3;\n      renderGraticule = false;\n      finalGraticuleOpacity = 0;\n      finalSphereOutlineWidth = 1.5;\n    }\n\n    if (defaultBackgroundColor !== 'transparent') {\n      const radius = (Math.min(finalWidth, finalHeight) / 2) * scale * 0.9;\n      svg\n        .append('circle')\n        .attr('cx', finalWidth / 2)\n        .attr('cy', finalHeight / 2)\n        .attr('r', radius)\n        .attr('fill', defaultBackgroundColor);\n    }\n\n    let projection: d3.GeoProjection | CustomProjection;\n    const path = d3.geoPath();\n\n    if (variant === 'wireframe') {\n      const t = progress / 100;\n      const alpha = Math.pow(t, 0.5);\n\n      const baseScale = Math.min(finalWidth, finalHeight) / 2;\n      const scaleRange = d3\n        .scaleLinear()\n        .domain([0, 1])\n        .range([baseScale * 0.9 * scale, baseScale * 0.54 * scale]);\n      const baseRotate = d3.scaleLinear().domain([0, 1]).range([0, 0]);\n\n      projection = interpolateProjection(orthographicRaw, equirectangularRaw)\n        .scale(scaleRange(alpha))\n        .translate([finalWidth / 2, finalHeight / 2])\n        .rotate([baseRotate(alpha) + rotation[0], rotation[1]])\n        .precision(0.1);\n\n      (projection as CustomProjection).alpha(alpha);\n      path.projection(projection);\n    } else {\n      projection = d3\n        .geoOrthographic()\n        .scale((Math.min(finalWidth, finalHeight) / 2) * scale * 0.9)\n        .translate([finalWidth / 2, finalHeight / 2])\n        .rotate([rotation[0], rotation[1]])\n        .precision(0.1);\n\n      path.projection(projection);\n    }\n\n    if (renderGraticule && finalGraticuleOpacity > 0) {\n      try {\n        const graticule = d3.geoGraticule();\n        const graticulePath = path(graticule());\n        if (graticulePath) {\n          svg\n            .append('path')\n            .datum(graticule())\n            .attr('d', graticulePath)\n            .attr('fill', 'none')\n            .attr('stroke', defaultGraticuleColor)\n            .attr('stroke-width', 1)\n            .attr('opacity', finalGraticuleOpacity);\n        }\n      } catch (error) {\n        console.error('Error creating graticule:', error);\n      }\n    }\n\n    svg\n      .selectAll('.country')\n      .data(worldData)\n      .enter()\n      .append('path')\n      .attr('class', 'country')\n      .attr('d', (d: GeoFeature) => {\n        try {\n          const pathString = path(d as unknown as GeoPermissibleObjects);\n          if (!pathString) return '';\n          if (\n            typeof pathString === 'string' &&\n            (pathString.includes('NaN') || pathString.includes('Infinity'))\n          ) {\n            return '';\n          }\n          return pathString;\n        } catch (error) {\n          return '';\n        }\n      })\n      .attr('fill', finalCountryFill)\n      .attr('stroke', defaultStrokeColor)\n      .attr('stroke-width', finalStrokeWidth)\n      .attr('opacity', finalOpacity)\n      // eslint-disable-next-line react-hooks/unsupported-syntax\n      .style('visibility', function (this: SVGPathElement) {\n        const pathData = d3.select(this).attr('d');\n        return pathData && pathData.length > 0 && !pathData.includes('NaN')\n          ? 'visible'\n          : 'hidden';\n      })\n      .on('mouseenter', function (this: SVGPathElement) {\n        if (countryHoverColor && variant === 'solid') {\n          d3.select(this).attr('fill', countryHoverColor);\n        }\n      })\n      .on('mouseleave', function (this: SVGPathElement) {\n        if (variant === 'solid') {\n          d3.select(this).attr('fill', finalCountryFill);\n        }\n      });\n\n    try {\n      const sphereOutline = path({ type: 'Sphere' });\n      if (sphereOutline) {\n        svg\n          .append('path')\n          .datum({ type: 'Sphere' })\n          .attr('d', sphereOutline)\n          .attr('fill', 'none')\n          .attr('stroke', defaultSphereOutlineColor)\n          .attr('stroke-width', finalSphereOutlineWidth)\n          .attr('opacity', variant === 'wireframe' ? 1.0 : 0.8);\n      }\n    } catch (error) {\n      console.error('Error creating sphere outline:', error);\n    }\n  }, [\n    worldData,\n    progress,\n    rotation,\n    isVisible,\n    finalWidth,\n    finalHeight,\n    defaultStrokeColor,\n    strokeWidth,\n    defaultGraticuleColor,\n    graticuleOpacity,\n    defaultSphereOutlineColor,\n    sphereOutlineWidth,\n    showGraticule,\n    defaultCountryFillColor,\n    countryHoverColor,\n    variant,\n    scale,\n    defaultBackgroundColor,\n    useResponsive,\n    dimensions.width,\n  ]);\n\n  return (\n    <div ref={containerRef} className={`relative ${className}`}>\n      <svg\n        ref={svgRef}\n        width={finalWidth}\n        height={finalHeight}\n        onMouseDown={handleMouseDown}\n        onMouseMove={handleMouseMove}\n        onMouseUp={handleMouseUp}\n        onMouseLeave={handleMouseLeave}\n        className={\n          useResponsive\n            ? 'w-full h-full opacity-0 transition-opacity duration-1000'\n            : ''\n        }\n        style={{\n          cursor: enableInteraction\n            ? isDragging\n              ? 'grabbing'\n              : 'grab'\n            : 'default',\n          opacity: useResponsive ? (dimensions.width > 0 ? 1 : 0) : 1,\n        }}\n      />\n    </div>\n  );\n}\n"
    }
  ]
}
