{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "deploy-region-globe", "type": "registry:ui", "title": "Region Globe", "description": "A card with an interactive globe over a region list. Pick a region, the camera flies to it and the latency caption updates, then confirm.", "dependencies": [ "cobe", "lucide-react" ], "files": [ { "path": "registry/ruixenui/deploy-region-globe.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport createGlobe from \"cobe\";\nimport { useTheme } from \"next-themes\";\nimport { ChevronDown, Globe } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface Region {\n /** Stable id. */\n id: string;\n /** Short name, e.g. \"Tokyo\". */\n name: string;\n /** Region code, e.g. \"ap-northeast-1\". */\n code: string;\n /** Caption city, e.g. \"Tokyo, Japan\". */\n city: string;\n /** `[latitude, longitude]` in degrees. */\n location: [number, number];\n /** p50 latency in ms, shown under the globe. */\n latency: number;\n}\n\nexport interface RegionGlobeProps {\n /** Heading at the top of the card. */\n title?: string;\n /** Sub-line under the heading. */\n subtitle?: string;\n /** Regions listed and marked on the globe. */\n regions?: Region[];\n /** Initially selected region id. */\n defaultRegionId?: string;\n /** Fires when the selected region changes. */\n onRegionChange?: (region: Region) => void;\n /** Fires when the confirm button is pressed, with the selected region. */\n onConfirm?: (region: Region) => void;\n /** Label to the left of the selected region code. */\n selectLabel?: string;\n /** Text of the confirm button. */\n actionLabel?: string;\n /** Globe marker RGB (0–1), also the accent dot color. */\n markerColor?: [number, number, number];\n /** Globe zoom (globe scale). 1 = full distant sphere, ~2 = close-up. */\n zoom?: number;\n /** Extra classes on the card. */\n className?: string;\n}\n\nconst DEFAULT_REGIONS: Region[] = [\n {\n id: \"iad\",\n name: \"N. Virginia\",\n code: \"us-east-1\",\n city: \"Ashburn, USA\",\n location: [38.95, -77.45],\n latency: 41,\n },\n {\n id: \"sfo\",\n name: \"N. California\",\n code: \"us-west-1\",\n city: \"San Francisco, USA\",\n location: [37.77, -122.42],\n latency: 63,\n },\n {\n id: \"gru\",\n name: \"São Paulo\",\n code: \"sa-east-1\",\n city: \"São Paulo, Brazil\",\n location: [-23.55, -46.63],\n latency: 118,\n },\n {\n id: \"fra\",\n name: \"Frankfurt\",\n code: \"eu-central-1\",\n city: \"Frankfurt, Germany\",\n location: [50.11, 8.68],\n latency: 74,\n },\n {\n id: \"dxb\",\n name: \"Dubai\",\n code: \"me-central-1\",\n city: \"Dubai, UAE\",\n location: [25.2, 55.27],\n latency: 96,\n },\n {\n id: \"bom\",\n name: \"Mumbai\",\n code: \"ap-south-1\",\n city: \"Mumbai, India\",\n location: [19.08, 72.88],\n latency: 88,\n },\n {\n id: \"sin\",\n name: \"Singapore\",\n code: \"ap-southeast-1\",\n city: \"Singapore\",\n location: [1.35, 103.82],\n latency: 61,\n },\n {\n id: \"nrt\",\n name: \"Tokyo\",\n code: \"ap-northeast-1\",\n city: \"Tokyo, Japan\",\n location: [35.68, 139.65],\n latency: 52,\n },\n {\n id: \"syd\",\n name: \"Sydney\",\n code: \"ap-southeast-2\",\n city: \"Sydney, Australia\",\n location: [-33.87, 151.21],\n latency: 79,\n },\n];\n\nconst DEFAULT_MARKER: [number, number, number] = [0.23, 0.51, 0.96]; // blue-500\n\nconst TRAVEL_DIP = 0.4; // how far the camera pulls back at mid-flight\nconst MIN_DURATION = 650; // ms — a short hop between neighbours\nconst MAX_DURATION = 1300; // ms — a flight across the globe\nconst easeInOutCubic = (t: number) =>\n t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;\n\n// Rotation (phi/theta) that brings a lat/long to the front-center of the globe.\nfunction locationToAngles(lat: number, long: number): [number, number] {\n return [\n Math.PI - ((long * Math.PI) / 180 - Math.PI / 2),\n (lat * Math.PI) / 180,\n ];\n}\n\nconst THEME = {\n light: {\n dark: 0,\n mapBrightness: 5,\n baseColor: [1, 1, 1],\n glowColor: [0.96, 0.96, 0.98],\n opacity: 0.9,\n },\n dark: {\n dark: 1,\n mapBrightness: 5,\n baseColor: [0.12, 0.13, 0.16],\n glowColor: [0.06, 0.06, 0.09],\n opacity: 0.95,\n },\n} as const;\n\nexport function DeployRegionGlobe({\n title = \"Global regions\",\n subtitle = \"Fastest region to your users\",\n regions = DEFAULT_REGIONS,\n defaultRegionId,\n onRegionChange,\n onConfirm,\n selectLabel = \"Region\",\n actionLabel = \"Select region\",\n markerColor = DEFAULT_MARKER,\n zoom = 1.5,\n className,\n}: RegionGlobeProps) {\n const { resolvedTheme } = useTheme();\n const [mounted, setMounted] = React.useState(false);\n const [selectedId, setSelectedId] = React.useState(\n defaultRegionId ?? regions[0]?.id,\n );\n const [open, setOpen] = React.useState(true);\n const listId = React.useId();\n\n const selected = regions.find((r) => r.id === selectedId) ?? regions[0];\n\n const canvasRef = React.useRef(null);\n const initial = locationToAngles(selected.location[0], selected.location[1]);\n const phiRef = React.useRef(initial[0]);\n const thetaRef = React.useRef(initial[1]);\n const scaleRef = React.useRef(zoom);\n const animRef = React.useRef<{\n sp: number;\n st: number;\n dp: number;\n dt: number;\n t0: number;\n dur: number;\n travel: number;\n } | null>(null);\n\n React.useEffect(() => setMounted(true), []);\n\n // Start a smooth camera flight to the newly selected region. Picking a new\n // region mid-flight just re-bases from wherever the camera is now — no snap.\n React.useEffect(() => {\n const [tp, tt] = locationToAngles(\n selected.location[0],\n selected.location[1],\n );\n let dp = (tp - phiRef.current) % (2 * Math.PI);\n if (dp > Math.PI) dp -= 2 * Math.PI;\n if (dp < -Math.PI) dp += 2 * Math.PI; // rotate the short way round\n const dt = tt - thetaRef.current;\n const travel = Math.min((Math.abs(dp) + Math.abs(dt)) / Math.PI, 1);\n animRef.current = {\n sp: phiRef.current,\n st: thetaRef.current,\n dp,\n dt,\n t0: performance.now(),\n dur: MIN_DURATION + (MAX_DURATION - MIN_DURATION) * travel,\n travel,\n };\n }, [selected.location]);\n\n // Stable keys so the globe is only rebuilt on real changes (not inline props).\n const markersKey = React.useMemo(\n () => regions.map((r) => r.location.join()).join(\"|\"),\n [regions],\n );\n const markerKey = markerColor.join();\n\n React.useEffect(() => {\n if (!mounted) return;\n const canvas = canvasRef.current;\n if (!canvas) return;\n\n const dpr = Math.min(window.devicePixelRatio || 1, 2);\n const cfg = resolvedTheme === \"dark\" ? THEME.dark : THEME.light;\n let w = 0;\n let globe: ReturnType | null = null;\n\n const build = () => {\n if (globe || w === 0) return;\n globe = createGlobe(canvas, {\n devicePixelRatio: dpr,\n width: w * dpr,\n height: w * dpr,\n phi: phiRef.current,\n theta: thetaRef.current,\n scale: scaleRef.current,\n dark: cfg.dark,\n diffuse: 1.2,\n mapSamples: 16000,\n mapBrightness: cfg.mapBrightness,\n baseColor: cfg.baseColor as [number, number, number],\n markerColor,\n glowColor: cfg.glowColor as [number, number, number],\n opacity: cfg.opacity,\n markers: regions.map((r) => ({ location: r.location, size: 0.03 })),\n onRender: (state) => {\n const anim = animRef.current;\n if (anim) {\n const t = Math.min((performance.now() - anim.t0) / anim.dur, 1);\n const e = easeInOutCubic(t);\n phiRef.current = anim.sp + anim.dp * e;\n thetaRef.current = anim.st + anim.dt * e;\n // Pull the camera back at mid-flight, ease it in on arrival —\n // scaled by how far we're travelling so short hops barely dip.\n scaleRef.current =\n zoom - TRAVEL_DIP * anim.travel * Math.sin(Math.PI * e);\n if (t >= 1) animRef.current = null;\n } else {\n scaleRef.current += (zoom - scaleRef.current) * 0.1;\n }\n state.phi = phiRef.current;\n state.theta = thetaRef.current;\n state.scale = scaleRef.current;\n state.width = w * dpr;\n state.height = w * dpr;\n },\n });\n requestAnimationFrame(() => {\n if (canvasRef.current) canvasRef.current.style.opacity = \"1\";\n });\n };\n\n const ro = new ResizeObserver(() => {\n w = canvas.offsetWidth;\n build();\n });\n ro.observe(canvas);\n\n return () => {\n ro.disconnect();\n globe?.destroy();\n canvas.style.opacity = \"0\";\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [mounted, resolvedTheme, markersKey, markerKey, zoom]);\n\n const select = (region: Region) => {\n setSelectedId(region.id);\n onRegionChange?.(region);\n };\n\n const accent = `rgb(${markerColor.map((c) => Math.round(c * 255)).join(\",\")})`;\n\n return (\n \n {/* Header */}\n
\n \n
\n

{title}

\n {subtitle && (\n

\n {subtitle}\n

\n )}\n
\n
\n\n {/* Globe */}\n
\n \n
\n

\n {selected.city} · {selected.latency}ms p50\n

\n\n {/* Region selector — expands below the trigger */}\n
\n setOpen((o) => !o)}\n aria-expanded={open}\n aria-controls={listId}\n aria-haspopup=\"listbox\"\n className=\"flex w-full items-center justify-between rounded-md text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card\"\n >\n {selectLabel}\n \n {selected.code}\n \n \n \n\n {open && (\n \n {regions.map((region) => {\n const isSelected = region.id === selectedId;\n return (\n
  • \n select(region)}\n className={cn(\n \"flex w-full items-center justify-between rounded-lg px-2.5 py-2 text-left outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring\",\n isSelected ? \"bg-muted\" : \"hover:bg-muted/60\",\n )}\n >\n \n \n {region.name}\n \n \n {region.code}\n \n \n
  • \n );\n })}\n \n )}\n
    \n\n onConfirm?.(selected)}\n className=\"mt-4 w-full rounded-xl bg-foreground py-3 text-sm font-semibold text-background outline-none transition-opacity hover:opacity-90 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card\"\n >\n {actionLabel}\n \n \n );\n}\n", "type": "registry:ui", "target": "components/ruixen/deploy-region-globe.tsx" } ] }