{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bubble-map",
  "title": "Bubble Map",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/charts/bubble-map/BubbleMap.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Map, MapMarker, type MapVariant } from '@/components/ui/map'\nimport { cn } from '@/lib/utils'\nimport { Globe } from 'lucide-react'\n\nexport interface MapBubble {\n  id: string\n  name: string\n  lat: number\n  lng: number\n  value: number\n  formattedValue?: string\n  category?: string\n  status?: 'optimal' | 'warning' | 'destructive' | 'neutral' | 'active'\n  color?: string\n  pulse?: boolean\n  description?: string\n}\n\nexport interface BubbleMapProps {\n  bubbles?: MapBubble[]\n  minRadius?: number\n  maxRadius?: number\n  showLegend?: boolean\n  legendTitle?: string\n  selectedId?: string\n  onSelectedIdChange?: (id: string | undefined) => void\n  onSelect?: (bubble: MapBubble) => void\n  interactive?: boolean\n  projection?: 'globe' | 'mercator'\n  variant?: MapVariant\n  center?: [number, number]\n  zoom?: number\n  className?: string\n}\n\nconst STATUS_COLORS: Record<string, string> = {\n  optimal: 'oklch(0.65 0.20 145)',\n  active: 'oklch(0.60 0.20 250)',\n  warning: 'oklch(0.75 0.18 65)',\n  destructive: 'oklch(0.60 0.22 25)',\n  neutral: 'oklch(0.65 0.05 240)',\n}\n\nfunction getBubbleColor(b: MapBubble): string {\n  if (b.color) return b.color\n  if (b.status && STATUS_COLORS[b.status]) return STATUS_COLORS[b.status]\n  return 'oklch(0.60 0.20 250)'\n}\n\nexport function BubbleMap({\n  bubbles = [],\n  minRadius = 10,\n  maxRadius = 42,\n  showLegend = true,\n  legendTitle = 'Scale by Magnitude',\n  selectedId: controlledSelectedId,\n  onSelectedIdChange,\n  onSelect,\n  interactive = true,\n  projection: initialProjection = 'globe',\n  variant = 'dark',\n  center = [0, 20],\n  zoom = 1.5,\n  className,\n}: BubbleMapProps) {\n  const [internalSelectedId, setInternalSelectedId] = React.useState<string | undefined>(controlledSelectedId)\n  const [projection, setProjection] = React.useState<'globe' | 'mercator'>(initialProjection)\n\n  const activeId = controlledSelectedId !== undefined ? controlledSelectedId : internalSelectedId\n\n  const values = React.useMemo(() => bubbles.map((b) => b.value), [bubbles])\n  const minValue = React.useMemo(() => (values.length ? Math.min(...values) : 1), [values])\n  const maxValue = React.useMemo(() => (values.length ? Math.max(...values) : 100), [values])\n\n  const getRadius = React.useCallback(\n    (val: number): number => {\n      if (maxValue === minValue) return (minRadius + maxRadius) / 2\n      const ratio = Math.sqrt(Math.max(0, val - minValue) / (maxValue - minValue))\n      return Math.round(minRadius + ratio * (maxRadius - minRadius))\n    },\n    [minValue, maxValue, minRadius, maxRadius],\n  )\n\n  const activeBubble = React.useMemo(() => bubbles.find((b) => b.id === activeId), [bubbles, activeId])\n\n  const handleSelect = (b: MapBubble) => {\n    if (!interactive) return\n    setInternalSelectedId(b.id)\n    onSelectedIdChange?.(b.id)\n    onSelect?.(b)\n  }\n\n  const toggleProjection = () => {\n    setProjection((prev) => (prev === 'globe' ? 'mercator' : 'globe'))\n  }\n\n  return (\n    <div\n      className={cn(\n        'border-border bg-card group relative h-[420px] w-full overflow-hidden rounded-xl border shadow-xs',\n        className,\n      )}\n    >\n      <Map variant={variant} projection={projection} center={center} zoom={zoom} className=\"size-full\">\n        {bubbles.map((b) => {\n          const radius = getRadius(b.value)\n          const isSelected = activeId === b.id\n          const color = getBubbleColor(b)\n\n          return (\n            <MapMarker\n              key={b.id}\n              longitude={b.lng}\n              latitude={b.lat}\n              anchor=\"center\"\n              className={cn(\n                'cursor-pointer transition-transform select-none',\n                isSelected ? 'z-30 scale-110' : 'z-20 hover:scale-105',\n              )}\n            >\n              <div\n                className=\"relative flex items-center justify-center\"\n                style={{ width: `${radius * 2}px`, height: `${radius * 2}px` }}\n                onClick={() => handleSelect(b)}\n              >\n                {b.pulse && (\n                  <span\n                    className=\"absolute inline-flex size-full animate-ping rounded-full opacity-40\"\n                    style={{ backgroundColor: color }}\n                  />\n                )}\n\n                <div className=\"absolute inset-0 rounded-full opacity-25\" style={{ backgroundColor: color }} />\n\n                <div\n                  className=\"relative flex size-4/5 items-center justify-center rounded-full border border-white/40 shadow-sm backdrop-blur-[1px] transition-[background-color,box-shadow]\"\n                  style={{\n                    backgroundColor: color,\n                    boxShadow: isSelected ? `0 0 16px ${color}` : 'none',\n                  }}\n                >\n                  {radius >= 20 ? (\n                    <span className=\"px-1 text-center font-mono text-[10px] font-bold text-white drop-shadow-xs\">\n                      {b.formattedValue || b.value}\n                    </span>\n                  ) : (\n                    <span className=\"size-1.5 rounded-full bg-white shadow-xs\" />\n                  )}\n                </div>\n              </div>\n            </MapMarker>\n          )\n        })}\n      </Map>\n\n      <div className=\"border-border/70 bg-card/85 absolute top-3 right-3 z-10 flex items-center gap-1 rounded-lg border p-1 shadow-xs backdrop-blur-md\">\n        <button\n          type=\"button\"\n          className=\"text-muted-foreground hover:bg-muted hover:text-foreground flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition\"\n          onClick={toggleProjection}\n        >\n          <Globe className=\"size-3.5\" />\n          <span className=\"capitalize\">{projection}</span>\n        </button>\n      </div>\n\n      {activeBubble && (\n        <div className=\"border-border/80 bg-card/95 animate-in fade-in slide-in-from-bottom-2 absolute bottom-3 left-3 z-10 max-w-sm rounded-xl border p-3.5 shadow-lg backdrop-blur-md duration-150\">\n          <div className=\"flex items-start justify-between gap-3\">\n            <div>\n              <div className=\"flex items-center gap-2\">\n                <span\n                  className=\"size-2 rounded-full ring-2 ring-white/20\"\n                  style={{ backgroundColor: getBubbleColor(activeBubble) }}\n                />\n                <span className=\"text-muted-foreground font-mono text-xs tracking-wider uppercase\">\n                  {activeBubble.category || 'Node'}\n                </span>\n              </div>\n              <h4 className=\"text-foreground mt-0.5 text-sm font-semibold\">{activeBubble.name}</h4>\n            </div>\n            <button\n              type=\"button\"\n              className=\"text-muted-foreground hover:text-foreground text-xs\"\n              onClick={() => {\n                setInternalSelectedId(undefined)\n                onSelectedIdChange?.(undefined)\n              }}\n            >\n              ✕\n            </button>\n          </div>\n\n          <div className=\"border-border/60 mt-2.5 flex items-baseline justify-between border-t pt-2 font-mono text-xs\">\n            <span className=\"text-muted-foreground\">Magnitude</span>\n            <span className=\"text-foreground font-semibold\">\n              {activeBubble.formattedValue || activeBubble.value.toLocaleString()}\n            </span>\n          </div>\n\n          {activeBubble.description && (\n            <p className=\"text-muted-foreground mt-1.5 text-xs leading-relaxed\">{activeBubble.description}</p>\n          )}\n        </div>\n      )}\n\n      {showLegend && bubbles.length > 0 && (\n        <div className=\"border-border/70 bg-card/85 absolute right-3 bottom-3 z-10 hidden items-center gap-3 rounded-lg border px-3 py-2 text-xs shadow-xs backdrop-blur-md sm:flex\">\n          <span className=\"text-muted-foreground font-mono text-[11px]\">{legendTitle}</span>\n          <div className=\"flex items-center gap-2\">\n            <span className=\"bg-muted-foreground/40 size-2 rounded-full\" />\n            <span className=\"text-muted-foreground font-mono text-[10px]\">{minValue.toLocaleString()}</span>\n            <span className=\"bg-muted-foreground/60 size-4 rounded-full\" />\n            <span className=\"text-foreground font-mono text-[10px] font-semibold\">{maxValue.toLocaleString()}</span>\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport function projectPoint(lat: number, lng: number): { x: number; y: number } {\n  const x = ((lng + 180) / 360) * 1000\n  const y = ((90 - lat) / 180) * 500\n  return { x, y }\n}\n\nexport const CONTINENT_LANDMASSES: Array<{ id: string; name: string; d: string }> = []\nexport default BubbleMap\n",
      "type": "registry:ui",
      "target": "~/components/ui/charts/bubble-map/BubbleMap.tsx"
    },
    {
      "path": "packages/registry-react/components/charts/bubble-map/index.ts",
      "content": "export {\n  BubbleMap,\n  default as BubbleMapDefault,\n  type MapBubble,\n  type BubbleMapProps,\n  projectPoint,\n  CONTINENT_LANDMASSES,\n} from './BubbleMap'\nexport { default } from './BubbleMap'\n",
      "type": "registry:ui",
      "target": "~/components/ui/charts/bubble-map/index.ts"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/map.json"
  ],
  "description": "Proportional symbol bubble map as dependency-free SVG. Eliminates geographic landmass distortion by scaling circle areas to continuous quantitative values with mathematical square-root radius normalization, pulsating concentric ripple rings, multi-tier size legend, and interactive hover cards.",
  "categories": [
    "chart"
  ]
}