{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dotted-map-chart",
  "title": "Dotted Map Chart",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/charts/dotted-map-chart/DottedMapChart.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport type { LineLayerSpecification } from 'mapbox-gl'\nimport { Map, MapMarker, MapSource, MapLayer, type MapVariant } from '@/components/ui/map'\nimport { cn } from '@/lib/utils'\nimport { Globe } from 'lucide-react'\n\nexport interface MapPin {\n  lat: number\n  lng: number\n  label?: string\n  color?: string\n  description?: string\n  value?: string | number\n  status?: string\n}\n\nexport interface MapRoute {\n  from: { lat: number; lng: number }\n  to: { lat: number; lng: number }\n  color?: string\n  width?: number\n  curvature?: number\n  animated?: boolean\n  dashed?: boolean\n  duration?: number\n  label?: string\n}\n\nexport interface DottedMapChartProps {\n  pins?: MapPin[]\n  routes?: MapRoute[]\n  map?: 'world' | 'usa'\n  grid?: 'vertical' | 'diagonal'\n  shape?: 'circle' | 'hexagon'\n  dotColor?: string\n  pulse?: boolean\n  height?: number | string\n  className?: string\n  ariaLabel?: string\n  interactive?: boolean\n  variant?: MapVariant\n  projection?: 'globe' | 'mercator'\n  /** Fired when a pin is clicked. */\n  onPinClick?: (pin: MapPin) => void\n  /** Fired with the hovered pin, or null when the pointer leaves it. */\n  onPinHover?: (pin: MapPin | null) => void\n}\n\nexport function DottedMapChart({\n  pins = [],\n  routes = [],\n  map = 'world',\n  grid = 'vertical',\n  shape = 'circle',\n  dotColor = 'rgba(255, 255, 255, 0.22)',\n  pulse = true,\n  height = 420,\n  interactive = true,\n  variant = 'dark',\n  projection: initialProjection = 'globe',\n  className,\n  onPinClick,\n  onPinHover,\n}: DottedMapChartProps) {\n  const [projection, setProjection] = React.useState<'globe' | 'mercator'>(initialProjection)\n  const [hoveredPin, setHoveredPin] = React.useState<MapPin | null>(null)\n\n  const mapCenter: [number, number] = React.useMemo(() => {\n    return map === 'usa' ? [-98, 39] : [0, 20]\n  }, [map])\n\n  const mapZoom = React.useMemo(() => {\n    return map === 'usa' ? 3.5 : 1.5\n  }, [map])\n\n  const dotGridGeoJson = React.useMemo(() => {\n    const features: any[] = []\n    const step = map === 'usa' ? 3 : 6\n    const latMin = map === 'usa' ? 25 : -55\n    const latMax = map === 'usa' ? 50 : 70\n    const lngMin = map === 'usa' ? -125 : -170\n    const lngMax = map === 'usa' ? -66 : 170\n\n    for (let lat = latMin; lat <= latMax; lat += step) {\n      for (let lng = lngMin; lng <= lngMax; lng += step) {\n        features.push({\n          type: 'Feature',\n          geometry: {\n            type: 'Point',\n            coordinates: [lng, lat],\n          },\n        })\n      }\n    }\n\n    return {\n      type: 'FeatureCollection',\n      features,\n    }\n  }, [map])\n\n  const routesGeoJson = React.useMemo(() => {\n    if (!routes || !routes.length) return null\n    return {\n      type: 'FeatureCollection',\n      features: routes.map((r, i) => ({\n        type: 'Feature',\n        id: i,\n        properties: {\n          color: r.color || 'rgba(56, 189, 248, 0.75)',\n          dashed: r.dashed ?? true,\n        },\n        geometry: {\n          type: 'LineString',\n          coordinates: [\n            [r.from.lng, r.from.lat],\n            [(r.from.lng + r.to.lng) / 2, (r.from.lat + r.to.lat) / 2 + 5],\n            [r.to.lng, r.to.lat],\n          ],\n        },\n      })),\n    }\n  }, [routes])\n\n  const dotPaint = React.useMemo(\n    () => ({\n      'circle-radius': 1.5,\n      'circle-color': dotColor || 'rgba(255, 255, 255, 0.22)',\n      'circle-opacity': 0.4,\n    }),\n    [dotColor],\n  )\n\n  // mapbox reads a data-driven paint value as an expression tuple; without the\n  // annotation TS widens ['get', 'color'] to string[] and the layer rejects it.\n  const routeLinePaint: LineLayerSpecification['paint'] = {\n    'line-color': ['get', 'color'],\n    'line-width': 1.5,\n    'line-dasharray': [2, 2],\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 w-full overflow-hidden rounded-xl border shadow-xs',\n        className,\n      )}\n      style={{ height: typeof height === 'number' ? `${height}px` : height }}\n    >\n      <Map variant={variant} projection={projection} center={mapCenter} zoom={mapZoom} className=\"size-full\">\n        <MapSource id=\"dot-grid-source\" type=\"geojson\" data={dotGridGeoJson}>\n          <MapLayer id=\"dot-grid-layer\" type=\"circle\" paint={dotPaint} />\n        </MapSource>\n\n        {routesGeoJson && (\n          <MapSource id=\"routes-source\" type=\"geojson\" data={routesGeoJson}>\n            <MapLayer id=\"routes-layer\" type=\"line\" paint={routeLinePaint} />\n          </MapSource>\n        )}\n\n        {pins.map((pin, i) => (\n          <MapMarker\n            key={i}\n            longitude={pin.lng}\n            latitude={pin.lat}\n            anchor=\"center\"\n            className=\"cursor-pointer select-none\"\n          >\n            <div\n              className=\"relative flex size-6 items-center justify-center\"\n              onMouseEnter={() => {\n                setHoveredPin(pin)\n                onPinHover?.(pin)\n              }}\n              onMouseLeave={() => {\n                setHoveredPin(null)\n                onPinHover?.(null)\n              }}\n              onClick={() => onPinClick?.(pin)}\n            >\n              {pulse && (\n                <span\n                  className=\"absolute inline-flex size-full animate-ping rounded-full opacity-60\"\n                  style={{ backgroundColor: pin.color || 'oklch(0.65 0.20 145)' }}\n                />\n              )}\n              <span\n                className=\"ring-background relative inline-flex size-2.5 rounded-full shadow-xs ring-2\"\n                style={{\n                  backgroundColor: pin.color || 'oklch(0.65 0.20 145)',\n                  boxShadow: `0 0 10px ${pin.color || 'oklch(0.65 0.20 145)'}`,\n                }}\n              />\n            </div>\n          </MapMarker>\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      {hoveredPin && interactive && (\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-xs rounded-xl border p-3 shadow-lg backdrop-blur-md duration-150\">\n          <div className=\"flex items-center gap-2\">\n            <span\n              className=\"size-2 rounded-full\"\n              style={{ backgroundColor: hoveredPin.color || 'oklch(0.65 0.20 145)' }}\n            />\n            <h5 className=\"text-foreground text-xs font-semibold\">{hoveredPin.label || 'Telemetry Node'}</h5>\n          </div>\n          {hoveredPin.description && <p className=\"text-muted-foreground mt-1 text-xs\">{hoveredPin.description}</p>}\n          {hoveredPin.value !== undefined && (\n            <div className=\"border-border/60 mt-2 flex items-baseline justify-between border-t pt-1.5 font-mono text-xs\">\n              <span className=\"text-muted-foreground\">Value</span>\n              <span className=\"text-foreground font-semibold\">{hoveredPin.value}</span>\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport default DottedMapChart\n",
      "type": "registry:ui",
      "target": "~/components/ui/charts/dotted-map-chart/DottedMapChart.tsx"
    },
    {
      "path": "packages/registry-react/components/charts/dotted-map-chart/index.ts",
      "content": "export { DottedMapChart, type DottedMapChartProps, type MapPin, type MapRoute } from './DottedMapChart'\n",
      "type": "registry:ui",
      "target": "~/components/ui/charts/dotted-map-chart/index.ts"
    }
  ],
  "dependencies": [
    "mapbox-gl",
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/map.json"
  ],
  "description": "React mirror of @uipkge/dotted-map-chart — see the Vue registry item for the canonical description.",
  "categories": [
    "chart"
  ]
}