{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "globe-demo",
  "type": "registry:component",
  "dependencies": [
    "cobe",
    "motion",
    "next-themes"
  ],
  "registryDependencies": [
    "globe"
  ],
  "files": [
    {
      "path": "registry/default/example/globe-demo.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport createGlobe from \"cobe\"\nimport { useInView, useReducedMotion } from \"motion/react\"\nimport { useTheme } from \"next-themes\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  getShowcaseArcs,\n  getShowcaseMarkers,\n  showcaseConfigs,\n  showcaseDefaultArcs,\n  showcaseDefaultMarkers,\n} from \"@/registry/default/ui/globe\"\n\nconst DEFAULT_KEY = \"default\" as const\n\n// ---------------------------------------------------------------------------\n// Theme → RGB for the WebGL globe (dark mode only; light matches the default demo look)\n// ---------------------------------------------------------------------------\n\nconst CSS_RGB_RE = /rgba?\\(\\s*([\\d.]+)\\s*,\\s*([\\d.]+)\\s*,\\s*([\\d.]+)/i\n\nfunction probeCssColorToRgb01(color: string): [number, number, number] {\n  if (typeof document === \"undefined\") {\n    return [0.5, 0.5, 0.5]\n  }\n  const el = document.createElement(\"div\")\n  el.style.color = color\n  el.style.position = \"fixed\"\n  el.style.left = \"0\"\n  el.style.top = \"0\"\n  el.style.opacity = \"0\"\n  el.style.pointerEvents = \"none\"\n  document.body.appendChild(el)\n  const rgb = getComputedStyle(el).color\n  document.body.removeChild(el)\n  const m = rgb.match(CSS_RGB_RE)\n  if (!m) {\n    return [0.5, 0.5, 0.5]\n  }\n  return [Number(m[1]) / 255, Number(m[2]) / 255, Number(m[3]) / 255]\n}\n\nfunction cssVarTriplet(name: `--${string}`): [number, number, number] {\n  return probeCssColorToRgb01(`var(${name})`)\n}\n\nfunction clamp01(n: number) {\n  return Math.min(1, Math.max(0, n))\n}\n\nfunction saturateRgb(\n  rgb: [number, number, number],\n  amount: number\n): [number, number, number] {\n  const lum = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]\n  return [\n    clamp01(lum + (rgb[0] - lum) * amount),\n    clamp01(lum + (rgb[1] - lum) * amount),\n    clamp01(lum + (rgb[2] - lum) * amount),\n  ]\n}\n\nfunction useResolvedIsDark(): boolean {\n  const { resolvedTheme } = useTheme()\n  return resolvedTheme === \"dark\"\n}\n\ntype CobeGlobeHandle = {\n  update: (state: Record<string, unknown>) => void\n  destroy: () => void\n}\n\nfunction anchorLabelStyle(\n  id: string,\n  kind: \"marker\" | \"arc\"\n): React.CSSProperties {\n  const visible =\n    kind === \"marker\" ? `--cobe-visible-${id}` : `--cobe-visible-arc-${id}`\n  const anchor = kind === \"marker\" ? `--cobe-${id}` : `--cobe-arc-${id}`\n  return {\n    position: \"absolute\",\n    bottom: \"anchor(top)\",\n    filter: `blur(calc((1 - var(${visible}, 0)) * 8px))`,\n    left: \"anchor(center)\",\n    opacity: `var(${visible}, 0)`,\n    positionAnchor: anchor,\n    transform: \"translateX(-50%)\",\n    transition: \"opacity 0.3s ease, filter 0.3s ease\",\n  } as React.CSSProperties\n}\n\nconst demoConfig = showcaseConfigs[DEFAULT_KEY]\n\n// ---------------------------------------------------------------------------\n// Default showcase globe (interactive COBE + CSS anchor labels)\n// ---------------------------------------------------------------------------\n\nexport interface DefaultShowcaseGlobeProps {\n  className?: string\n  globeSize?: number\n}\n\nexport function DefaultShowcaseGlobe({\n  className,\n  globeSize = 520,\n}: DefaultShowcaseGlobeProps) {\n  const canvasRef = React.useRef<HTMLCanvasElement>(null)\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  const isInView = useInView(rootRef, { once: false, margin: \"120px\" })\n  const shouldReduceMotion = useReducedMotion()\n  const isDark = useResolvedIsDark()\n  const phiAutoRef = React.useRef(0)\n  const pointerInteracting = React.useRef<{ x: number; y: number } | null>(null)\n  const lastPointer = React.useRef<{ x: number; y: number; t: number } | null>(\n    null\n  )\n  const dragOffset = React.useRef({ phi: 0, theta: 0 })\n  const velocity = React.useRef({ phi: 0, theta: 0 })\n  const phiOffsetRef = React.useRef(0)\n  const thetaOffsetRef = React.useRef(0)\n  const isPausedRef = React.useRef(false)\n  const speedRef = React.useRef(1)\n  const bufferSize = globeSize * 2\n\n  const cobeMarkers = React.useMemo(\n    () => getShowcaseMarkers(DEFAULT_KEY, demoConfig.markerSize),\n    []\n  )\n  const cobeArcs = React.useMemo(() => getShowcaseArcs(DEFAULT_KEY), [])\n\n  const handlePointerDown = React.useCallback(\n    (e: React.PointerEvent<HTMLCanvasElement>) => {\n      pointerInteracting.current = { x: e.clientX, y: e.clientY }\n      if (canvasRef.current) {\n        canvasRef.current.style.cursor = \"grabbing\"\n      }\n      isPausedRef.current = true\n    },\n    []\n  )\n\n  const handlePointerMove = React.useCallback((e: PointerEvent) => {\n    if (pointerInteracting.current === null) {\n      return\n    }\n    const deltaX = e.clientX - pointerInteracting.current.x\n    const deltaY = e.clientY - pointerInteracting.current.y\n    dragOffset.current = { phi: deltaX / 300, theta: deltaY / 1000 }\n\n    const now = Date.now()\n    if (lastPointer.current) {\n      const dt = Math.max(now - lastPointer.current.t, 1)\n      const maxVelocity = 0.15\n      velocity.current = {\n        phi: Math.max(\n          -maxVelocity,\n          Math.min(\n            maxVelocity,\n            ((e.clientX - lastPointer.current.x) / dt) * 0.3\n          )\n        ),\n        theta: Math.max(\n          -maxVelocity,\n          Math.min(\n            maxVelocity,\n            ((e.clientY - lastPointer.current.y) / dt) * 0.08\n          )\n        ),\n      }\n    }\n    lastPointer.current = { x: e.clientX, y: e.clientY, t: now }\n  }, [])\n\n  const handlePointerUp = React.useCallback(() => {\n    if (pointerInteracting.current !== null) {\n      phiOffsetRef.current += dragOffset.current.phi\n      thetaOffsetRef.current += dragOffset.current.theta\n      dragOffset.current = { phi: 0, theta: 0 }\n      lastPointer.current = null\n    }\n    pointerInteracting.current = null\n    if (canvasRef.current) {\n      canvasRef.current.style.cursor = \"grab\"\n    }\n    isPausedRef.current = false\n  }, [])\n\n  React.useEffect(() => {\n    window.addEventListener(\"pointermove\", handlePointerMove, { passive: true })\n    window.addEventListener(\"pointerup\", handlePointerUp, { passive: true })\n    return () => {\n      window.removeEventListener(\"pointermove\", handlePointerMove)\n      window.removeEventListener(\"pointerup\", handlePointerUp)\n    }\n  }, [handlePointerMove, handlePointerUp])\n\n  React.useEffect(() => {\n    if (!(canvasRef.current && isInView)) {\n      return\n    }\n\n    const canvas = canvasRef.current\n    const logicalSize = bufferSize\n    const dpr = Math.min(\n      window.devicePixelRatio || 1,\n      window.innerWidth < 640 ? 1.8 : 2\n    )\n\n    let baseColor: [number, number, number]\n    let markerColor: [number, number, number]\n    let arcColor: [number, number, number]\n    let glowColor: [number, number, number]\n    let dark: number\n    let mapBrightness: number\n    let opacity: number\n\n    if (isDark) {\n      const cardRgb = cssVarTriplet(\"--card\")\n      const mutedRgb = cssVarTriplet(\"--muted\")\n      baseColor = [\n        cardRgb[0] * 0.52 + 0.03,\n        cardRgb[1] * 0.55 + 0.05,\n        Math.min(0.96, cardRgb[2] * 0.6 + 0.1),\n      ]\n      markerColor = saturateRgb(cssVarTriplet(\"--chart-3\"), 1.28)\n      arcColor = saturateRgb(cssVarTriplet(\"--chart-4\"), 1.18)\n      glowColor = mutedRgb.map((c) => Math.min(1, c * 0.55)) as [\n        number,\n        number,\n        number,\n      ]\n      dark = 1\n      mapBrightness = 6.8\n      opacity = 0.88\n    } else {\n      baseColor = demoConfig.baseColor\n      markerColor = demoConfig.markerColor\n      arcColor = demoConfig.arcColor\n      glowColor = [0.94, 0.93, 0.91]\n      dark = demoConfig.dark\n      mapBrightness = demoConfig.mapBrightness\n      opacity = 0.72\n    }\n\n    const globe = createGlobe(canvas, {\n      devicePixelRatio: dpr,\n      width: logicalSize,\n      height: logicalSize,\n      phi: phiAutoRef.current + phiOffsetRef.current,\n      theta: demoConfig.theta + thetaOffsetRef.current,\n      dark,\n      diffuse: 1.5,\n      mapSamples: 16_000,\n      mapBrightness,\n      baseColor,\n      markerColor,\n      glowColor,\n      scale: 1,\n      offset: [0, 0],\n      opacity,\n      markers: cobeMarkers,\n      arcs: cobeArcs,\n      arcColor,\n      arcWidth: 0.5,\n      arcHeight: 0.25,\n      markerElevation: demoConfig.markerElevation,\n    } as never) as unknown as CobeGlobeHandle\n\n    let raf = 0\n    const tick = () => {\n      if (!isPausedRef.current) {\n        if (!shouldReduceMotion) {\n          phiAutoRef.current += 0.003 * speedRef.current\n        }\n        if (\n          Math.abs(velocity.current.phi) > 0.0001 ||\n          Math.abs(velocity.current.theta) > 0.0001\n        ) {\n          phiOffsetRef.current += velocity.current.phi\n          thetaOffsetRef.current += velocity.current.theta\n          velocity.current.phi *= 0.95\n          velocity.current.theta *= 0.95\n        }\n        const thetaMin = -0.4\n        const thetaMax = 0.4\n        if (thetaOffsetRef.current < thetaMin) {\n          thetaOffsetRef.current += (thetaMin - thetaOffsetRef.current) * 0.1\n        } else if (thetaOffsetRef.current > thetaMax) {\n          thetaOffsetRef.current += (thetaMax - thetaOffsetRef.current) * 0.1\n        }\n      }\n      globe.update({\n        phi: phiAutoRef.current + phiOffsetRef.current + dragOffset.current.phi,\n        theta:\n          demoConfig.theta + thetaOffsetRef.current + dragOffset.current.theta,\n        width: logicalSize,\n        height: logicalSize,\n        markers: cobeMarkers,\n        arcs: cobeArcs,\n      })\n      raf = requestAnimationFrame(tick)\n    }\n    raf = requestAnimationFrame(tick)\n\n    return () => {\n      cancelAnimationFrame(raf)\n      globe.destroy()\n    }\n  }, [isInView, cobeMarkers, cobeArcs, bufferSize, shouldReduceMotion, isDark])\n\n  return (\n    <div\n      className={cn(\n        \"relative mx-auto w-full max-w-[min(100vw-2rem,640px)]\",\n        className\n      )}\n      data-slot=\"default-showcase-globe\"\n      ref={rootRef}\n    >\n      <div\n        className=\"relative mx-auto aspect-square w-full\"\n        style={{ maxWidth: globeSize, height: globeSize, width: globeSize }}\n      >\n        <canvas\n          className=\"relative z-1 block size-full max-h-full max-w-full cursor-grab touch-none\"\n          height={bufferSize}\n          onPointerDown={handlePointerDown}\n          onPointerEnter={() => {\n            speedRef.current = 0.8\n          }}\n          onPointerLeave={() => {\n            speedRef.current = 1\n          }}\n          ref={canvasRef}\n          style={{ contain: \"layout paint size\" }}\n          width={bufferSize}\n        />\n\n        {showcaseDefaultMarkers.map((m) => (\n          <div\n            className=\"pointer-events-none z-3 rounded-sm border border-chart-4/80 bg-chart-3 px-2 py-1 font-mono text-[9px] text-white uppercase tracking-wide shadow-sm sm:text-[10px] dark:border-chart-2/70 dark:bg-chart-2 dark:text-background\"\n            key={m.id}\n            style={anchorLabelStyle(m.id, \"marker\")}\n          >\n            {m.label}\n          </div>\n        ))}\n        {showcaseDefaultArcs.map((a) => (\n          <div\n            className=\"pointer-events-none z-3 rounded-sm border-2 border-chart-3 bg-background px-1.5 py-0.5 font-mono text-[8px] text-chart-3 uppercase tracking-wide shadow-sm sm:text-[9px] dark:border-chart-2 dark:bg-card dark:text-chart-2\"\n            key={a.id}\n            style={anchorLabelStyle(a.id, \"arc\")}\n          >\n            {a.label}\n          </div>\n        ))}\n      </div>\n    </div>\n  )\n}\n\nexport default DefaultShowcaseGlobe\n",
      "type": "registry:component"
    }
  ]
}