{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dotted-globe",
  "type": "registry:component",
  "title": "DottedGlobe",
  "description": "A 3D Earth: continents as a field of glowing dots sampled from open Natural Earth data, great-circle arcs between cities, and a Fresnel atmosphere. No tiles, no token; composes with bloom.",
  "dependencies": [
    "three",
    "@react-three/fiber",
    "@react-three/drei",
    "@react-three/postprocessing",
    "topojson-client",
    "d3-geo",
    "world-atlas"
  ],
  "registryDependencies": [
    "https://prism.icglabs.co/r/canvas-perf.json"
  ],
  "tier": "pro",
  "note": "Imports world-atlas land-110m.json for continents (bundled, free, no token). Render via next/dynamic ssr:false.",
  "files": [
    {
      "path": "components/prism/DottedGlobe.tsx",
      "content": "\"use client\";\n\nimport { useMemo, useRef } from \"react\";\nimport { Canvas, useFrame } from \"@react-three/fiber\";\nimport { OrbitControls } from \"@react-three/drei\";\nimport { EffectComposer, Bloom } from \"@react-three/postprocessing\";\nimport * as THREE from \"three\";\nimport { feature } from \"topojson-client\";\nimport { geoContains } from \"d3-geo\";\nimport landTopo from \"world-atlas/land-110m.json\";\nimport { useDeviceTier } from \"@/lib/useDeviceTier\";\nimport { AutoDpr, PauseWhenOffscreen } from \"@/components/showcase/CanvasPerf\";\n\n/**\n * DottedGlobe — a 3D Earth: continents rendered as a field of glowing dots\n * (sampled from Natural Earth land data, no tiles/token), glowing great-circle\n * arcs between cities, and a back-side Fresnel atmosphere. All emissive, so it\n * blooms through Prism's post pipeline. Slow auto-rotate; drag to orbit.\n */\nconst R = 2;\n\n// Build the land polygon once (pure data; topojson → GeoJSON). `never` casts keep\n// tsc happy without resolving the topojson-specification types.\nconst topo = landTopo as unknown as { objects: { land: object } };\nconst LAND = feature(topo as never, topo.objects.land as never) as never;\n\nfunction latLngToVec3(lat: number, lng: number, r = R) {\n  const phi = ((90 - lat) * Math.PI) / 180;\n  const th = ((lng + 180) * Math.PI) / 180;\n  return new THREE.Vector3(\n    -r * Math.sin(phi) * Math.cos(th),\n    r * Math.cos(phi),\n    r * Math.sin(phi) * Math.sin(th)\n  );\n}\n\nfunction Dots({ color, samples }: { color: string; samples: number }) {\n  const geo = useMemo(() => {\n    const pos: number[] = [];\n    const golden = Math.PI * (1 + Math.sqrt(5));\n    for (let i = 0; i < samples; i++) {\n      const t = i / samples;\n      const phi = Math.acos(1 - 2 * t);\n      const theta = golden * i;\n      const lat = 90 - (phi * 180) / Math.PI;\n      const lng = (((theta * 180) / Math.PI) % 360) - 180;\n      if (geoContains(LAND, [lng, lat])) {\n        const v = latLngToVec3(lat, lng, R + 0.006);\n        pos.push(v.x, v.y, v.z);\n      }\n    }\n    const g = new THREE.BufferGeometry();\n    g.setAttribute(\"position\", new THREE.Float32BufferAttribute(pos, 3));\n    return g;\n  }, [samples]);\n\n  return (\n    <points geometry={geo}>\n      <pointsMaterial size={0.028} color={color} sizeAttenuation toneMapped={false} />\n    </points>\n  );\n}\n\nfunction Atmosphere({ color }: { color: string }) {\n  const mat = useMemo(\n    () =>\n      new THREE.ShaderMaterial({\n        side: THREE.BackSide,\n        blending: THREE.AdditiveBlending,\n        transparent: true,\n        depthWrite: false,\n        uniforms: { c: { value: new THREE.Color(color) } },\n        vertexShader: `varying vec3 vN; void main(){ vN=normalize(normalMatrix*normal); gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0); }`,\n        fragmentShader: `varying vec3 vN; uniform vec3 c; void main(){ float i=pow(0.72-dot(vN,vec3(0.0,0.0,1.0)),3.0); gl_FragColor=vec4(c,1.0)*i; }`,\n      }),\n    [color]\n  );\n  return (\n    <mesh material={mat}>\n      <sphereGeometry args={[R * 1.18, 48, 48]} />\n    </mesh>\n  );\n}\n\nfunction Arc({ a, b, color }: { a: [number, number]; b: [number, number]; color: string }) {\n  const obj = useMemo(() => {\n    const s = latLngToVec3(a[0], a[1]);\n    const e = latLngToVec3(b[0], b[1]);\n    const mid = s.clone().add(e).multiplyScalar(0.5).setLength(R * 1.5);\n    const pts = new THREE.QuadraticBezierCurve3(s, mid, e).getPoints(60);\n    const geo = new THREE.BufferGeometry().setFromPoints(pts);\n    const mat = new THREE.LineDashedMaterial({\n      color: new THREE.Color(color),\n      dashSize: 0.18,\n      gapSize: 0.12,\n      transparent: true,\n      toneMapped: false,\n    });\n    const line = new THREE.Line(geo, mat);\n    line.computeLineDistances();\n    return line;\n  }, [a, b, color]);\n\n  useFrame(({ clock }) => {\n    (obj.material as THREE.LineDashedMaterial).opacity =\n      0.4 + 0.45 * Math.sin(clock.elapsedTime * 1.4 + a[1] * 0.1);\n  });\n\n  return <primitive object={obj} />;\n}\n\nconst ARCS: { a: [number, number]; b: [number, number] }[] = [\n  { a: [37.77, -122.42], b: [40.71, -74.0] },\n  { a: [51.5, -0.12], b: [35.68, 139.69] },\n  { a: [-33.86, 151.2], b: [1.35, 103.82] },\n  { a: [48.85, 2.35], b: [-23.55, -46.63] },\n];\n\nfunction Scene({ samples, colors }: { samples: number; colors: [string, string, string] }) {\n  const g = useRef<THREE.Group>(null);\n  const reduced =\n    typeof window !== \"undefined\" &&\n    window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n  useFrame((_, d) => {\n    if (g.current && !reduced) g.current.rotation.y += d * 0.06;\n  });\n  return (\n    <group ref={g}>\n      <mesh>\n        <sphereGeometry args={[R, 64, 64]} />\n        <meshBasicMaterial color=\"#0a1120\" />\n      </mesh>\n      <Dots color={colors[0]} samples={samples} />\n      <Atmosphere color={colors[1]} />\n      {ARCS.map((arc, i) => (\n        <Arc key={i} a={arc.a} b={arc.b} color={colors[2]} />\n      ))}\n    </group>\n  );\n}\n\nexport function DottedGlobe({\n  colors = [\"#67e8f9\", \"#38bdf8\", \"#f0abfc\"],\n  className,\n}: {\n  /** [land dots, atmosphere, arcs] hex. */\n  colors?: [string, string, string];\n  className?: string;\n}) {\n  const q = useDeviceTier();\n  const samples = Math.round(11000 * q.scale);\n  return (\n    <div className={className} style={{ width: \"100%\", height: \"100%\" }}>\n      <Canvas dpr={q.dpr} gl={{ antialias: true, powerPreference: \"high-performance\" }} camera={{ position: [0, 0, 6], fov: 45 }}>\n        <color attach=\"background\" args={[\"#05070f\"]} />\n        <Scene samples={samples} colors={colors} />\n        <OrbitControls enableZoom={false} enablePan={false} autoRotate={false} />\n        <AutoDpr />\n        <PauseWhenOffscreen />\n        {q.bloom && (\n          <EffectComposer>\n            <Bloom intensity={1.1} luminanceThreshold={0.12} luminanceSmoothing={0.9} mipmapBlur radius={0.7} />\n          </EffectComposer>\n        )}\n      </Canvas>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/prism/DottedGlobe.tsx"
    }
  ]
}