{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "route-flow-map",
  "title": "Route Flow Map",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/charts/route-flow-map/RouteFlowMap.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Map, MapMarker, MapSource, MapLayer, type MapVariant } from '@/components/ui/map'\nimport { cn } from '@/lib/utils'\nimport { useChartTheme } from '../useChartTheme'\nimport { Globe, Plane } from 'lucide-react'\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  onSelectedRouteChange?: (id: string) => void\n  onRouteSelect?: (route: FlightRoute) => void\n  onHubClick?: (hub: RouteHub) => void\n  showHubLabels?: boolean\n  showGraticule?: boolean\n  height?: number | string\n  interactive?: boolean\n  className?: string\n  ariaLabel?: string\n  projection?: 'globe' | 'mercator'\n}\n\nexport function RouteFlowMap({\n  hubs = [],\n  routes = [],\n  selectedRoute: controlledRoute,\n  onSelectedRouteChange,\n  onRouteSelect,\n  onHubClick,\n  showHubLabels = true,\n  showGraticule = true,\n  height = 480,\n  interactive = true,\n  projection: initialProjection = 'globe',\n  className,\n}: RouteFlowMapProps) {\n  const [internalRoute, setInternalRoute] = React.useState<string>('')\n  const [projection, setProjection] = React.useState<'globe' | 'mercator'>(initialProjection)\n  const [hoveredHub, setHoveredHub] = React.useState<RouteHub | null>(null)\n  const theme = useChartTheme()\n\n  const activeRouteId = controlledRoute !== undefined ? controlledRoute : internalRoute\n\n  const hubMap = React.useMemo(() => {\n    const map = new globalThis.Map<string, RouteHub>()\n    for (const h of hubs) map.set(h.id, h)\n    return map\n  }, [hubs])\n\n  const routesGeoJson = React.useMemo(() => {\n    if (!routes || !routes.length) return null\n    return {\n      type: 'FeatureCollection',\n      features: routes\n        .map((r) => {\n          const fromHub = hubMap.get(r.from)\n          const toHub = hubMap.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 === 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  }, [routes, hubMap, activeRouteId])\n\n  const routeLinePaint = React.useMemo(\n    () => ({\n      'line-color': ['case', ['==', ['get', 'id'], activeRouteId], theme.accentColor, ['get', 'color']] as any,\n      'line-width': ['case', ['==', ['get', 'id'], activeRouteId], 3, 1.5] as any,\n      'line-dasharray': [2, 2],\n    }),\n    [activeRouteId, theme.accentColor],\n  )\n\n  const activeRoute = React.useMemo(() => routes.find((r) => r.id === activeRouteId), [routes, activeRouteId])\n\n  const handleSelectRoute = (r: FlightRoute) => {\n    if (!interactive) return\n    setInternalRoute(r.id)\n    onSelectedRouteChange?.(r.id)\n    onRouteSelect?.(r)\n  }\n\n  const getVehiclePosition = (r: FlightRoute): [number, number] | null => {\n    const fromHub = hubMap.get(r.from)\n    const toHub = hubMap.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\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=\"dark\" projection={projection} center={[10, 25]} zoom={1.6} className=\"size-full\">\n        {routesGeoJson && (\n          <MapSource id=\"flight-routes-source\" type=\"geojson\" data={routesGeoJson}>\n            <MapLayer id=\"flight-routes-layer\" type=\"line\" paint={routeLinePaint} />\n          </MapSource>\n        )}\n\n        {hubs.map((hub) => (\n          <MapMarker\n            key={hub.id}\n            longitude={hub.lng}\n            latitude={hub.lat}\n            anchor=\"center\"\n            className=\"cursor-pointer select-none\"\n          >\n            <div\n              className=\"relative flex flex-col items-center\"\n              onMouseEnter={() => setHoveredHub(hub)}\n              onMouseLeave={() => setHoveredHub(null)}\n              onClick={() => onHubClick?.(hub)}\n            >\n              <span\n                className=\"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              {showHubLabels && (\n                <span className=\"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                  {hub.id}\n                </span>\n              )}\n            </div>\n          </MapMarker>\n        ))}\n\n        {routes.map((r) => {\n          const pos = getVehiclePosition(r)\n          if (!pos) return null\n          const isSelected = activeRouteId === r.id\n\n          return (\n            <MapMarker\n              key={`vehicle-${r.id}`}\n              longitude={pos[0]}\n              latitude={pos[1]}\n              anchor=\"center\"\n              className={cn(\n                'cursor-pointer transition-transform select-none',\n                isSelected ? 'z-30 scale-125' : 'z-20 hover:scale-110',\n              )}\n            >\n              <div\n                className=\"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                onClick={() => handleSelectRoute(r)}\n              >\n                <Plane className=\"size-3 rotate-45 text-sky-400\" />\n                <span className=\"font-mono text-[9px] font-semibold text-sky-200\">{r.callsign || r.id}</span>\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      {activeRoute && (\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 className=\"size-2 animate-pulse rounded-full bg-sky-400\" />\n                <span className=\"text-foreground font-mono text-xs font-semibold tracking-wider uppercase\">\n                  {activeRoute.callsign || activeRoute.id}\n                </span>\n                <span className=\"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 className=\"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                {activeRoute.aircraft && <span className=\"text-[10px]\">({activeRoute.aircraft})</span>}\n              </div>\n            </div>\n            <button\n              type=\"button\"\n              className=\"text-muted-foreground hover:text-foreground text-xs\"\n              onClick={() => {\n                setInternalRoute('')\n                onSelectedRouteChange?.('')\n              }}\n            >\n              ✕\n            </button>\n          </div>\n\n          <div className=\"border-border/60 mt-3 grid grid-cols-3 gap-2 border-t pt-2 font-mono text-[11px]\">\n            <div>\n              <span className=\"text-muted-foreground block text-[9px] uppercase\">Speed</span>\n              <span className=\"text-foreground font-semibold\">{activeRoute.speed || '480 kts'}</span>\n            </div>\n            <div>\n              <span className=\"text-muted-foreground block text-[9px] uppercase\">Altitude</span>\n              <span className=\"text-foreground font-semibold\">{activeRoute.altitude || 'FL360'}</span>\n            </div>\n            <div>\n              <span className=\"text-muted-foreground block text-[9px] uppercase\">ETA</span>\n              <span className=\"text-foreground font-semibold\">{activeRoute.eta || '02h 15m'}</span>\n            </div>\n          </div>\n        </div>\n      )}\n\n      {hoveredHub && (\n        <div className=\"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          <div className=\"flex items-center gap-2\">\n            <span\n              className=\"size-2 rounded-full\"\n              style={{ backgroundColor: hoveredHub.color || 'oklch(0.65 0.20 145)' }}\n            />\n            <h5 className=\"text-foreground text-xs font-semibold\">\n              {hoveredHub.name} ({hoveredHub.id})\n            </h5>\n          </div>\n          {hoveredHub.latency && (\n            <div className=\"mt-1.5 flex items-baseline justify-between font-mono text-xs\">\n              <span className=\"text-muted-foreground\">Turnaround</span>\n              <span className=\"text-foreground font-semibold\">{hoveredHub.latency}</span>\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport default RouteFlowMap\n",
      "type": "registry:ui",
      "target": "~/components/ui/charts/route-flow-map/RouteFlowMap.tsx"
    },
    {
      "path": "packages/registry-react/components/charts/route-flow-map/index.ts",
      "content": "export { RouteFlowMap, type RouteFlowMapProps, type RouteHub, type FlightRoute } from './RouteFlowMap'\nexport { default } from './RouteFlowMap'\n",
      "type": "registry:ui",
      "target": "~/components/ui/charts/route-flow-map/index.ts"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/map.json"
  ],
  "description": "React mirror of @uipkge/route-flow-map — see the Vue registry item for the canonical description.",
  "categories": [
    "chart"
  ]
}