{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "dotted-map-chart",
  "title": "Dotted Map Chart",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/charts/dotted-map-chart/dotted-map-chart.component.ts",
      "content": "import {\n  AfterViewChecked,\n  ChangeDetectionStrategy,\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  OnInit,\n  Output,\n  QueryList,\n  SimpleChanges,\n  ViewChildren,\n  signal,\n} from '@angular/core'\nimport type * as mapboxgl from 'mapbox-gl'\nimport { Globe, LucideAngularModule } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiMapComponent, type MapLayerInput, type MapSourceInput, type MapVariant } from '@/ui/map/map.component'\nimport { onChartThemeChange, toCanvasColor } from '../use-chart-theme'\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\nconst DEFAULT_PIN_COLOR = 'oklch(0.65 0.20 145)'\nconst DEFAULT_DOT_COLOR = 'rgba(255, 255, 255, 0.22)'\nconst DEFAULT_ROUTE_COLOR = 'rgba(56, 189, 248, 0.75)'\n\n// Mapbox GL paint needs a concrete color — resolve `var(--token)` values\n// (the natural way to pass theme colors) via getComputedStyle first.\nexport function resolvePaintColor(value: string): string {\n  const match = value.trim().match(/^var\\(\\s*(--[\\w-]+)\\s*\\)$/)\n  const name = match?.[1]\n  if (!name || typeof window === 'undefined') return value\n  const resolved = getComputedStyle(document.documentElement).getPropertyValue(name).trim()\n  return resolved ? toCanvasColor(resolved) : value\n}\n\n/** React's three-point route arc: straight to a midpoint lifted 5° of latitude, then on to the end. */\nexport function routeCoordinates(r: MapRoute): [number, number][] {\n  return [\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\nfunction envMapboxToken(): string | undefined {\n  try {\n    return ((import.meta as any).env?.PUBLIC_MAPBOX_TOKEN as string | undefined) || undefined\n  } catch {\n    return undefined\n  }\n}\n\n/**\n * Angular port of the UIPKGE DottedMapChart: a Mapbox GL map (via `ui-map`)\n * with a dot-grid circle layer, route line layers and pulsing HTML pin\n * markers with a hover card. Without a Mapbox token `ui-map` renders its\n * \"Mapbox token required\" placeholder.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-dotted-map-chart, [ui-dotted-map-chart]',\n  standalone: true,\n  imports: [UiMapComponent, LucideAngularModule],\n  host: {\n    '[attr.data-slot]': '\"dotted-map-chart\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': '\"contents\"',\n  },\n  template: `\n    <div\n      [class]=\"rootClass\"\n      [style.height]=\"heightStyle\"\n      [attr.role]=\"ariaLabel ? 'region' : null\"\n      [attr.aria-label]=\"ariaLabel || null\"\n    >\n      <ui-map\n        [variant]=\"variant\"\n        [projection]=\"currentProjection()\"\n        [center]=\"mapCenter\"\n        [zoom]=\"mapZoom\"\n        [accessToken]=\"resolvedToken\"\n        [sources]=\"mapSources\"\n        [layers]=\"mapLayers\"\n        class=\"size-full\"\n        (created)=\"onMapCreated($event)\"\n      />\n\n      <div class=\"hidden\">\n        @for (pin of pins; track $index) {\n          <div #pinMarker class=\"cursor-pointer select-none\">\n            <div\n              data-slot=\"dotted-map-pin\"\n              class=\"relative flex size-6 items-center justify-center\"\n              (mouseenter)=\"onPinEnter(pin)\"\n              (mouseleave)=\"onPinLeave()\"\n              (click)=\"pinClick.emit(pin)\"\n            >\n              @if (pulse) {\n                <span\n                  class=\"absolute inline-flex size-full animate-ping rounded-full opacity-60\"\n                  [style.background-color]=\"pin.color || defaultPinColor\"\n                ></span>\n              }\n              <span\n                class=\"ring-background relative inline-flex size-2.5 rounded-full shadow-xs ring-2\"\n                [style.background-color]=\"pin.color || defaultPinColor\"\n                [style.box-shadow]=\"'0 0 10px ' + (pin.color || defaultPinColor)\"\n              ></span>\n            </div>\n          </div>\n        }\n      </div>\n\n      <div\n        class=\"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      >\n        <button\n          type=\"button\"\n          class=\"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          (click)=\"toggleProjection()\"\n        >\n          <lucide-icon [img]=\"Globe\" class=\"size-3.5\" />\n          <span class=\"capitalize\">{{ currentProjection() }}</span>\n        </button>\n      </div>\n\n      @if (hoveredPin(); as hp) {\n        @if (interactive) {\n          <div\n            data-slot=\"dotted-map-hover-card\"\n            class=\"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          >\n            <div class=\"flex items-center gap-2\">\n              <span class=\"size-2 rounded-full\" [style.background-color]=\"hp.color || defaultPinColor\"></span>\n              <h5 class=\"text-foreground text-xs font-semibold\">{{ hp.label || 'Telemetry Node' }}</h5>\n            </div>\n            @if (hp.description) {\n              <p class=\"text-muted-foreground mt-1 text-xs\">{{ hp.description }}</p>\n            }\n            @if (hp.value !== undefined) {\n              <div class=\"border-border/60 mt-2 flex items-baseline justify-between border-t pt-1.5 font-mono text-xs\">\n                <span class=\"text-muted-foreground\">Value</span>\n                <span class=\"text-foreground font-semibold\">{{ hp.value }}</span>\n              </div>\n            }\n          </div>\n        }\n      }\n    </div>\n  `,\n})\nexport class UiDottedMapChartComponent implements OnInit, OnChanges, AfterViewChecked, OnDestroy {\n  @Input() pins: MapPin[] = []\n  @Input() routes: MapRoute[] = []\n  @Input() map: 'world' | 'usa' = 'world'\n  /** Accepted for API parity with React; the dot grid is always a vertical lattice. */\n  @Input() grid: 'vertical' | 'diagonal' = 'vertical'\n  /** Accepted for API parity with React; dots always render as circles. */\n  @Input() shape: 'circle' | 'hexagon' = 'circle'\n  @Input() dotColor = DEFAULT_DOT_COLOR\n  @Input() pulse = true\n  @Input() height: number | string = 420\n  @Input() ariaLabel?: string\n  /** Show the hover card for the hovered pin. */\n  @Input() interactive = true\n  @Input() variant: MapVariant = 'dark'\n  /** Initial projection; the corner toggle switches it afterwards. */\n  @Input() projection: 'globe' | 'mercator' = 'globe'\n  /** Mapbox token. Falls back to `PUBLIC_MAPBOX_TOKEN`; pass `''` to force the no-token placeholder. */\n  @Input() accessToken?: string\n  @Input('class') className?: string\n\n  /** Fired when a pin is clicked. */\n  @Output() pinClick = new EventEmitter<MapPin>()\n  /** Fired with the hovered pin, or null when the pointer leaves it. */\n  @Output() pinHover = new EventEmitter<MapPin | null>()\n\n  @ViewChildren('pinMarker') pinMarkers?: QueryList<ElementRef<HTMLDivElement>>\n\n  protected readonly Globe = Globe\n  protected readonly defaultPinColor = DEFAULT_PIN_COLOR\n\n  readonly currentProjection = signal<'globe' | 'mercator'>('globe')\n  readonly hoveredPin = signal<MapPin | null>(null)\n\n  mapSources: MapSourceInput[] = []\n  mapLayers: MapLayerInput[] = []\n\n  private mapInstance: mapboxgl.Map | null = null\n  private Marker: typeof mapboxgl.Marker | null = null\n  private markers: mapboxgl.Marker[] = []\n  private markersDirty = false\n  private unsubscribeTheme: (() => void) | null = null\n\n  get rootClass(): string {\n    return cn('border-border bg-card group relative w-full overflow-hidden rounded-xl border shadow-xs', this.className)\n  }\n\n  get heightStyle(): string {\n    return /^\\d+(\\.\\d+)?$/.test(String(this.height)) ? `${this.height}px` : String(this.height)\n  }\n\n  get mapCenter(): [number, number] {\n    return this.map === 'usa' ? [-98, 39] : [0, 20]\n  }\n\n  get mapZoom(): number {\n    return this.map === 'usa' ? 3.5 : 1.5\n  }\n\n  get resolvedToken(): string {\n    return this.accessToken ?? envMapboxToken() ?? ''\n  }\n\n  /** Dot lattice over the world (6° step) or the contiguous USA (3° step). */\n  buildDotGrid(): { type: 'FeatureCollection'; features: unknown[] } {\n    const features: unknown[] = []\n    const usa = this.map === 'usa'\n    const step = usa ? 3 : 6\n    const latMin = usa ? 25 : -55\n    const latMax = usa ? 50 : 70\n    const lngMin = usa ? -125 : -170\n    const lngMax = usa ? -66 : 170\n    for (let lat = latMin; lat <= latMax; lat += step) {\n      for (let lng = lngMin; lng <= lngMax; lng += step) {\n        features.push({ type: 'Feature', geometry: { type: 'Point', coordinates: [lng, lat] } })\n      }\n    }\n    return { type: 'FeatureCollection', features }\n  }\n\n  /**\n   * Route line features. Like React/Vue, only `color` is read (passed to Mapbox as given);\n   * `width`, `dashed` and `curvature` are accepted but every route draws 1.5px, dashed, on the fixed arc.\n   */\n  buildRoutes(): { type: 'FeatureCollection'; features: unknown[] } {\n    return {\n      type: 'FeatureCollection',\n      features: (this.routes ?? []).map((r, i) => ({\n        type: 'Feature',\n        id: i,\n        properties: {\n          color: r.color || DEFAULT_ROUTE_COLOR,\n          dashed: r.dashed ?? true,\n        },\n        geometry: { type: 'LineString', coordinates: routeCoordinates(r) },\n      })),\n    }\n  }\n\n  buildDotPaint(): Record<string, unknown> {\n    return {\n      'circle-radius': 1.5,\n      'circle-color': resolvePaintColor(this.dotColor || DEFAULT_DOT_COLOR),\n      'circle-opacity': 0.4,\n    }\n  }\n\n  ngOnInit(): void {\n    this.currentProjection.set(this.projection)\n    this.rebuildLayers()\n    this.unsubscribeTheme = onChartThemeChange(() => this.refreshMapData())\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes['pins']) this.markersDirty = true\n    if (!changes['routes'] && !changes['map'] && !changes['dotColor']) return\n    this.refreshMapData()\n  }\n\n  ngAfterViewChecked(): void {\n    if (this.markersDirty && this.mapInstance && this.Marker) this.syncMarkers()\n  }\n\n  ngOnDestroy(): void {\n    this.unsubscribeTheme?.()\n    this.unsubscribeTheme = null\n    this.removeMarkers()\n    this.mapInstance = null\n  }\n\n  async onMapCreated(map: mapboxgl.Map): Promise<void> {\n    this.mapInstance = map\n    const mod = await import('mapbox-gl')\n    const mapbox = mod.default ?? mod\n    this.Marker = mapbox.Marker as unknown as typeof mapboxgl.Marker\n    this.syncMarkers()\n  }\n\n  onPinEnter(pin: MapPin): void {\n    this.hoveredPin.set(pin)\n    this.pinHover.emit(pin)\n  }\n\n  onPinLeave(): void {\n    this.hoveredPin.set(null)\n    this.pinHover.emit(null)\n  }\n\n  toggleProjection(): void {\n    const next = this.currentProjection() === 'globe' ? 'mercator' : 'globe'\n    this.currentProjection.set(next)\n    try {\n      ;(this.mapInstance as unknown as { setProjection?: (p: string) => void } | null)?.setProjection?.(next)\n    } catch {\n      /* older api */\n    }\n  }\n\n  private rebuildLayers(): void {\n    this.mapSources = [\n      { id: 'dot-grid-source', type: 'geojson', data: this.buildDotGrid() },\n      { id: 'routes-source', type: 'geojson', data: this.buildRoutes() },\n    ]\n    this.mapLayers = [\n      { id: 'dot-grid-layer', type: 'circle', source: 'dot-grid-source', paint: this.buildDotPaint() },\n      {\n        id: 'routes-layer',\n        type: 'line',\n        source: 'routes-source',\n        paint: { 'line-color': ['get', 'color'], 'line-width': 1.5, 'line-dasharray': [2, 2] },\n      },\n    ]\n  }\n\n  /** Rebuild source data + paint and push them onto a live map (sources exist once the style loaded). */\n  private refreshMapData(): void {\n    this.rebuildLayers()\n    const map = this.mapInstance\n    if (!map) return\n    try {\n      for (const s of this.mapSources) {\n        ;(map.getSource(s.id) as { setData?: (d: unknown) => void } | undefined)?.setData?.(s.data)\n      }\n      if (map.getLayer('dot-grid-layer')) {\n        map.setPaintProperty('dot-grid-layer', 'circle-color', this.buildDotPaint()['circle-color'] as string)\n      }\n    } catch {\n      /* style reloading — ui-map re-adds sources from the inputs on style.load */\n    }\n  }\n\n  private removeMarkers(): void {\n    for (const m of this.markers) m.remove()\n    this.markers = []\n  }\n\n  private syncMarkers(): void {\n    this.markersDirty = false\n    const map = this.mapInstance\n    const Marker = this.Marker\n    if (!map || !Marker) return\n    this.removeMarkers()\n    const els = this.pinMarkers?.toArray() ?? []\n    els.forEach((ref, i) => {\n      const pin = this.pins[i]\n      if (!pin) return\n      const marker = new Marker({ element: ref.nativeElement, anchor: 'center' })\n        .setLngLat([pin.lng, pin.lat])\n        .addTo(map)\n      this.markers.push(marker)\n    })\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/charts/dotted-map-chart/dotted-map-chart.component.ts"
    },
    {
      "path": "packages/registry-angular/components/charts/dotted-map-chart/index.ts",
      "content": "export {\n  UiDottedMapChartComponent,\n  resolvePaintColor,\n  routeCoordinates,\n  type MapPin,\n  type MapRoute,\n} from './dotted-map-chart.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/charts/dotted-map-chart/index.ts"
    },
    {
      "path": "packages/registry-angular/components/charts/use-chart-theme.ts",
      "content": "// Angular port of `useChartTheme.ts` (Vue) — framework-free helpers + plain\n// functions instead of Vue `computed` refs. The palette is driven by Tailwind\n// v4 CSS variables (`--chart-1`..`--chart-5`, `--muted-foreground`, `--border`,\n// `--popover`, …) so dark/light flips happen automatically when the consumer\n// toggles their theme class. Values resolve at runtime via `getComputedStyle`,\n// so they pick up whatever the consumer set in their own `tailwind.css`.\n//\n// In a non-DOM environment (SSR / unit tests) every accessor returns the\n// fallback palette, which roughly matches the shadcn Neutral defaults so the\n// first paint doesn't flicker.\n\nexport const CHART_FALLBACK = ['#f59e0b', '#14b8a6', '#3b82f6', '#f97316', '#eab308']\n\n// ECharts' canvas renderer does not accept OKLCH in every browser. Assigning\n// one of our Tailwind tokens to `fillStyle` can silently leave the sentinel\n// colour in place, turning a whole chart black. Convert OKLCH ourselves and\n// let the canvas normalize older CSS colour formats.\nlet _hexCanvas: CanvasRenderingContext2D | null = null\n\nexport function toCanvasColor(cssColor: string): string {\n  const value = cssColor.trim()\n  const match = value.match(\n    /^oklch\\(\\s*([+-]?(?:\\d+\\.?\\d*|\\.\\d+))(%?)\\s+([+-]?(?:\\d+\\.?\\d*|\\.\\d+))\\s+([+-]?(?:\\d+\\.?\\d*|\\.\\d+))(?:deg)?(?:\\s*\\/\\s*([+-]?(?:\\d+\\.?\\d*|\\.\\d+))(%?))?\\s*\\)$/i,\n  )\n\n  if (match) {\n    const lightness = Number(match[1]) / (match[2] === '%' ? 100 : 1)\n    const chroma = Number(match[3])\n    const hue = (Number(match[4]) * Math.PI) / 180\n    const alpha = match[5] == null ? 1 : Number(match[5]) / (match[6] === '%' ? 100 : 1)\n    const a = chroma * Math.cos(hue)\n    const b = chroma * Math.sin(hue)\n\n    const l = Math.pow(lightness + 0.3963377774 * a + 0.2158037573 * b, 3)\n    const m = Math.pow(lightness - 0.1055613458 * a - 0.0638541728 * b, 3)\n    const s = Math.pow(lightness - 0.0894841775 * a - 1.291485548 * b, 3)\n    const linear = [\n      4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,\n      -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,\n      -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s,\n    ]\n    const rgb = linear.map((channel) => {\n      const encoded = channel <= 0.0031308 ? 12.92 * channel : 1.055 * Math.pow(channel, 1 / 2.4) - 0.055\n      return Math.round(Math.min(1, Math.max(0, encoded)) * 255)\n    })\n\n    return alpha < 1 ? `rgba(${rgb.join(', ')}, ${alpha})` : `rgb(${rgb.join(', ')})`\n  }\n\n  if (typeof document === 'undefined') return cssColor\n  if (!_hexCanvas) {\n    _hexCanvas = document.createElement('canvas').getContext('2d')\n  }\n  if (!_hexCanvas) return cssColor\n  _hexCanvas.fillStyle = '#010203'\n  _hexCanvas.fillStyle = value\n  const normalized = _hexCanvas.fillStyle as string\n  return normalized === '#010203' && value.toLowerCase() !== '#010203' ? value : normalized\n}\n\n// Convert any CSS color (hex, rgb, oklch, color()) + alpha 0..1 to a\n// canvas-safe rgba(r,g,b,a). `colorString + '40'` (8-digit hex alpha)\n// only works when `colorString` is `#rrggbb`; once tokens resolve to\n// oklch() post-hydration the gradient stops break and the canvas paint\n// throws every frame. Stay defensive and always return rgba.\nexport function toRgba(cssColor: string, alpha: number): string {\n  const normalized = toCanvasColor(cssColor)\n  if (normalized.startsWith('#') && normalized.length === 7) {\n    const r = parseInt(normalized.slice(1, 3), 16)\n    const g = parseInt(normalized.slice(3, 5), 16)\n    const b = parseInt(normalized.slice(5, 7), 16)\n    return `rgba(${r},${g},${b},${alpha})`\n  }\n  if (normalized.startsWith('rgba(')) {\n    return normalized.replace(/,\\s*[\\d.]+\\s*\\)$/, `,${alpha})`)\n  }\n  if (normalized.startsWith('rgb(')) {\n    return normalized.replace(/^rgb\\(/, 'rgba(').replace(/\\)$/, `,${alpha})`)\n  }\n  // Canvas refused to parse this color -- ship the original string and\n  // let ECharts complain (better than crashing the paint loop).\n  return cssColor\n}\n\nexport function resolveCssVar(name: string, fallback: string): string {\n  if (typeof window === 'undefined') return fallback\n  const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim()\n  if (!v) return fallback\n  return toCanvasColor(v)\n}\n\nexport function getChartColors(): string[] {\n  return Array.from({ length: 5 }, (_, i) => resolveCssVar(`--chart-${i + 1}`, CHART_FALLBACK[i]!))\n}\n\nexport function getChartTextColor(): string {\n  return resolveCssVar('--muted-foreground', '#888888')\n}\n\nexport function getChartAxisColor(): string {\n  return resolveCssVar('--border', '#e5e5e5')\n}\n\nexport function getChartSplitLineColor(): string {\n  return resolveCssVar('--border', '#f0f0f0')\n}\n\nexport function getChartTooltipBg(): string {\n  return resolveCssVar('--popover', 'rgba(255,255,255,0.96)')\n}\n\nexport function getChartTooltipBorder(): string {\n  return resolveCssVar('--border', '#e5e5e5')\n}\n\nexport function getChartTooltipText(): string {\n  return resolveCssVar('--popover-foreground', '#333333')\n}\n\nexport function getChartBgColor(): string {\n  return resolveCssVar('--card', resolveCssVar('--background', '#ffffff'))\n}\n\n// The app's accent, for the one highlighted element on a chart or map — a\n// selected route, a focused bar. Category series keep using getChartColors().\nexport function getChartAccentColor(): string {\n  return resolveCssVar('--primary', '#38bdf8')\n}\n\n// Out-of-bounds / failure colour for charts that encode good vs bad rather\n// than a category — control limits, error series.\nexport function getChartDangerColor(): string {\n  return resolveCssVar('--destructive', '#dc2626')\n}\n\n// Two-level deep merge for ECharts option blocks (xAxis, yAxis, grid,\n// tooltip, legend, singleAxis, parallel, etc.). The top-level keys merge\n// shallowly, but one nested level (axisLabel, axisLine, splitLine, etc.)\n// merges shallowly too so a consumer passing `xAxis: { axisLabel: { fontSize: 9 } }`\n// doesn't blow away the wrapper's `color` + base font defaults on the same\n// axisLabel block. Arrays + primitives replace outright.\nexport function mergeOptionBlock<T extends Record<string, any>>(base: T, user: Partial<T> | undefined): T {\n  if (!user) return base\n  const out: any = { ...base }\n  for (const k of Object.keys(user)) {\n    const bv = (base as any)[k]\n    const uv = (user as any)[k]\n    if (\n      bv != null &&\n      uv != null &&\n      typeof bv === 'object' &&\n      typeof uv === 'object' &&\n      !Array.isArray(bv) &&\n      !Array.isArray(uv)\n    ) {\n      out[k] = { ...bv, ...uv }\n    } else {\n      out[k] = uv\n    }\n  }\n  return out\n}\n\n// Default gauge stoplight: teal (safe) -> amber (warning) -> red (danger).\n// Static because gauges have semantic meaning (green safe / red danger) that\n// we deliberately don't theme-flip.\nexport const gaugeThresholds: [number, string][] = [\n  [0.6, '#14b8a6'],\n  [0.85, '#f59e0b'],\n  [1, '#dc2626'],\n]\n\n/**\n * Re-render callback subscription for theme flips: bumps every listener\n * whenever `<html>` class/style changes (the typical shadcn dark-mode pivot)\n * so chart components can rebuild their options and re-paint. Returns an\n * unsubscribe function. No-op (never fires) outside the DOM.\n */\nexport function onChartThemeChange(cb: () => void): () => void {\n  if (typeof window === 'undefined' || typeof MutationObserver === 'undefined') return () => {}\n  const observer = new MutationObserver(() => cb())\n  observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] })\n  return () => observer.disconnect()\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/charts/use-chart-theme.ts"
    }
  ],
  "dependencies": [
    "mapbox-gl",
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/map.json"
  ],
  "description": "Dotted world/USA map on Mapbox GL. Telemetry dot grid with theme-aware dot color, connection route arcs, and pulsing lat/lng pins with hover cards.",
  "categories": [
    "chart"
  ]
}