{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "globe",
  "type": "registry:component",
  "title": "Globe",
  "description": "A dotted globe drawn on a plain canvas, no WebGL, that turns on its own and spins under your finger.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://ui.minidev.pro/r/button.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/premium/globe.tsx",
      "type": "registry:ui",
      "target": "components/ui/globe.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { useReducedMotion } from \"motion/react\"\nimport { ArrowRight } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\n\n/**\n * A dotted globe drawn on a plain canvas, no WebGL, that turns on its own and spins under your finger.\n * Two thousand points sit on a sphere, rotate with simple trig and draw with\n * depth shading. Markers take latitude and longitude, carry labels and are\n * joined by arcs with a pulse travelling along each one. Drag to rotate with\n * inertia, or focus the globe and use the arrow keys. Colours resolve through\n * the cascade; the canvas is sharp on high density screens, sleeps off screen\n * and draws one still frame for reduced motion. Content ignores the pointer\n * where it is empty, so the globe stays draggable beside it.\n */\ntype GlobeMarker = {\n  /** Latitude in degrees, north positive. */\n  lat: number\n  /** Longitude in degrees, east positive. */\n  lng: number\n  /** Label drawn beside the marker. */\n  label?: string\n}\n\ntype GlobeProps = {\n  /** Places to mark, in degrees. Consecutive markers are joined by arcs. */\n  markers?: GlobeMarker[]\n  /** Number of dots on the sphere. */\n  dots?: number\n  /** Idle spin in degrees per second. 0 holds still until dragged. */\n  speed?: number\n  /** Join consecutive markers with arcs. */\n  arcs?: boolean\n  /** Where the globe sits on wide panels. The demo content takes the other side. */\n  align?: \"left\" | \"center\" | \"right\"\n  /** Dot colour. Any CSS colour, including var(--token). Defaults to the muted foreground. */\n  color?: string\n  /** Marker, arc and glow colour. Any CSS colour. Defaults to the accent. */\n  accent?: string\n  className?: string\n  children?: React.ReactNode\n}\n\nconst DEFAULT_MARKERS: GlobeMarker[] = [\n  { lat: 28.61, lng: 77.21, label: \"Delhi\" },\n  { lat: 51.51, lng: -0.13, label: \"London\" },\n  { lat: 37.77, lng: -122.42, label: \"San Francisco\" },\n  { lat: 1.35, lng: 103.82, label: \"Singapore\" },\n  { lat: -23.55, lng: -46.63, label: \"São Paulo\" },\n]\n\n/** Panels narrower than this stack the content above the globe. Matches the @lg container breakpoint. */\nconst NARROW = 512\nconst GOLDEN = Math.PI * (3 - Math.sqrt(5))\nconst DEG = Math.PI / 180\n\ntype RGBA = [number, number, number, number]\n\n/** Unit vector for a place, y up and z toward the viewer at zero rotation. */\nfunction toVector(lat: number, lng: number): [number, number, number] {\n  const la = lat * DEG\n  const lo = lng * DEG\n  return [Math.cos(la) * Math.sin(lo), Math.sin(la), Math.cos(la) * Math.cos(lo)]\n}\n\nfunction Globe({\n  markers = DEFAULT_MARKERS,\n  dots = 2000,\n  speed = 6,\n  arcs = true,\n  align = \"right\",\n  color = \"var(--fg-muted)\",\n  accent = \"var(--accent)\",\n  className,\n  children,\n}: GlobeProps) {\n  const reduce = useReducedMotion()\n  const boxRef = React.useRef<HTMLDivElement>(null)\n  const canvasRef = React.useRef<HTMLCanvasElement>(null)\n  const markerKey = JSON.stringify(markers)\n  const places = markers.map((m) => m.label).filter(Boolean)\n  const label = `Globe${places.length ? ` with markers for ${places.join(\", \")}` : \"\"}. Drag or use the arrow keys to rotate.`\n\n  React.useEffect(() => {\n    const box = boxRef.current\n    const canvas = canvasRef.current\n    const ctx = canvas?.getContext(\"2d\")\n    if (!box || !canvas || !ctx) return\n\n    const marks = JSON.parse(markerKey) as GlobeMarker[]\n    const count = Math.max(0, Math.round(dots))\n    const auto = speed * DEG\n    let w = 0\n    let h = 0\n    let dpr = 1\n    let raf = 0\n    let last = 0\n    let clock = 0\n    let colorAt = 0\n    let visible = true\n    let font = \"sans-serif\"\n\n    // View state.\n    let rotY = -1.1\n    let rotX = 0.4\n    let spin = auto\n    let tiltV = 0\n    let dragging = false\n    let lastX = 0\n    let lastY = 0\n    let lastT = 0\n    let velX = 0\n    let velY = 0\n\n    // Points on a Fibonacci sphere: even spacing, no clumping at the poles.\n    const pts = new Float32Array(count * 3)\n    for (let i = 0; i < count; i++) {\n      const y = 1 - (2 * (i + 0.5)) / count\n      const rr = Math.sqrt(1 - y * y)\n      const th = i * GOLDEN\n      pts[i * 3] = Math.cos(th) * rr\n      pts[i * 3 + 1] = y\n      pts[i * 3 + 2] = Math.sin(th) * rr\n    }\n    const marksV = marks.map((m) => toVector(m.lat, m.lng))\n\n    // Colours resolve through the cascade into rgba, so tokens follow theme and material.\n    // No transition on the canvas colour, or a reduced-motion stylesheet can hand back a mid-transition value.\n    canvas.style.setProperty(\"transition\", \"none\", \"important\")\n    const probe = document.createElement(\"canvas\")\n    probe.width = probe.height = 1\n    const pctx = probe.getContext(\"2d\", { willReadFrequently: true })\n    const rgba = (css: string): RGBA => {\n      canvas.style.color = css\n      const resolved = getComputedStyle(canvas).color || \"currentColor\"\n      if (!pctx) return [128, 128, 128, 1]\n      pctx.clearRect(0, 0, 1, 1)\n      pctx.fillStyle = \"rgb(128 128 128)\"\n      pctx.fillStyle = resolved\n      pctx.fillRect(0, 0, 1, 1)\n      const d = pctx.getImageData(0, 0, 1, 1).data\n      return [d[0], d[1], d[2], d[3] / 255]\n    }\n    const col = (c: RGBA, a = 1) => `rgba(${c[0]},${c[1]},${c[2]},${Math.max(0, Math.min(1, a * c[3]))})`\n    let cDot: RGBA = [128, 128, 128, 1]\n    let cAccent: RGBA = [128, 128, 128, 1]\n    let cAccent2: RGBA = [128, 128, 128, 1]\n    let cFg: RGBA = [128, 128, 128, 1]\n    let cRaised: RGBA = [255, 255, 255, 1]\n    let cSunken: RGBA = [240, 240, 240, 1]\n    let cBorder: RGBA = [200, 200, 200, 1]\n    let cRim: RGBA = [180, 180, 180, 1]\n    const readColors = () => {\n      cDot = rgba(color)\n      cAccent = rgba(accent)\n      cAccent2 = rgba(\"var(--accent-2)\")\n      cFg = rgba(\"var(--fg)\")\n      cRaised = rgba(\"var(--raised)\")\n      cSunken = rgba(\"var(--sunken)\")\n      cBorder = rgba(\"var(--border)\")\n      cRim = rgba(\"var(--border-strong)\")\n      font = getComputedStyle(box).fontFamily || \"sans-serif\"\n    }\n\n    const resize = () => {\n      const r = box.getBoundingClientRect()\n      w = Math.max(1, Math.round(r.width))\n      h = Math.max(1, Math.round(r.height))\n      dpr = Math.min(window.devicePixelRatio || 1, 2)\n      canvas.width = Math.round(w * dpr)\n      canvas.height = Math.round(h * dpr)\n    }\n\n    const layout = () => {\n      if (w < NARROW) return { cx: w / 2, cy: h * 0.98, r: Math.min(w * 0.46, h * 0.72) }\n      const r = Math.min(h * 0.44, w * 0.25)\n      const cx = align === \"center\" ? w / 2 : align === \"right\" ? w * 0.74 : w * 0.26\n      return { cx, cy: h / 2, r }\n    }\n\n    const rotate = (x: number, y: number, z: number, out: number[]) => {\n      const sy = Math.sin(rotY)\n      const cy = Math.cos(rotY)\n      const sx = Math.sin(rotX)\n      const cx = Math.cos(rotX)\n      const x1 = x * cy + z * sy\n      const z1 = -x * sy + z * cy\n      out[0] = x1\n      out[1] = y * cx - z1 * sx\n      out[2] = y * sx + z1 * cx\n    }\n\n    const tmp = [0, 0, 0]\n    const draw = (t: number) => {\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0)\n      ctx.clearRect(0, 0, w, h)\n      const { cx, cy, r } = layout()\n      if (r < 8) return\n\n      // Atmosphere.\n      const halo = ctx.createRadialGradient(cx, cy, r * 0.98, cx, cy, r * 1.14)\n      halo.addColorStop(0, col(cAccent, 0.26))\n      halo.addColorStop(0.45, col(cAccent, 0.07))\n      halo.addColorStop(1, col(cAccent, 0))\n      ctx.fillStyle = halo\n      ctx.beginPath()\n      ctx.arc(cx, cy, r * 1.14, 0, Math.PI * 2)\n      ctx.fill()\n\n      // The body, lit from the top left.\n      const body = ctx.createRadialGradient(cx - r * 0.42, cy - r * 0.48, r * 0.06, cx, cy, r)\n      body.addColorStop(0, col(cRaised, 1))\n      body.addColorStop(0.55, col(cRaised, 0.7))\n      body.addColorStop(1, col(cSunken, 1))\n      ctx.fillStyle = body\n      ctx.beginPath()\n      ctx.arc(cx, cy, r, 0, Math.PI * 2)\n      ctx.fill()\n      ctx.strokeStyle = col(cRim, 1)\n      ctx.lineWidth = 1\n      ctx.stroke()\n\n      // Dots, batched into depth buckets so each bucket is one fill.\n      const base = Math.max(0.7, r / 112)\n      const buckets = 6\n      for (let b = 0; b < buckets; b++) {\n        const lo = b / buckets\n        const hi = (b + 1) / buckets\n        const mid = (lo + hi) / 2\n        ctx.beginPath()\n        const rad = base * (0.55 + 0.6 * mid)\n        for (let i = 0; i < count; i++) {\n          rotate(pts[i * 3], pts[i * 3 + 1], pts[i * 3 + 2], tmp)\n          const z = tmp[2]\n          if (z < lo || (z >= hi && b < buckets - 1)) continue\n          const sx = cx + r * tmp[0]\n          const sy = cy - r * tmp[1]\n          ctx.moveTo(sx + rad, sy)\n          ctx.arc(sx, sy, rad, 0, Math.PI * 2)\n        }\n        ctx.fillStyle = col(cDot, 0.14 + 0.72 * mid)\n        ctx.fill()\n      }\n\n      // Project the markers once.\n      const proj = marksV.map((v) => {\n        rotate(v[0], v[1], v[2], tmp)\n        return { x: cx + r * tmp[0], y: cy - r * tmp[1], z: tmp[2] }\n      })\n\n      // Arcs between consecutive markers, lifted off the surface, hidden behind the body.\n      if (arcs && marksV.length > 1) {\n        const segs = 40\n        for (let i = 0; i < marksV.length - 1; i++) {\n          const a = marksV[i]\n          const b = marksV[i + 1]\n          const dot = Math.max(-1, Math.min(1, a[0] * b[0] + a[1] * b[1] + a[2] * b[2]))\n          const ang = Math.acos(dot)\n          if (ang < 1e-4) continue\n          const lift = 0.04 + 0.12 * (ang / Math.PI)\n          const sinA = Math.sin(ang)\n          const pt = (k: number) => {\n            const wa = Math.sin((1 - k) * ang) / sinA\n            const wb = Math.sin(k * ang) / sinA\n            const m = 1 + lift * Math.sin(Math.PI * k)\n            rotate((a[0] * wa + b[0] * wb) * m, (a[1] * wa + b[1] * wb) * m, (a[2] * wa + b[2] * wb) * m, tmp)\n            // Routes show on the near half only, so nothing floats past the limb.\n            return { x: cx + r * tmp[0], y: cy - r * tmp[1], ok: tmp[2] > -0.04 }\n          }\n          const trace = (from: number, to: number, width: number, style: string) => {\n            ctx.beginPath()\n            let open = false\n            const n = Math.max(2, Math.round(segs * (to - from)))\n            for (let s = 0; s <= n; s++) {\n              const p = pt(from + ((to - from) * s) / n)\n              if (p.ok) {\n                if (open) ctx.lineTo(p.x, p.y)\n                else ctx.moveTo(p.x, p.y)\n                open = true\n              } else open = false\n            }\n            ctx.lineWidth = width\n            ctx.strokeStyle = style\n            ctx.stroke()\n          }\n          trace(0, 1, 1, col(cAccent, 0.5))\n          if (!reduce) {\n            // A pulse travels each route, with a short tail behind it.\n            const head = (t * 0.11 + i * 0.29) % 1\n            const tail = Math.max(0, head - 0.14)\n            if (head > 0.01) trace(tail, head, 1.8, col(cAccent2, 0.95))\n            const hp = pt(head)\n            if (hp.ok) {\n              ctx.beginPath()\n              ctx.arc(hp.x, hp.y, 2.2, 0, Math.PI * 2)\n              ctx.fillStyle = col(cAccent2, 1)\n              ctx.fill()\n            }\n          }\n        }\n      }\n\n      // Markers, back to front, with labels that stay inside the canvas.\n      ctx.font = `500 11px ${font}`\n      ctx.textBaseline = \"middle\"\n      const order = proj.map((p, i) => ({ p, i })).filter(({ p }) => p.z > -0.02).sort((a, b) => a.p.z - b.p.z)\n      for (const { p, i } of order) {\n        const vis = Math.min(1, Math.max(0.1, p.z * 3 + 0.15))\n        if (!reduce) {\n          const ph = (t * 0.55 + i * 0.23) % 1\n          ctx.beginPath()\n          ctx.arc(p.x, p.y, 4 + 13 * ph, 0, Math.PI * 2)\n          ctx.lineWidth = 1\n          ctx.strokeStyle = col(cAccent, (1 - ph) * 0.55 * vis)\n          ctx.stroke()\n        }\n        ctx.beginPath()\n        ctx.arc(p.x, p.y, 6.5, 0, Math.PI * 2)\n        ctx.fillStyle = col(cAccent, 0.22 * vis)\n        ctx.fill()\n        ctx.beginPath()\n        ctx.arc(p.x, p.y, 3.2, 0, Math.PI * 2)\n        ctx.fillStyle = col(cAccent, vis)\n        ctx.fill()\n        ctx.beginPath()\n        ctx.arc(p.x, p.y, 1.2, 0, Math.PI * 2)\n        ctx.fillStyle = col(cRaised, vis)\n        ctx.fill()\n        const text = marks[i].label\n        if (!text) continue\n        const tw = ctx.measureText(text).width\n        const pw = tw + 14\n        const ph = 20\n        let lx = p.x + 11\n        if (lx + pw > w - 6) lx = p.x - 11 - pw\n        const ly = p.y - ph / 2\n        ctx.beginPath()\n        ctx.roundRect(lx, ly, pw, ph, 6)\n        ctx.fillStyle = col(cRaised, 0.94 * vis)\n        ctx.fill()\n        ctx.lineWidth = 1\n        ctx.strokeStyle = col(cBorder, vis)\n        ctx.stroke()\n        ctx.fillStyle = col(cFg, vis)\n        ctx.fillText(text, lx + 7, ly + ph / 2 + 0.5)\n      }\n    }\n\n    const step = (dt: number) => {\n      if (dragging) return\n      // Inertia eases back into the idle spin; tilt settles where it was left.\n      spin = auto + (spin - auto) * Math.exp(-dt * 1.6)\n      tiltV *= Math.exp(-dt * 3)\n      rotY += spin * dt\n      rotX = Math.max(-1.2, Math.min(1.2, rotX + tiltV * dt))\n    }\n\n    const loop = (t: number) => {\n      const dt = last ? Math.min(0.05, (t - last) / 1000) : 0\n      last = t\n      clock += dt\n      if (t - colorAt > 1000) {\n        colorAt = t\n        readColors()\n      }\n      step(dt)\n      draw(clock)\n      raf = requestAnimationFrame(loop)\n    }\n    const stop = () => {\n      cancelAnimationFrame(raf)\n      raf = 0\n      last = 0\n    }\n    const sync = () => {\n      if (reduce) {\n        stop()\n        draw(0)\n        return\n      }\n      const run = visible && !document.hidden\n      if (run && !raf) raf = requestAnimationFrame(loop)\n      if (!run && raf) stop()\n    }\n\n    const onDown = (e: PointerEvent) => {\n      if (e.button !== 0 && e.pointerType === \"mouse\") return\n      dragging = true\n      lastX = e.clientX\n      lastY = e.clientY\n      lastT = performance.now()\n      velX = 0\n      velY = 0\n      spin = 0\n      tiltV = 0\n      canvas.setPointerCapture(e.pointerId)\n      canvas.dataset.dragging = \"true\"\n    }\n    const onMove = (e: PointerEvent) => {\n      if (!dragging) return\n      const { r } = layout()\n      const now = performance.now()\n      const dt = Math.max(0.004, (now - lastT) / 1000)\n      const dx = (e.clientX - lastX) / r\n      const dy = (e.clientY - lastY) / r\n      lastX = e.clientX\n      lastY = e.clientY\n      lastT = now\n      rotY += dx\n      rotX = Math.max(-1.2, Math.min(1.2, rotX + dy))\n      velX = velX * 0.4 + (dx / dt) * 0.6\n      velY = velY * 0.4 + (dy / dt) * 0.6\n      if (reduce) draw(0)\n    }\n    const onUp = (e: PointerEvent) => {\n      if (!dragging) return\n      dragging = false\n      delete canvas.dataset.dragging\n      if (canvas.hasPointerCapture(e.pointerId)) canvas.releasePointerCapture(e.pointerId)\n      // A flick keeps turning; a held release just rests, unless reduced motion asks for stillness.\n      const stale = performance.now() - lastT > 80\n      spin = reduce || stale ? 0 : Math.max(-7, Math.min(7, velX))\n      tiltV = reduce || stale ? 0 : Math.max(-5, Math.min(5, velY))\n      if (reduce) draw(0)\n    }\n    const onKey = (e: KeyboardEvent) => {\n      const k = e.key\n      if (k !== \"ArrowLeft\" && k !== \"ArrowRight\" && k !== \"ArrowUp\" && k !== \"ArrowDown\") return\n      e.preventDefault()\n      if (k === \"ArrowLeft\") rotY -= 0.22\n      if (k === \"ArrowRight\") rotY += 0.22\n      if (k === \"ArrowUp\") rotX = Math.max(-1.2, rotX - 0.18)\n      if (k === \"ArrowDown\") rotX = Math.min(1.2, rotX + 0.18)\n      if (reduce || !raf) draw(clock)\n    }\n\n    readColors()\n    resize()\n    const ro = new ResizeObserver(() => {\n      resize()\n      draw(clock)\n    })\n    const io = new IntersectionObserver(([e]) => {\n      visible = e.isIntersecting\n      sync()\n    })\n    // Theme and material can change on any ancestor (a docs stage, a section), so watch the whole tree, one read per frame.\n    let pending = 0\n    const mo = new MutationObserver(() => {\n      if (pending) return\n      pending = requestAnimationFrame(() => {\n        pending = 0\n        readColors()\n        if (!raf) draw(clock)\n      })\n    })\n    ro.observe(box)\n    io.observe(box)\n    mo.observe(document.documentElement, { attributes: true, subtree: true, attributeFilter: [\"class\", \"data-material\", \"data-theme\"] })\n    document.addEventListener(\"visibilitychange\", sync)\n    canvas.addEventListener(\"pointerdown\", onDown)\n    canvas.addEventListener(\"pointermove\", onMove)\n    canvas.addEventListener(\"pointerup\", onUp)\n    canvas.addEventListener(\"pointercancel\", onUp)\n    canvas.addEventListener(\"keydown\", onKey)\n    sync()\n\n    return () => {\n      stop()\n      cancelAnimationFrame(pending)\n      ro.disconnect()\n      io.disconnect()\n      mo.disconnect()\n      document.removeEventListener(\"visibilitychange\", sync)\n      canvas.removeEventListener(\"pointerdown\", onDown)\n      canvas.removeEventListener(\"pointermove\", onMove)\n      canvas.removeEventListener(\"pointerup\", onUp)\n      canvas.removeEventListener(\"pointercancel\", onUp)\n      canvas.removeEventListener(\"keydown\", onKey)\n    }\n  }, [markerKey, dots, speed, arcs, align, color, accent, reduce])\n\n  return (\n    <div\n      ref={boxRef}\n      data-slot=\"globe\"\n      className={cn(\"@container relative isolate h-[360px] w-full overflow-hidden rounded-2xl border border-border bg-bg\", className)}\n    >\n      <canvas\n        ref={canvasRef}\n        role=\"img\"\n        aria-label={label}\n        tabIndex={0}\n        data-slot=\"globe-canvas\"\n        className=\"absolute inset-0 z-0 size-full cursor-grab touch-pan-y outline-none select-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-inset data-[dragging=true]:cursor-grabbing\"\n      />\n      <div className=\"pointer-events-none relative z-10 flex h-full items-start @lg:items-center [&>*]:pointer-events-auto\">\n        {children ?? (\n          <div\n            className={cn(\n              \"flex flex-col items-start gap-4 p-6 text-left @lg:p-10\",\n              align === \"center\" && \"@lg:mx-auto @lg:items-center @lg:text-center\",\n              align === \"left\" && \"@lg:ml-auto @lg:w-1/2\",\n              align === \"right\" && \"@lg:w-1/2\"\n            )}\n          >\n            <span className=\"rounded-full border border-accent-line bg-accent-soft px-2.5 py-0.5 text-xs font-medium text-accent-fg\">\n              Lumen worldwide\n            </span>\n            <p className=\"max-w-[16ch] text-3xl font-medium tracking-[-0.035em] text-balance text-fg\">\n              Invoices in 140 currencies, settled locally\n            </p>\n            <Button>\n              See coverage\n              <ArrowRight />\n            </Button>\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n\nexport { Globe }\nexport type { GlobeProps, GlobeMarker }\n"
    }
  ]
}
