{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "3d-background",
  "type": "registry:ui",
  "title": "3D Globe Background",
  "description": "An interactive WebGL globe with country borders, built with three.js.",
  "files": [
    {
      "path": "src/registry/backgrounds/3d-background.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { useEffect, useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport { Canvas, useFrame } from \"@react-three/fiber\";\nimport { OrbitControls } from \"@react-three/drei\";\nimport * as THREE from \"three\";\nimport { FloatingDots } from \"@/components/ui/floating-dots\";\nimport { cn } from \"@/lib/utils\";\n\ntype Position = number[];\ntype Ring = Position[];\ntype CountryGeometry =\n  | { type: \"Polygon\"; coordinates: Ring[] }\n  | { type: \"MultiPolygon\"; coordinates: Ring[][] };\ntype CountryCollection = {\n  features?: { geometry?: CountryGeometry | null }[];\n};\n\nexport type GlobeBackgroundProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /**\n   * URL of a countries GeoJSON FeatureCollection (Polygon/MultiPolygon features) used to draw borders.\n   * You must host this file yourself, e.g. in `public/`. Get one from\n   * https://github.com/nvkelso/natural-earth-vector (110m admin-0 countries) or any countries GeoJSON.\n   * If it fails to load, the globe renders without borders.\n   * @default \"/countries-110m.geojson\"\n   */\n  geoJsonUrl?: string;\n  /** Globe surface color (hex, rgb(), hsl() or a named color). @default \"#182b4b\" */\n  globeColor?: string;\n  /** Country border color (hex, rgb(), hsl() or a named color). @default \"#ffffff\" */\n  lineColor?: string;\n  /** Auto-rotation speed in radians per second. @default 0.15 */\n  rotationSpeed?: number;\n  /** Let users drag to rotate the globe. Blocks touch scrolling over it, so it is off by default. @default false */\n  interactive?: boolean;\n  /** Overlay drifting dots behind the globe. @default true */\n  showDots?: boolean;\n};\n\nconst GLOBE_RADIUS = 1;\nconst BORDER_RADIUS = 1.005;\n// Initial orientation: India faces the viewer.\nconst FOCUS_LON = THREE.MathUtils.degToRad(78.9629);\nconst FOCUS_LAT = THREE.MathUtils.degToRad(20.5937);\nconst MAX_FRAME_DELTA = 0.1;\n\nconst reducedMotionQuery = \"(prefers-reduced-motion: reduce)\";\nfunction subscribeReducedMotion(onChange: () => void) {\n  const mql = window.matchMedia(reducedMotionQuery);\n  mql.addEventListener(\"change\", onChange);\n  return () => mql.removeEventListener(\"change\", onChange);\n}\nconst getReducedMotion = () => window.matchMedia(reducedMotionQuery).matches;\nconst getServerReducedMotion = () => false;\n\n/** Flattens every ring of every country into line-segment pairs on a sphere. */\nfunction buildBorderPositions(collection: CountryCollection): Float32Array {\n  const rings: Ring[] = [];\n  for (const feature of collection.features ?? []) {\n    const geometry = feature.geometry;\n    if (geometry?.type === \"Polygon\") rings.push(...geometry.coordinates);\n    else if (geometry?.type === \"MultiPolygon\") {\n      for (const polygon of geometry.coordinates) rings.push(...polygon);\n    }\n  }\n\n  let segments = 0;\n  for (const ring of rings) segments += Math.max(ring.length - 1, 0);\n\n  const positions = new Float32Array(segments * 6);\n  const point = new THREE.Vector3();\n  let offset = 0;\n  for (const ring of rings) {\n    for (let i = 0; i < ring.length; i++) {\n      const [lon, lat] = ring[i];\n      point.setFromSphericalCoords(\n        BORDER_RADIUS,\n        THREE.MathUtils.degToRad(90 - lat),\n        THREE.MathUtils.degToRad(lon),\n      );\n      // End of the previous segment, then start of the next one.\n      if (i > 0) {\n        point.toArray(positions, offset);\n        offset += 3;\n      }\n      if (i < ring.length - 1) {\n        point.toArray(positions, offset);\n        offset += 3;\n      }\n    }\n  }\n  return positions;\n}\n\nfunction Globe({\n  borders,\n  globeColor,\n  lineColor,\n  rotationSpeed,\n  animate,\n}: {\n  borders: Float32Array | null;\n  globeColor: string;\n  lineColor: string;\n  rotationSpeed: number;\n  animate: boolean;\n}) {\n  const spinRef = useRef<THREE.Group>(null);\n\n  useFrame((_, delta) => {\n    if (!animate || !spinRef.current) return;\n    spinRef.current.rotation.y += Math.min(delta, MAX_FRAME_DELTA) * rotationSpeed;\n  });\n\n  return (\n    <group rotation={[FOCUS_LAT, 0, 0]}>\n      <group ref={spinRef} rotation={[0, -FOCUS_LON, 0]}>\n        <mesh>\n          <sphereGeometry args={[GLOBE_RADIUS * 0.97, 64, 64]} />\n          <meshPhongMaterial\n            color={globeColor}\n            specular=\"#204080\"\n            shininess={5}\n            transparent\n            opacity={0.95}\n          />\n        </mesh>\n        {borders && borders.length > 0 && (\n          <lineSegments>\n            <bufferGeometry>\n              <bufferAttribute attach=\"attributes-position\" args={[borders, 3]} />\n            </bufferGeometry>\n            <lineBasicMaterial color={lineColor} transparent opacity={0.8} />\n          </lineSegments>\n        )}\n      </group>\n    </group>\n  );\n}\n\n/**\n * A slowly rotating 3D globe with country borders over a dark backdrop. Fills its nearest positioned ancestor.\n * Country borders are loaded from `geoJsonUrl`, which you need to host yourself (see the prop docs).\n * Rendering pauses offscreen; auto-rotation stops when reduced motion is preferred.\n */\nexport function GlobeBackground({\n  geoJsonUrl = \"/countries-110m.geojson\",\n  globeColor = \"#182b4b\",\n  lineColor = \"#ffffff\",\n  rotationSpeed = 0.15,\n  interactive = false,\n  showDots = true,\n  className,\n  ...props\n}: GlobeBackgroundProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const [visible, setVisible] = useState(true);\n  const [countries, setCountries] = useState<CountryCollection | null>(null);\n  const reducedMotion = useSyncExternalStore(\n    subscribeReducedMotion,\n    getReducedMotion,\n    getServerReducedMotion,\n  );\n\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n    const observer = new IntersectionObserver(([entry]) => setVisible(entry.isIntersecting));\n    observer.observe(root);\n    return () => observer.disconnect();\n  }, []);\n\n  useEffect(() => {\n    const controller = new AbortController();\n    fetch(geoJsonUrl, { signal: controller.signal })\n      .then((response) => {\n        if (!response.ok) throw new Error(`Failed to load ${geoJsonUrl}: ${response.status}`);\n        return response.json() as Promise<CountryCollection>;\n      })\n      .then(setCountries)\n      .catch(() => {\n        // Aborted or unavailable: render the globe without borders.\n        if (!controller.signal.aborted) setCountries(null);\n      });\n    return () => controller.abort();\n  }, [geoJsonUrl]);\n\n  const borders = useMemo(() => (countries ? buildBorderPositions(countries) : null), [countries]);\n  const animate = visible && !reducedMotion;\n\n  return (\n    <div\n      ref={rootRef}\n      aria-hidden\n      data-slot=\"3d-background\"\n      className={cn(\n        \"pointer-events-none absolute inset-0 overflow-hidden bg-[radial-gradient(ellipse_at_center,#0b1a3a_0%,#020617_70%)] text-white\",\n        className,\n      )}\n      {...props}\n    >\n      {showDots && <FloatingDots maxRadius={0.5} minSpeed={0.1} maxSpeed={0.8} />}\n      <div className={cn(\"absolute inset-0\", interactive && \"pointer-events-auto\")}>\n        <Canvas\n          dpr={[1, 1.5]}\n          frameloop={animate ? \"always\" : \"demand\"}\n          camera={{ position: [0, 0.4, 2.2], fov: 75 }}\n          onCreated={({ camera }) => camera.lookAt(0, 0.2, 0)}\n        >\n          <ambientLight intensity={0.75} />\n          <directionalLight position={[3, 3, 3]} intensity={1.5} />\n          <Globe\n            borders={borders}\n            globeColor={globeColor}\n            lineColor={lineColor}\n            rotationSpeed={rotationSpeed}\n            animate={animate}\n          />\n          {interactive && (\n            <OrbitControls\n              enableZoom={false}\n              enablePan={false}\n              rotateSpeed={0.3}\n              target={[0, 0.2, 0]}\n            />\n          )}\n        </Canvas>\n      </div>\n    </div>\n  );\n}\n"
    }
  ],
  "dependencies": [
    "@react-three/drei",
    "@react-three/fiber",
    "three"
  ],
  "registryDependencies": [
    "https://ui.devsloka.in/r/floating-dots.json"
  ]
}
