{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "route-flow-map",
  "title": "Route Flow Map",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/charts/route-flow-map/RouteFlowMap.vue",
      "content": "<script lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\n\nexport interface RouteHub {\n  id: string\n  name: string\n  city?: string\n  lat: number\n  lng: number\n  status?: 'optimal' | 'busy' | 'delayed' | string\n  latency?: string | number\n  color?: string\n}\n\nexport interface FlightRoute {\n  id: string\n  from: string\n  to: string\n  callsign?: string\n  aircraft?: string\n  speed?: string\n  altitude?: string\n  progress?: number\n  eta?: string\n  status?: 'en-route' | 'scheduled' | 'approaching' | 'diverted' | string\n  color?: string\n  vehicleType?: 'plane' | 'ship' | 'packet' | 'pulse' | 'dot'\n  duration?: number\n  curvature?: number\n}\n\nexport interface RouteFlowMapProps {\n  hubs?: RouteHub[]\n  routes?: FlightRoute[]\n  selectedRoute?: string\n  showHubLabels?: boolean\n  showGraticule?: boolean\n  height?: number | string\n  interactive?: boolean\n  class?: HTMLAttributes['class']\n  ariaLabel?: string\n  projection?: 'globe' | 'mercator'\n}\n</script>\n\n<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\nimport { Map, MapMarker, MapSource, MapLayer, type MapVariant } from '@/components/ui/map'\nimport { cn } from '@/lib/utils'\nimport { chartAccentColor } from '../useChartTheme'\nimport { Globe, Plane, Navigation2 } from 'lucide-vue-next'\n\nconst props = withDefaults(defineProps<RouteFlowMapProps>(), {\n  hubs: () => [],\n  routes: () => [],\n  selectedRoute: undefined,\n  showHubLabels: true,\n  showGraticule: true,\n  height: 480,\n  interactive: true,\n  ariaLabel: 'Global Route and Flight Flow Map',\n  projection: 'globe',\n})\n\nconst emit = defineEmits<{\n  (e: 'update:selectedRoute', id: string): void\n  (e: 'routeSelect', route: FlightRoute): void\n  (e: 'hubClick', hub: RouteHub): void\n}>()\n\nconst activeRouteId = ref(props.selectedRoute || '')\nconst currentProjection = ref<'globe' | 'mercator'>(props.projection)\nconst hoveredHub = ref<RouteHub | null>(null)\n\nwatch(\n  () => props.selectedRoute,\n  (newVal) => {\n    if (newVal !== undefined) activeRouteId.value = newVal\n  },\n)\n\nconst hubMap = computed(() => {\n  const map = new globalThis.Map<string, RouteHub>()\n  for (const h of props.hubs) map.set(h.id, h)\n  return map\n})\n\nconst routesGeoJson = computed(() => {\n  if (!props.routes || !props.routes.length) return null\n  return {\n    type: 'FeatureCollection',\n    features: props.routes\n      .map((r) => {\n        const fromHub = hubMap.value.get(r.from)\n        const toHub = hubMap.value.get(r.to)\n        if (!fromHub || !toHub) return null\n\n        const midLng = (fromHub.lng + toHub.lng) / 2\n        const midLat = (fromHub.lat + toHub.lat) / 2 + 10\n\n        return {\n          type: 'Feature',\n          id: r.id,\n          properties: {\n            id: r.id,\n            color: r.color || 'rgba(56, 189, 248, 0.8)',\n            selected: activeRouteId.value === r.id,\n          },\n          geometry: {\n            type: 'LineString',\n            coordinates: [\n              [fromHub.lng, fromHub.lat],\n              [midLng, midLat],\n              [toHub.lng, toHub.lat],\n            ],\n          },\n        }\n      })\n      .filter(Boolean),\n  }\n})\n\nconst routeLinePaint = computed(() => ({\n  'line-color': ['case', ['==', ['get', 'id'], activeRouteId.value], chartAccentColor.value, ['get', 'color']],\n  'line-width': ['case', ['==', ['get', 'id'], activeRouteId.value], 3, 1.5],\n  'line-dasharray': [2, 2],\n}))\n\nconst activeRoute = computed(() => props.routes.find((r) => r.id === activeRouteId.value))\n\nfunction selectRoute(r: FlightRoute) {\n  if (!props.interactive) return\n  activeRouteId.value = r.id\n  emit('update:selectedRoute', r.id)\n  emit('routeSelect', r)\n}\n\nfunction getVehiclePosition(r: FlightRoute): [number, number] | null {\n  const fromHub = hubMap.value.get(r.from)\n  const toHub = hubMap.value.get(r.to)\n  if (!fromHub || !toHub) return null\n  const progress = (r.progress ?? 50) / 100\n  const lng = fromHub.lng + (toHub.lng - fromHub.lng) * progress\n  const lat = fromHub.lat + (toHub.lat - fromHub.lat) * progress + Math.sin(progress * Math.PI) * 10\n  return [lng, lat]\n}\n\nfunction toggleProjection() {\n  currentProjection.value = currentProjection.value === 'globe' ? 'mercator' : 'globe'\n}\n</script>\n\n<template>\n  <div\n    :class=\"cn('border-border bg-card group relative w-full overflow-hidden rounded-xl border shadow-xs', props.class)\"\n    :style=\"{\n      height:\n        typeof props.height === 'number'\n          ? `${props.height}px`\n          : /^\\d+$/.test(String(props.height))\n            ? `${props.height}px`\n            : props.height,\n    }\"\n  >\n    <Map variant=\"dark\" :projection=\"currentProjection\" :center=\"[10, 25]\" :zoom=\"1.6\" class=\"size-full\">\n      <!-- Great Circle Route Arcs -->\n      <MapSource v-if=\"routesGeoJson\" id=\"flight-routes-source\" type=\"geojson\" :data=\"routesGeoJson\">\n        <MapLayer id=\"flight-routes-layer\" type=\"line\" :paint=\"routeLinePaint\" />\n      </MapSource>\n\n      <!-- Hub Markers -->\n      <MapMarker\n        v-for=\"hub in hubs\"\n        :key=\"hub.id\"\n        :lng-lat=\"[hub.lng, hub.lat]\"\n        anchor=\"center\"\n        class=\"cursor-pointer select-none\"\n      >\n        <div\n          class=\"relative flex flex-col items-center\"\n          @mouseenter=\"hoveredHub = hub\"\n          @mouseleave=\"hoveredHub = null\"\n          @click=\"emit('hubClick', hub)\"\n        >\n          <span\n            class=\"size-2 rounded-full shadow-xs ring-2\"\n            :style=\"{\n              backgroundColor: hub.color || 'oklch(0.65 0.20 145)',\n              boxShadow: `0 0 8px ${hub.color || 'oklch(0.65 0.20 145)'}`,\n            }\"\n          />\n          <span\n            v-if=\"showHubLabels\"\n            class=\"border-border/80 bg-background/85 py-0.2 text-foreground mt-1 rounded border px-1 font-mono text-[9px] font-bold shadow-xs backdrop-blur-xs\"\n          >\n            {{ hub.id }}\n          </span>\n        </div>\n      </MapMarker>\n\n      <!-- Aircraft / Cargo Vehicles in Flight -->\n      <template v-for=\"r in routes\" :key=\"`vehicle-${r.id}`\">\n        <MapMarker\n          v-if=\"getVehiclePosition(r)\"\n          :lng-lat=\"getVehiclePosition(r)!\"\n          anchor=\"center\"\n          :class=\"\n            cn(\n              'cursor-pointer transition-transform select-none',\n              activeRouteId === r.id ? 'z-30 scale-125' : 'z-20 hover:scale-110',\n            )\n          \"\n        >\n          <div\n            class=\"flex items-center gap-1 rounded-full border border-sky-400/40 bg-sky-950/80 px-1.5 py-0.5 shadow-md backdrop-blur-xs\"\n            @click=\"selectRoute(r)\"\n          >\n            <Plane class=\"size-3 rotate-45 text-sky-400\" />\n            <span class=\"font-mono text-[9px] font-semibold text-sky-200\">\n              {{ r.callsign || r.id }}\n            </span>\n          </div>\n        </MapMarker>\n      </template>\n    </Map>\n\n    <!-- Top-Right Controls -->\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        <Globe class=\"size-3.5\" />\n        <span class=\"capitalize\">{{ currentProjection }}</span>\n      </button>\n    </div>\n\n    <!-- Active Flight Route HUD Card -->\n    <div\n      v-if=\"activeRoute\"\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-sm rounded-xl border p-3.5 shadow-lg backdrop-blur-md duration-150\"\n    >\n      <div class=\"flex items-start justify-between gap-3\">\n        <div>\n          <div class=\"flex items-center gap-2\">\n            <span class=\"size-2 animate-pulse rounded-full bg-sky-400\" />\n            <span class=\"text-foreground font-mono text-xs font-semibold tracking-wider uppercase\">\n              {{ activeRoute.callsign || activeRoute.id }}\n            </span>\n            <span class=\"py-0.2 rounded bg-sky-500/10 px-1.5 font-mono text-[10px] text-sky-400 capitalize\">\n              {{ activeRoute.status || 'En-route' }}\n            </span>\n          </div>\n          <div class=\"text-muted-foreground mt-1 flex items-center gap-2 font-mono text-xs\">\n            <span>{{ activeRoute.from }}</span>\n            <span>→</span>\n            <span>{{ activeRoute.to }}</span>\n            <span v-if=\"activeRoute.aircraft\" class=\"text-[10px]\">({{ activeRoute.aircraft }})</span>\n          </div>\n        </div>\n        <button type=\"button\" class=\"text-muted-foreground hover:text-foreground text-xs\" @click=\"activeRouteId = ''\">\n          ✕\n        </button>\n      </div>\n\n      <div class=\"border-border/60 mt-3 grid grid-cols-3 gap-2 border-t pt-2 font-mono text-[11px]\">\n        <div>\n          <span class=\"text-muted-foreground block text-[9px] uppercase\">Speed</span>\n          <span class=\"text-foreground font-semibold\">{{ activeRoute.speed || '480 kts' }}</span>\n        </div>\n        <div>\n          <span class=\"text-muted-foreground block text-[9px] uppercase\">Altitude</span>\n          <span class=\"text-foreground font-semibold\">{{ activeRoute.altitude || 'FL360' }}</span>\n        </div>\n        <div>\n          <span class=\"text-muted-foreground block text-[9px] uppercase\">ETA</span>\n          <span class=\"text-foreground font-semibold\">{{ activeRoute.eta || '02h 15m' }}</span>\n        </div>\n      </div>\n    </div>\n\n    <!-- Hovered Hub HUD -->\n    <div\n      v-if=\"hoveredHub\"\n      class=\"border-border/80 bg-card/95 animate-in fade-in slide-in-from-bottom-2 absolute right-3 bottom-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=\"{ backgroundColor: hoveredHub.color || 'oklch(0.65 0.20 145)' }\" />\n        <h5 class=\"text-foreground text-xs font-semibold\">{{ hoveredHub.name }} ({{ hoveredHub.id }})</h5>\n      </div>\n      <div v-if=\"hoveredHub.latency\" class=\"mt-1.5 flex items-baseline justify-between font-mono text-xs\">\n        <span class=\"text-muted-foreground\">Turnaround</span>\n        <span class=\"text-foreground font-semibold\">{{ hoveredHub.latency }}</span>\n      </div>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/charts/route-flow-map/RouteFlowMap.vue"
    },
    {
      "path": "packages/registry-vue/components/charts/route-flow-map/index.ts",
      "content": "export {\n  default as RouteFlowMap,\n  default,\n  type RouteFlowMapProps,\n  type RouteHub,\n  type FlightRoute,\n} from './RouteFlowMap.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/charts/route-flow-map/index.ts"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/map.json"
  ],
  "description": "Global route and flight corridor flow map as pure dependency-free SVG. Renders great-circle Bezier curved arcs with traveling aircraft or data pulses via native SVG motion, origin and destination telemetry hubs, and hover inspection cards.",
  "categories": [
    "chart"
  ]
}