{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "dotted-map-chart",
  "title": "Dotted Map Chart",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/dotted-map-chart/DottedMapChart.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { HTMLAttributes } from 'svelte/elements'\n  import type { MapVariant } from '$lib/components/ui/map'\n\n  export 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\n  export 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\n  export interface DottedMapChartProps extends HTMLAttributes<HTMLDivElement> {\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    ariaLabel?: string\n    interactive?: boolean\n    variant?: MapVariant\n    projection?: 'globe' | 'mercator'\n    onPinClick?: (pin: MapPin) => void\n    onPinHover?: (pin: MapPin | null) => void\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { Globe } from '@lucide/svelte'\n  import { Map, MapLayer, MapMarker, MapSource } from '$lib/components/ui/map'\n  import { cn } from '$lib/utils'\n\n  // Mapbox GL paint needs a concrete color — resolve `var(--token)` values\n  // (the natural way to pass theme colors) via getComputedStyle first.\n  // Inlined (svelte charts vendor their own theme code per-component).\n  let _hexCanvas: CanvasRenderingContext2D | null = null\n  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    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      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      return alpha < 1 ? `rgba(${rgb.join(', ')}, ${alpha})` : `rgb(${rgb.join(', ')})`\n    }\n    if (typeof document === 'undefined') return cssColor\n    if (!_hexCanvas) _hexCanvas = document.createElement('canvas').getContext('2d')\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  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  let {\n    class: className,\n    pins = [],\n    routes = [],\n    map = 'world',\n    // Accepted for twin parity; the map renderer currently draws circles only.\n    grid = 'vertical',\n    shape = 'circle',\n    dotColor = 'rgba(255, 255, 255, 0.22)',\n    pulse = true,\n    height = 420,\n    ariaLabel,\n    interactive = true,\n    variant = 'dark',\n    projection = 'globe',\n    onPinClick,\n    onPinHover,\n    ref = $bindable(null),\n    ...restProps\n  }: DottedMapChartProps = $props()\n\n  void grid\n  void shape\n\n  let currentProjection = $state<'globe' | 'mercator'>(projection)\n  let hoveredPin = $state<MapPin | null>(null)\n\n  function onPinEnter(pin: MapPin) {\n    hoveredPin = pin\n    onPinHover?.(pin)\n  }\n\n  function onPinLeave() {\n    hoveredPin = null\n    onPinHover?.(null)\n  }\n\n  function toggleProjection() {\n    currentProjection = currentProjection === 'globe' ? 'mercator' : 'globe'\n  }\n\n  const mapCenter = $derived<[number, number]>(map === 'usa' ? [-98, 39] : [0, 20])\n  const mapZoom = $derived(map === 'usa' ? 3.5 : 1.5)\n\n  // Generate synthetic telemetry dot matrix over land\n  const dotGridGeoJson = $derived.by(() => {\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  })\n\n  const routesGeoJson = $derived(\n    !routes || !routes.length\n      ? null\n      : {\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  )\n\n  const dotPaint = $derived({\n    'circle-radius': 1.5,\n    'circle-color': resolvePaintColor(dotColor || 'rgba(255, 255, 255, 0.22)'),\n    'circle-opacity': 0.4,\n  })\n\n  const routeLinePaint = {\n    'line-color': ['get', 'color'],\n    'line-width': 1.5,\n    'line-dasharray': [2, 2],\n  }\n\n  const heightStyle = $derived(\n    typeof height === 'number' ? `${height}px` : /^\\d+$/.test(String(height)) ? `${height}px` : height,\n  )\n</script>\n\n<div\n  bind:this={ref}\n  role=\"img\"\n  aria-label={ariaLabel || 'Dotted map chart'}\n  class={cn('border-border bg-card group relative w-full overflow-hidden rounded-xl border shadow-xs', className)}\n  style:height={heightStyle}\n  {...restProps}\n>\n  <Map {variant} projection={currentProjection} center={mapCenter} zoom={mapZoom} class=\"size-full\">\n    <!-- Telemetry Dot Grid -->\n    <MapSource id=\"dot-grid-source\" type=\"geojson\" data={dotGridGeoJson}>\n      <MapLayer id=\"dot-grid-layer\" type=\"circle\" paint={dotPaint} />\n    </MapSource>\n\n    <!-- Connection Routes -->\n    {#if routesGeoJson}\n      <MapSource id=\"routes-source\" type=\"geojson\" data={routesGeoJson}>\n        <MapLayer id=\"routes-layer\" type=\"line\" paint={routeLinePaint} />\n      </MapSource>\n    {/if}\n\n    <!-- Pins -->\n    {#each pins as pin, i (i)}\n      <MapMarker lngLat={[pin.lng, pin.lat]} anchor=\"center\" class=\"cursor-pointer select-none\">\n        <div\n          class=\"relative flex size-6 items-center justify-center\"\n          onmouseenter={() => onPinEnter(pin)}\n          onmouseleave={onPinLeave}\n          onclick={() => onPinClick?.(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 || 'oklch(0.65 0.20 145)'}\"\n            ></span>\n          {/if}\n          <span\n            class=\"ring-background relative inline-flex size-2.5 rounded-full shadow-xs ring-2\"\n            style=\"background-color: {pin.color || 'oklch(0.65 0.20 145)'}; box-shadow: 0 0 10px {pin.color ||\n              'oklch(0.65 0.20 145)'};\"\n          ></span>\n        </div>\n      </MapMarker>\n    {/each}\n  </Map>\n\n  <!-- Projection Switcher -->\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      onclick={toggleProjection}\n    >\n      <Globe class=\"size-3.5\" />\n      <span class=\"capitalize\">{currentProjection}</span>\n    </button>\n  </div>\n\n  <!-- Active Pin Card -->\n  {#if hoveredPin && interactive}\n    <div\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: {hoveredPin.color || 'oklch(0.65 0.20 145)'}\"\n        ></span>\n        <h5 class=\"text-foreground text-xs font-semibold\">\n          {hoveredPin.label || 'Telemetry Node'}\n        </h5>\n      </div>\n      {#if hoveredPin.description}\n        <p class=\"text-muted-foreground mt-1 text-xs\">\n          {hoveredPin.description}\n        </p>\n      {/if}\n      {#if hoveredPin.value !== undefined}\n        <div\n          class=\"border-border/60 mt-2 flex items-baseline justify-between border-t pt-1.5 font-mono text-xs\"\n        >\n          <span class=\"text-muted-foreground\">Value</span>\n          <span class=\"text-foreground font-semibold\">{hoveredPin.value}</span>\n        </div>\n      {/if}\n    </div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dotted-map-chart/DottedMapChart.svelte"
    },
    {
      "path": "packages/registry-svelte/components/dotted-map-chart/index.ts",
      "content": "export {\n  default as DottedMapChart,\n  type DottedMapChartProps,\n  type MapPin,\n  type MapRoute,\n} from './DottedMapChart.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/dotted-map-chart/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/map.json"
  ],
  "description": "Dotted map with telemetry pins and connection routes over a dot-matrix landmass. Globe/mercator projection toggle, pulsing pins with hover detail cards, and world/USA grids. Built on the map primitive.",
  "categories": [
    "chart"
  ]
}