{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "map",
  "title": "Map",
  "description": "A MapLibre-powered map component with markers, popups, tooltips, routes, and controls.",
  "dependencies": [
    "maplibre-gl",
    "lucide-react"
  ],
  "devDependencies": [
    "@types/geojson"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "src/registry/map.tsx",
      "content": "\"use client\";\n\nimport MapLibreGL, { type PopupOptions, type MarkerOptions } from \"maplibre-gl\";\nimport \"maplibre-gl/dist/maplibre-gl.css\";\nimport type * as GeoJSON from \"geojson\";\nimport {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { X, Minus, Plus, Locate, Maximize, Loader2 } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst defaultStyles = {\n  dark: \"https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json\",\n  light: \"https://basemaps.cartocdn.com/gl/positron-gl-style/style.json\",\n};\n\n// A tile-less, dependency-free style with a transparent background. Use it for\n// data visualizations (choropleths, world arcs, dot maps) where you draw your\n// own layers and don't need a street basemap. The easiest way to opt in is the\n// `blank` prop:\n//   <Map blank>...</Map>\n// The transparent background lets the themed container show through.\nconst blankMapStyle: MapLibreGL.StyleSpecification = {\n  version: 8,\n  sources: {},\n  layers: [\n    {\n      id: \"background\",\n      type: \"background\",\n      paint: { \"background-color\": \"rgba(0, 0, 0, 0)\" },\n    },\n  ],\n};\n\nfunction mergeHoverPaint<T extends Record<string, unknown>>(\n  paint: T,\n  hoverPaint: T | undefined,\n): T {\n  if (!hoverPaint) return paint;\n  const merged: Record<string, unknown> = { ...paint };\n  for (const [key, hoverValue] of Object.entries(hoverPaint)) {\n    if (hoverValue === undefined) continue;\n    const baseValue = merged[key];\n    merged[key] =\n      baseValue === undefined\n        ? hoverValue\n        : [\n            \"case\",\n            [\"boolean\", [\"feature-state\", \"hover\"], false],\n            hoverValue,\n            baseValue,\n          ];\n  }\n  return merged as T;\n}\n\ntype Theme = \"light\" | \"dark\";\n\n// Check document class for theme (works with next-themes, etc.)\nfunction getDocumentTheme(): Theme | null {\n  if (typeof document === \"undefined\") return null;\n  if (document.documentElement.classList.contains(\"dark\")) return \"dark\";\n  if (document.documentElement.classList.contains(\"light\")) return \"light\";\n  return null;\n}\n\n// Get system preference\nfunction getSystemTheme(): Theme {\n  if (typeof window === \"undefined\") return \"light\";\n  return window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n    ? \"dark\"\n    : \"light\";\n}\n\nfunction useResolvedTheme(themeProp?: \"light\" | \"dark\"): Theme {\n  const [detectedTheme, setDetectedTheme] = useState<Theme>(\n    () => getDocumentTheme() ?? getSystemTheme(),\n  );\n\n  useEffect(() => {\n    if (themeProp) return; // Skip detection if theme is provided via prop\n\n    // Watch for document class changes (e.g., next-themes toggling dark class)\n    const observer = new MutationObserver(() => {\n      const docTheme = getDocumentTheme();\n      if (docTheme) {\n        setDetectedTheme(docTheme);\n      }\n    });\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    // Also watch for system preference changes\n    const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\");\n    const handleSystemChange = (e: MediaQueryListEvent) => {\n      // Only use system preference if no document class is set\n      if (!getDocumentTheme()) {\n        setDetectedTheme(e.matches ? \"dark\" : \"light\");\n      }\n    };\n    mediaQuery.addEventListener(\"change\", handleSystemChange);\n\n    return () => {\n      observer.disconnect();\n      mediaQuery.removeEventListener(\"change\", handleSystemChange);\n    };\n  }, [themeProp]);\n\n  return themeProp ?? detectedTheme;\n}\n\ntype MapContextValue = {\n  map: MapLibreGL.Map | null;\n  isLoaded: boolean;\n  resolvedTheme: Theme;\n};\n\nconst MapContext = createContext<MapContextValue | null>(null);\n\nfunction useMap() {\n  const context = useContext(MapContext);\n  if (!context) {\n    throw new Error(\"useMap must be used within a Map component\");\n  }\n  return context;\n}\n\n/** Map viewport state */\ntype MapViewport = {\n  /** Center coordinates [longitude, latitude] */\n  center: [number, number];\n  /** Zoom level */\n  zoom: number;\n  /** Bearing (rotation) in degrees */\n  bearing: number;\n  /** Pitch (tilt) in degrees */\n  pitch: number;\n};\n\ntype MapStyleOption = string | MapLibreGL.StyleSpecification;\n\ntype MapRef = MapLibreGL.Map;\n\ntype MapProps = {\n  children?: ReactNode;\n  /** Additional CSS classes for the map container */\n  className?: string;\n  /**\n   * Theme for the map. If not provided, automatically detects system preference.\n   * Pass your theme value here.\n   */\n  theme?: Theme;\n  /** Custom map styles for light and dark themes. Overrides the default Carto styles. */\n  styles?: {\n    light?: MapStyleOption;\n    dark?: MapStyleOption;\n  };\n  /**\n   * Use a transparent, tile-less basemap instead of the default Carto street\n   * basemap — a blank canvas. Used alone it renders nothing; add your own\n   * layers on top (`<MapGeoJSON>`, `<MapArc>`, markers, etc.). Ideal for data\n   * visualizations (choropleths, arcs, dot maps).\n   * Ignored when an explicit `styles` prop is provided.\n   */\n  blank?: boolean;\n  /** Map projection type. Use `{ type: \"globe\" }` for 3D globe view. */\n  projection?: MapLibreGL.ProjectionSpecification;\n  /**\n   * Controlled viewport. When provided with onViewportChange,\n   * the map becomes controlled and viewport is driven by this prop.\n   */\n  viewport?: Partial<MapViewport>;\n  /**\n   * Callback fired continuously as the viewport changes (pan, zoom, rotate, pitch).\n   * Can be used standalone to observe changes, or with `viewport` prop\n   * to enable controlled mode where the map viewport is driven by your state.\n   */\n  onViewportChange?: (viewport: MapViewport) => void;\n  /** Show a loading indicator on the map */\n  loading?: boolean;\n} & Omit<MapLibreGL.MapOptions, \"container\" | \"style\">;\n\nfunction DefaultLoader() {\n  return (\n    <div className=\"bg-background/50 absolute inset-0 z-10 flex items-center justify-center backdrop-blur-xs\">\n      <div className=\"flex gap-1\">\n        <span className=\"bg-muted-foreground/60 size-1.5 animate-pulse rounded-full\" />\n        <span className=\"bg-muted-foreground/60 size-1.5 animate-pulse rounded-full [animation-delay:150ms]\" />\n        <span className=\"bg-muted-foreground/60 size-1.5 animate-pulse rounded-full [animation-delay:300ms]\" />\n      </div>\n    </div>\n  );\n}\n\nfunction getViewport(map: MapLibreGL.Map): MapViewport {\n  const center = map.getCenter();\n  return {\n    center: [center.lng, center.lat],\n    zoom: map.getZoom(),\n    bearing: map.getBearing(),\n    pitch: map.getPitch(),\n  };\n}\n\nconst Map = forwardRef<MapRef, MapProps>(function Map(\n  {\n    children,\n    className,\n    theme: themeProp,\n    styles,\n    blank = false,\n    projection,\n    viewport,\n    onViewportChange,\n    loading = false,\n    ...props\n  },\n  ref,\n) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [mapInstance, setMapInstance] = useState<MapLibreGL.Map | null>(null);\n  const [isLoaded, setIsLoaded] = useState(false);\n  const [isStyleLoaded, setIsStyleLoaded] = useState(false);\n  const currentStyleRef = useRef<MapStyleOption | null>(null);\n  const styleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const internalUpdateRef = useRef(false);\n  const resolvedTheme = useResolvedTheme(themeProp);\n\n  const isControlled = viewport !== undefined && onViewportChange !== undefined;\n\n  const onViewportChangeRef = useRef(onViewportChange);\n  onViewportChangeRef.current = onViewportChange;\n\n  const mapStyles = useMemo(() => {\n    // Explicit styles win. Otherwise `blank` opts into the transparent\n    // tile-less basemap; with neither, fall back to the Carto defaults.\n    if (styles) {\n      return {\n        dark: styles.dark ?? defaultStyles.dark,\n        light: styles.light ?? defaultStyles.light,\n      };\n    }\n    if (blank) {\n      return { dark: blankMapStyle, light: blankMapStyle };\n    }\n    return defaultStyles;\n  }, [styles, blank]);\n\n  // Expose the map instance to the parent component\n  useImperativeHandle(ref, () => mapInstance as MapLibreGL.Map, [mapInstance]);\n\n  const clearStyleTimeout = useCallback(() => {\n    if (styleTimeoutRef.current) {\n      clearTimeout(styleTimeoutRef.current);\n      styleTimeoutRef.current = null;\n    }\n  }, []);\n\n  // Initialize the map\n  useEffect(() => {\n    if (!containerRef.current) return;\n\n    const initialStyle =\n      resolvedTheme === \"dark\" ? mapStyles.dark : mapStyles.light;\n    currentStyleRef.current = initialStyle;\n\n    const map = new MapLibreGL.Map({\n      container: containerRef.current,\n      style: initialStyle,\n      renderWorldCopies: false,\n      attributionControl: {\n        compact: true,\n      },\n      ...props,\n      ...viewport,\n    });\n\n    const styleDataHandler = () => {\n      clearStyleTimeout();\n      // Delay to ensure style is fully processed before allowing layer operations\n      // This is a workaround to avoid race conditions with the style loading\n      // else we have to force update every layer on setStyle change\n      styleTimeoutRef.current = setTimeout(() => {\n        setIsStyleLoaded(true);\n        if (projection) {\n          map.setProjection(projection);\n        }\n      }, 100);\n    };\n    const loadHandler = () => setIsLoaded(true);\n\n    // Viewport change handler - skip if triggered by internal update\n    const handleMove = () => {\n      if (internalUpdateRef.current) return;\n      onViewportChangeRef.current?.(getViewport(map));\n    };\n\n    map.on(\"load\", loadHandler);\n    map.on(\"styledata\", styleDataHandler);\n    map.on(\"move\", handleMove);\n    setMapInstance(map);\n\n    return () => {\n      clearStyleTimeout();\n      map.off(\"load\", loadHandler);\n      map.off(\"styledata\", styleDataHandler);\n      map.off(\"move\", handleMove);\n      map.remove();\n      setIsLoaded(false);\n      setIsStyleLoaded(false);\n      setMapInstance(null);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  // Sync controlled viewport to map\n  useEffect(() => {\n    if (!mapInstance || !isControlled || !viewport) return;\n    if (mapInstance.isMoving()) return;\n\n    const current = getViewport(mapInstance);\n    const next = {\n      center: viewport.center ?? current.center,\n      zoom: viewport.zoom ?? current.zoom,\n      bearing: viewport.bearing ?? current.bearing,\n      pitch: viewport.pitch ?? current.pitch,\n    };\n\n    if (\n      next.center[0] === current.center[0] &&\n      next.center[1] === current.center[1] &&\n      next.zoom === current.zoom &&\n      next.bearing === current.bearing &&\n      next.pitch === current.pitch\n    ) {\n      return;\n    }\n\n    internalUpdateRef.current = true;\n    mapInstance.jumpTo(next);\n    internalUpdateRef.current = false;\n  }, [mapInstance, isControlled, viewport]);\n\n  // Handle style change\n  useEffect(() => {\n    if (!mapInstance || !resolvedTheme) return;\n\n    const newStyle =\n      resolvedTheme === \"dark\" ? mapStyles.dark : mapStyles.light;\n\n    if (currentStyleRef.current === newStyle) return;\n\n    clearStyleTimeout();\n    currentStyleRef.current = newStyle;\n    setIsStyleLoaded(false);\n\n    mapInstance.setStyle(newStyle, { diff: true });\n  }, [mapInstance, resolvedTheme, mapStyles, clearStyleTimeout]);\n\n  // Sync projection when the prop changes after mount.\n  useEffect(() => {\n    if (!mapInstance || !isStyleLoaded || !projection) return;\n    mapInstance.setProjection(projection);\n  }, [mapInstance, isStyleLoaded, projection]);\n\n  const contextValue = useMemo(\n    () => ({\n      map: mapInstance,\n      isLoaded: isLoaded && isStyleLoaded,\n      resolvedTheme,\n    }),\n    [mapInstance, isLoaded, isStyleLoaded, resolvedTheme],\n  );\n\n  return (\n    <MapContext.Provider value={contextValue}>\n      <div\n        ref={containerRef}\n        className={cn(\"relative h-full w-full\", className)}\n      >\n        {(!isLoaded || loading) && <DefaultLoader />}\n        {/* SSR-safe: children render only when map is loaded on client */}\n        {mapInstance && children}\n      </div>\n    </MapContext.Provider>\n  );\n});\n\ntype MarkerContextValue = {\n  marker: MapLibreGL.Marker;\n  map: MapLibreGL.Map | null;\n};\n\nconst MarkerContext = createContext<MarkerContextValue | null>(null);\n\nfunction useMarkerContext() {\n  const context = useContext(MarkerContext);\n  if (!context) {\n    throw new Error(\"Marker components must be used within MapMarker\");\n  }\n  return context;\n}\n\ntype MapMarkerProps = {\n  /** Longitude coordinate for marker position */\n  longitude: number;\n  /** Latitude coordinate for marker position */\n  latitude: number;\n  /** Marker subcomponents (MarkerContent, MarkerPopup, MarkerTooltip, MarkerLabel) */\n  children: ReactNode;\n  /** Callback when marker is clicked */\n  onClick?: (e: MouseEvent) => void;\n  /** Callback when mouse enters marker */\n  onMouseEnter?: (e: MouseEvent) => void;\n  /** Callback when mouse leaves marker */\n  onMouseLeave?: (e: MouseEvent) => void;\n  /** Callback when marker drag starts (requires draggable: true) */\n  onDragStart?: (lngLat: { lng: number; lat: number }) => void;\n  /** Callback during marker drag (requires draggable: true) */\n  onDrag?: (lngLat: { lng: number; lat: number }) => void;\n  /** Callback when marker drag ends (requires draggable: true) */\n  onDragEnd?: (lngLat: { lng: number; lat: number }) => void;\n} & Omit<MarkerOptions, \"element\">;\n\nfunction MapMarker({\n  longitude,\n  latitude,\n  children,\n  onClick,\n  onMouseEnter,\n  onMouseLeave,\n  onDragStart,\n  onDrag,\n  onDragEnd,\n  draggable = false,\n  ...markerOptions\n}: MapMarkerProps) {\n  const { map } = useMap();\n\n  const callbacksRef = useRef({\n    onClick,\n    onMouseEnter,\n    onMouseLeave,\n    onDragStart,\n    onDrag,\n    onDragEnd,\n  });\n  callbacksRef.current = {\n    onClick,\n    onMouseEnter,\n    onMouseLeave,\n    onDragStart,\n    onDrag,\n    onDragEnd,\n  };\n\n  const marker = useMemo(() => {\n    const markerInstance = new MapLibreGL.Marker({\n      ...markerOptions,\n      element: document.createElement(\"div\"),\n      draggable,\n    }).setLngLat([longitude, latitude]);\n\n    const handleClick = (e: MouseEvent) => callbacksRef.current.onClick?.(e);\n    const handleMouseEnter = (e: MouseEvent) =>\n      callbacksRef.current.onMouseEnter?.(e);\n    const handleMouseLeave = (e: MouseEvent) =>\n      callbacksRef.current.onMouseLeave?.(e);\n\n    markerInstance.getElement()?.addEventListener(\"click\", handleClick);\n    markerInstance\n      .getElement()\n      ?.addEventListener(\"mouseenter\", handleMouseEnter);\n    markerInstance\n      .getElement()\n      ?.addEventListener(\"mouseleave\", handleMouseLeave);\n\n    const handleDragStart = () => {\n      const lngLat = markerInstance.getLngLat();\n      callbacksRef.current.onDragStart?.({ lng: lngLat.lng, lat: lngLat.lat });\n    };\n    const handleDrag = () => {\n      const lngLat = markerInstance.getLngLat();\n      callbacksRef.current.onDrag?.({ lng: lngLat.lng, lat: lngLat.lat });\n    };\n    const handleDragEnd = () => {\n      const lngLat = markerInstance.getLngLat();\n      callbacksRef.current.onDragEnd?.({ lng: lngLat.lng, lat: lngLat.lat });\n    };\n\n    markerInstance.on(\"dragstart\", handleDragStart);\n    markerInstance.on(\"drag\", handleDrag);\n    markerInstance.on(\"dragend\", handleDragEnd);\n\n    return markerInstance;\n\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  useEffect(() => {\n    if (!map) return;\n\n    marker.addTo(map);\n\n    return () => {\n      marker.remove();\n    };\n\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [map]);\n\n  const { offset, rotation, rotationAlignment, pitchAlignment } = markerOptions;\n\n  useEffect(() => {\n    const current = marker.getLngLat();\n    if (current.lng !== longitude || current.lat !== latitude) {\n      marker.setLngLat([longitude, latitude]);\n    }\n\n    if (marker.isDraggable() !== draggable) {\n      marker.setDraggable(draggable);\n    }\n\n    const currentOffset = marker.getOffset();\n    const newOffset = offset ?? [0, 0];\n    const [newOffsetX, newOffsetY] = Array.isArray(newOffset)\n      ? newOffset\n      : [newOffset.x, newOffset.y];\n    if (currentOffset.x !== newOffsetX || currentOffset.y !== newOffsetY) {\n      marker.setOffset(newOffset);\n    }\n\n    if (marker.getRotation() !== (rotation ?? 0)) {\n      marker.setRotation(rotation ?? 0);\n    }\n    if (marker.getRotationAlignment() !== (rotationAlignment ?? \"auto\")) {\n      marker.setRotationAlignment(rotationAlignment ?? \"auto\");\n    }\n    if (marker.getPitchAlignment() !== (pitchAlignment ?? \"auto\")) {\n      marker.setPitchAlignment(pitchAlignment ?? \"auto\");\n    }\n  }, [\n    marker,\n    longitude,\n    latitude,\n    draggable,\n    offset,\n    rotation,\n    rotationAlignment,\n    pitchAlignment,\n  ]);\n\n  return (\n    <MarkerContext.Provider value={{ marker, map }}>\n      {children}\n    </MarkerContext.Provider>\n  );\n}\n\ntype MarkerContentProps = {\n  /** Custom marker content. Defaults to a blue dot if not provided */\n  children?: ReactNode;\n  /** Additional CSS classes for the marker container */\n  className?: string;\n};\n\nfunction MarkerContent({ children, className }: MarkerContentProps) {\n  const { marker } = useMarkerContext();\n\n  return createPortal(\n    <div className={cn(\"relative cursor-pointer\", className)}>\n      {children || <DefaultMarkerIcon />}\n    </div>,\n    marker.getElement(),\n  );\n}\n\nfunction DefaultMarkerIcon() {\n  return (\n    <div className=\"relative h-4 w-4 rounded-full border-2 border-white bg-blue-500 shadow-lg\" />\n  );\n}\n\nfunction PopupCloseButton({ onClick }: { onClick: () => void }) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      aria-label=\"Close popup\"\n      className=\"focus-visible:ring-ring hover:bg-muted text-foreground absolute top-1 right-1 z-10 inline-flex size-5 cursor-pointer items-center justify-center rounded-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-inset\"\n    >\n      <X className=\"size-3.5\" />\n    </button>\n  );\n}\n\ntype MarkerPopupProps = {\n  /** Popup content */\n  children: ReactNode;\n  /** Additional CSS classes for the popup container */\n  className?: string;\n  /** Show a close button in the popup (default: false) */\n  closeButton?: boolean;\n} & Omit<PopupOptions, \"className\" | \"closeButton\">;\n\nfunction MarkerPopup({\n  children,\n  className,\n  closeButton = false,\n  ...popupOptions\n}: MarkerPopupProps) {\n  const { marker, map } = useMarkerContext();\n  const container = useMemo(() => document.createElement(\"div\"), []);\n  const { offset, maxWidth } = popupOptions;\n\n  const popup = useMemo(() => {\n    const popupInstance = new MapLibreGL.Popup({\n      offset: 16,\n      ...popupOptions,\n      closeButton: false,\n    })\n      .setMaxWidth(\"none\")\n      .setDOMContent(container);\n\n    return popupInstance;\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  useEffect(() => {\n    if (!map) return;\n\n    popup.setDOMContent(container);\n    marker.setPopup(popup);\n\n    return () => {\n      marker.setPopup(null);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [map]);\n\n  // Sync popup options when they change.\n  useEffect(() => {\n    popup.setOffset(offset ?? 16);\n    if (maxWidth) {\n      popup.setMaxWidth(maxWidth);\n    }\n  }, [popup, offset, maxWidth]);\n\n  const handleClose = () => popup.remove();\n\n  return createPortal(\n    <div\n      className={cn(\n        \"bg-popover text-popover-foreground relative max-w-62 rounded-md border p-3 shadow-md\",\n        \"animate-in fade-in-0 zoom-in-95 duration-200 ease-out\",\n        className,\n      )}\n    >\n      {closeButton && <PopupCloseButton onClick={handleClose} />}\n      {children}\n    </div>,\n    container,\n  );\n}\n\ntype MarkerTooltipProps = {\n  /** Tooltip content */\n  children: ReactNode;\n  /** Additional CSS classes for the tooltip container */\n  className?: string;\n} & Omit<PopupOptions, \"className\" | \"closeButton\" | \"closeOnClick\">;\n\nfunction MarkerTooltip({\n  children,\n  className,\n  ...popupOptions\n}: MarkerTooltipProps) {\n  const { marker, map } = useMarkerContext();\n  const container = useMemo(() => document.createElement(\"div\"), []);\n  const { offset, maxWidth } = popupOptions;\n\n  const tooltip = useMemo(() => {\n    const tooltipInstance = new MapLibreGL.Popup({\n      offset: 16,\n      ...popupOptions,\n      closeOnClick: true,\n      closeButton: false,\n    }).setMaxWidth(\"none\");\n\n    return tooltipInstance;\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  useEffect(() => {\n    if (!map) return;\n\n    tooltip.setDOMContent(container);\n\n    const handleMouseEnter = () => {\n      tooltip.setLngLat(marker.getLngLat()).addTo(map);\n    };\n    const handleMouseLeave = () => tooltip.remove();\n\n    marker.getElement()?.addEventListener(\"mouseenter\", handleMouseEnter);\n    marker.getElement()?.addEventListener(\"mouseleave\", handleMouseLeave);\n\n    return () => {\n      marker.getElement()?.removeEventListener(\"mouseenter\", handleMouseEnter);\n      marker.getElement()?.removeEventListener(\"mouseleave\", handleMouseLeave);\n      tooltip.remove();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [map]);\n\n  // Sync tooltip options when they change.\n  useEffect(() => {\n    tooltip.setOffset(offset ?? 16);\n    if (maxWidth) {\n      tooltip.setMaxWidth(maxWidth);\n    }\n  }, [tooltip, offset, maxWidth]);\n\n  return createPortal(\n    <div\n      className={cn(\n        \"bg-foreground text-background pointer-events-none rounded-md px-2 py-1 text-xs text-balance shadow-md\",\n        \"animate-in fade-in-0 zoom-in-95 duration-200 ease-out\",\n        className,\n      )}\n    >\n      {children}\n    </div>,\n    container,\n  );\n}\n\ntype MarkerLabelProps = {\n  /** Label text content */\n  children: ReactNode;\n  /** Additional CSS classes for the label */\n  className?: string;\n  /** Position of the label relative to the marker (default: \"top\") */\n  position?: \"top\" | \"bottom\";\n};\n\nfunction MarkerLabel({\n  children,\n  className,\n  position = \"top\",\n}: MarkerLabelProps) {\n  const positionClasses = {\n    top: \"bottom-full mb-1\",\n    bottom: \"top-full mt-1\",\n  };\n\n  return (\n    <div\n      className={cn(\n        \"absolute left-1/2 -translate-x-1/2 whitespace-nowrap\",\n        \"text-foreground text-[10px] font-medium\",\n        positionClasses[position],\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n\ntype MapControlsProps = {\n  /** Position of the controls on the map (default: \"bottom-right\") */\n  position?: \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\";\n  /** Show zoom in/out buttons (default: true) */\n  showZoom?: boolean;\n  /** Show compass button to reset bearing (default: false) */\n  showCompass?: boolean;\n  /** Show locate button to find user's location (default: false) */\n  showLocate?: boolean;\n  /** Show fullscreen toggle button (default: false) */\n  showFullscreen?: boolean;\n  /** Additional CSS classes for the controls container */\n  className?: string;\n  /** Callback with user coordinates when located */\n  onLocate?: (coords: { longitude: number; latitude: number }) => void;\n};\n\nconst positionClasses = {\n  \"top-left\": \"top-2 left-2\",\n  \"top-right\": \"top-2 right-2\",\n  \"bottom-left\": \"bottom-2 left-2\",\n  \"bottom-right\": \"bottom-10 right-2\",\n};\n\nfunction ControlGroup({ children }: { children: React.ReactNode }) {\n  return (\n    <div className=\"border-border bg-background [&>button:not(:last-child)]:border-border flex flex-col overflow-hidden rounded-md border shadow-sm [&>button:not(:last-child)]:border-b\">\n      {children}\n    </div>\n  );\n}\n\nfunction ControlButton({\n  onClick,\n  label,\n  children,\n  disabled = false,\n}: {\n  onClick: () => void;\n  label: string;\n  children: React.ReactNode;\n  disabled?: boolean;\n}) {\n  return (\n    <button\n      onClick={onClick}\n      aria-label={label}\n      type=\"button\"\n      className={cn(\n        \"flex size-8 items-center justify-center transition-all\",\n        \"first:rounded-t-md last:rounded-b-md\",\n        \"hover:bg-accent dark:hover:bg-accent/40\",\n        \"focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset\",\n        \"disabled:pointer-events-none disabled:opacity-50\",\n      )}\n      disabled={disabled}\n    >\n      {children}\n    </button>\n  );\n}\n\nfunction MapControls({\n  position = \"bottom-right\",\n  showZoom = true,\n  showCompass = false,\n  showLocate = false,\n  showFullscreen = false,\n  className,\n  onLocate,\n}: MapControlsProps) {\n  const { map } = useMap();\n  const [waitingForLocation, setWaitingForLocation] = useState(false);\n\n  const handleZoomIn = useCallback(() => {\n    map?.zoomTo(map.getZoom() + 1, { duration: 300 });\n  }, [map]);\n\n  const handleZoomOut = useCallback(() => {\n    map?.zoomTo(map.getZoom() - 1, { duration: 300 });\n  }, [map]);\n\n  const handleResetBearing = useCallback(() => {\n    map?.resetNorthPitch({ duration: 300 });\n  }, [map]);\n\n  const handleLocate = useCallback(() => {\n    setWaitingForLocation(true);\n    if (\"geolocation\" in navigator) {\n      navigator.geolocation.getCurrentPosition(\n        (pos) => {\n          const coords = {\n            longitude: pos.coords.longitude,\n            latitude: pos.coords.latitude,\n          };\n          map?.flyTo({\n            center: [coords.longitude, coords.latitude],\n            zoom: 14,\n            duration: 1500,\n          });\n          onLocate?.(coords);\n          setWaitingForLocation(false);\n        },\n        (error) => {\n          console.error(\"Error getting location:\", error);\n          setWaitingForLocation(false);\n        },\n      );\n    }\n  }, [map, onLocate]);\n\n  const handleFullscreen = useCallback(() => {\n    const container = map?.getContainer();\n    if (!container) return;\n    if (document.fullscreenElement) {\n      document.exitFullscreen();\n    } else {\n      container.requestFullscreen();\n    }\n  }, [map]);\n\n  return (\n    <div\n      className={cn(\n        \"absolute z-10 flex flex-col gap-1.5\",\n        positionClasses[position],\n        className,\n      )}\n    >\n      {showZoom && (\n        <ControlGroup>\n          <ControlButton onClick={handleZoomIn} label=\"Zoom in\">\n            <Plus className=\"size-4\" />\n          </ControlButton>\n          <ControlButton onClick={handleZoomOut} label=\"Zoom out\">\n            <Minus className=\"size-4\" />\n          </ControlButton>\n        </ControlGroup>\n      )}\n      {showCompass && (\n        <ControlGroup>\n          <CompassButton onClick={handleResetBearing} />\n        </ControlGroup>\n      )}\n      {showLocate && (\n        <ControlGroup>\n          <ControlButton\n            onClick={handleLocate}\n            label=\"Find my location\"\n            disabled={waitingForLocation}\n          >\n            {waitingForLocation ? (\n              <Loader2 className=\"size-4 animate-spin\" />\n            ) : (\n              <Locate className=\"size-4\" />\n            )}\n          </ControlButton>\n        </ControlGroup>\n      )}\n      {showFullscreen && (\n        <ControlGroup>\n          <ControlButton onClick={handleFullscreen} label=\"Toggle fullscreen\">\n            <Maximize className=\"size-4\" />\n          </ControlButton>\n        </ControlGroup>\n      )}\n    </div>\n  );\n}\n\nfunction CompassButton({ onClick }: { onClick: () => void }) {\n  const { map } = useMap();\n  const compassRef = useRef<SVGSVGElement>(null);\n\n  useEffect(() => {\n    if (!map || !compassRef.current) return;\n\n    const compass = compassRef.current;\n\n    const updateRotation = () => {\n      const bearing = map.getBearing();\n      const pitch = map.getPitch();\n      compass.style.transform = `rotateX(${pitch}deg) rotateZ(${-bearing}deg)`;\n    };\n\n    map.on(\"rotate\", updateRotation);\n    map.on(\"pitch\", updateRotation);\n    updateRotation();\n\n    return () => {\n      map.off(\"rotate\", updateRotation);\n      map.off(\"pitch\", updateRotation);\n    };\n  }, [map]);\n\n  return (\n    <ControlButton onClick={onClick} label=\"Reset bearing to north\">\n      <svg\n        ref={compassRef}\n        viewBox=\"0 0 24 24\"\n        className=\"size-5 transition-transform duration-200\"\n        style={{ transformStyle: \"preserve-3d\" }}\n      >\n        <path d=\"M12 2L16 12H12V2Z\" className=\"fill-red-500\" />\n        <path d=\"M12 2L8 12H12V2Z\" className=\"fill-red-300\" />\n        <path d=\"M12 22L16 12H12V22Z\" className=\"fill-muted-foreground/60\" />\n        <path d=\"M12 22L8 12H12V22Z\" className=\"fill-muted-foreground/30\" />\n      </svg>\n    </ControlButton>\n  );\n}\n\ntype MapPopupProps = {\n  /** Longitude coordinate for popup position */\n  longitude: number;\n  /** Latitude coordinate for popup position */\n  latitude: number;\n  /** Callback when popup is closed */\n  onClose?: () => void;\n  /** Popup content */\n  children: ReactNode;\n  /** Additional CSS classes for the popup container */\n  className?: string;\n  /** Show a close button in the popup (default: false) */\n  closeButton?: boolean;\n} & Omit<PopupOptions, \"className\" | \"closeButton\">;\n\nfunction MapPopup({\n  longitude,\n  latitude,\n  onClose,\n  children,\n  className,\n  closeButton = false,\n  ...popupOptions\n}: MapPopupProps) {\n  const { map } = useMap();\n  const onCloseRef = useRef(onClose);\n  onCloseRef.current = onClose;\n  const container = useMemo(() => document.createElement(\"div\"), []);\n  const { offset, maxWidth } = popupOptions;\n\n  const popup = useMemo(() => {\n    const popupInstance = new MapLibreGL.Popup({\n      offset: 16,\n      ...popupOptions,\n      closeButton: false,\n    })\n      .setMaxWidth(\"none\")\n      .setLngLat([longitude, latitude]);\n\n    return popupInstance;\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  useEffect(() => {\n    if (!map) return;\n\n    const onCloseProp = () => onCloseRef.current?.();\n\n    popup.on(\"close\", onCloseProp);\n\n    popup.setDOMContent(container);\n    popup.addTo(map);\n\n    return () => {\n      popup.off(\"close\", onCloseProp);\n      if (popup.isOpen()) {\n        popup.remove();\n      }\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [map]);\n\n  // Sync popup position and options when they change.\n  useEffect(() => {\n    const current = popup.getLngLat();\n    if (!current || current.lng !== longitude || current.lat !== latitude) {\n      popup.setLngLat([longitude, latitude]);\n    }\n    popup.setOffset(offset ?? 16);\n    if (maxWidth) {\n      popup.setMaxWidth(maxWidth);\n    }\n  }, [popup, longitude, latitude, offset, maxWidth]);\n\n  const handleClose = () => {\n    popup.remove();\n  };\n\n  return createPortal(\n    <div\n      className={cn(\n        \"bg-popover text-popover-foreground relative max-w-62 rounded-md border p-3 shadow-md\",\n        \"animate-in fade-in-0 zoom-in-95 duration-200 ease-out\",\n        className,\n      )}\n    >\n      {closeButton && <PopupCloseButton onClick={handleClose} />}\n      {children}\n    </div>,\n    container,\n  );\n}\n\ntype MapRouteProps = {\n  /** Optional unique identifier for the route layer */\n  id?: string;\n  /** Array of [longitude, latitude] coordinate pairs defining the route */\n  coordinates: [number, number][];\n  /** Line color as CSS color value (default: \"#4285F4\") */\n  color?: string;\n  /** Line width in pixels (default: 3) */\n  width?: number;\n  /** Line opacity from 0 to 1 (default: 0.8) */\n  opacity?: number;\n  /** Dash pattern [dash length, gap length] for dashed lines */\n  dashArray?: [number, number];\n  /** Callback when the route line is clicked */\n  onClick?: () => void;\n  /** Callback when mouse enters the route line */\n  onMouseEnter?: () => void;\n  /** Callback when mouse leaves the route line */\n  onMouseLeave?: () => void;\n  /** Whether the route is interactive - shows pointer cursor on hover (default: true) */\n  interactive?: boolean;\n};\n\nfunction MapRoute({\n  id: propId,\n  coordinates,\n  color = \"#4285F4\",\n  width = 3,\n  opacity = 0.8,\n  dashArray,\n  onClick,\n  onMouseEnter,\n  onMouseLeave,\n  interactive = true,\n}: MapRouteProps) {\n  const { map, isLoaded } = useMap();\n  const autoId = useId();\n  const id = propId ?? autoId;\n  const sourceId = `route-source-${id}`;\n  const layerId = `route-layer-${id}`;\n\n  // Add source and layer on mount\n  useEffect(() => {\n    if (!isLoaded || !map) return;\n\n    map.addSource(sourceId, {\n      type: \"geojson\",\n      data: {\n        type: \"Feature\",\n        properties: {},\n        geometry: { type: \"LineString\", coordinates: [] },\n      },\n    });\n\n    map.addLayer({\n      id: layerId,\n      type: \"line\",\n      source: sourceId,\n      layout: { \"line-join\": \"round\", \"line-cap\": \"round\" },\n      paint: {\n        \"line-color\": color,\n        \"line-width\": width,\n        \"line-opacity\": opacity,\n        ...(dashArray && { \"line-dasharray\": dashArray }),\n      },\n    });\n\n    return () => {\n      try {\n        if (map.getLayer(layerId)) map.removeLayer(layerId);\n        if (map.getSource(sourceId)) map.removeSource(sourceId);\n      } catch {\n        // ignore\n      }\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isLoaded, map]);\n\n  // When coordinates change, update the source data\n  useEffect(() => {\n    if (!isLoaded || !map || coordinates.length < 2) return;\n\n    const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource;\n    if (source) {\n      source.setData({\n        type: \"Feature\",\n        properties: {},\n        geometry: { type: \"LineString\", coordinates },\n      });\n    }\n  }, [isLoaded, map, coordinates, sourceId]);\n\n  useEffect(() => {\n    if (!isLoaded || !map || !map.getLayer(layerId)) return;\n\n    map.setPaintProperty(layerId, \"line-color\", color);\n    map.setPaintProperty(layerId, \"line-width\", width);\n    map.setPaintProperty(layerId, \"line-opacity\", opacity);\n    map.setPaintProperty(layerId, \"line-dasharray\", dashArray);\n  }, [isLoaded, map, layerId, color, width, opacity, dashArray]);\n\n  // Handle click and hover events\n  useEffect(() => {\n    if (!isLoaded || !map || !interactive) return;\n\n    const handleClick = () => {\n      onClick?.();\n    };\n    const handleMouseEnter = () => {\n      map.getCanvas().style.cursor = \"pointer\";\n      onMouseEnter?.();\n    };\n    const handleMouseLeave = () => {\n      map.getCanvas().style.cursor = \"\";\n      onMouseLeave?.();\n    };\n\n    map.on(\"click\", layerId, handleClick);\n    map.on(\"mouseenter\", layerId, handleMouseEnter);\n    map.on(\"mouseleave\", layerId, handleMouseLeave);\n\n    return () => {\n      map.off(\"click\", layerId, handleClick);\n      map.off(\"mouseenter\", layerId, handleMouseEnter);\n      map.off(\"mouseleave\", layerId, handleMouseLeave);\n    };\n  }, [\n    isLoaded,\n    map,\n    layerId,\n    onClick,\n    onMouseEnter,\n    onMouseLeave,\n    interactive,\n  ]);\n\n  return null;\n}\n\ntype MapGeoJSONData<\n  P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties,\n> =\n  | GeoJSON.FeatureCollection<GeoJSON.Geometry, P>\n  | GeoJSON.Feature<GeoJSON.Geometry, P>\n  | GeoJSON.Geometry\n  | string;\n\ntype MapFillPaint = NonNullable<MapLibreGL.FillLayerSpecification[\"paint\"]>;\ntype MapLinePaint = NonNullable<MapLibreGL.LineLayerSpecification[\"paint\"]>;\n\n/** A rendered feature with strongly-typed `properties`. */\ntype MapGeoJSONFeature<\n  P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties,\n> = Omit<MapLibreGL.MapGeoJSONFeature, \"properties\"> & { properties: P };\n\n/** Event payload passed to MapGeoJSON interaction callbacks. */\ntype MapGeoJSONEvent<\n  P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties,\n> = {\n  /** The feature under the cursor, with its typed GeoJSON properties. */\n  feature: MapGeoJSONFeature<P>;\n  /** Longitude of the cursor at the time of the event. */\n  longitude: number;\n  /** Latitude of the cursor at the time of the event. */\n  latitude: number;\n  /** The underlying MapLibre mouse event for advanced use cases. */\n  originalEvent: MapLibreGL.MapLayerMouseEvent;\n};\n\ntype MapGeoJSONProps<\n  P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties,\n> = {\n  /** GeoJSON data (FeatureCollection, Feature, Geometry) or a URL to fetch it from. */\n  data: MapGeoJSONData<P>;\n  /** Optional unique identifier prefix for the source/layers. Auto-generated if not provided. */\n  id?: string;\n  /**\n   * Feature property to promote to the feature `id`. Required for hover\n   * feature-state (`fillHoverPaint`) and stable `onHover`/`onClick` payloads.\n   */\n  promoteId?: string;\n  /**\n   * Paint for the polygon fill layer. Merged on top of a theme-aware monochrome\n   * surface tone (`fill-color`). Pass `false` to omit the fill layer entirely\n   * (e.g. outlines only).\n   */\n  fillPaint?: MapFillPaint | false;\n  /**\n   * Paint for the outline layer. Merged on top of a hairline default\n   * (`line-color` = a near-surface neutral, `line-width` = 0.5) for thin\n   * separators. Override `line-color` if your container differs, or pass\n   * `false` to omit the layer.\n   */\n  linePaint?: MapLinePaint | false;\n  /**\n   * Paint merged onto the fill layer for the feature under the cursor, applied\n   * as a `case` expression keyed on hover feature-state. Requires `promoteId`.\n   */\n  fillHoverPaint?: MapFillPaint;\n  /** Callback when a feature is clicked. */\n  onClick?: (e: MapGeoJSONEvent<P>) => void;\n  /** Callback fired when the hovered feature changes; `null` when the cursor leaves. */\n  onHover?: (e: MapGeoJSONEvent<P> | null) => void;\n  /** Whether features respond to mouse events (default: false). */\n  interactive?: boolean;\n  /** Optional MapLibre layer id to insert the layers before (z-order control). */\n  beforeId?: string;\n};\n\n// Monochrome defaults: a neutral-gray fill (hex of the grayscale chart tokens)\n// with a fixed near-surface line for thin separators. Colors are hardcoded (not\n// theme tokens), tuned for a typical light/dark surface. Override via\n// `fillPaint` / `linePaint`.\nconst GEOJSON_DEFAULT_COLORS = {\n  light: { fill: \"#d4d4d4\", line: \"#ffffff\" },\n  dark: { fill: \"#404040\", line: \"#171717\" },\n} satisfies Record<Theme, { fill: string; line: string }>;\n\n/**\n * Renders arbitrary GeoJSON as fill + outline layers on the map. Composes like\n * `MapRoute` / `MapArc` — drop it inside `<Map>` (typically with `blank`) for\n * choropleths and region/data maps. For full control over expressions and\n * multiple layers, manage layers directly via `useMap()` instead.\n */\nfunction MapGeoJSON<\n  P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties,\n>({\n  data,\n  id: propId,\n  promoteId,\n  fillPaint,\n  linePaint,\n  fillHoverPaint,\n  onClick,\n  onHover,\n  interactive = false,\n  beforeId,\n}: MapGeoJSONProps<P>) {\n  const { map, isLoaded, resolvedTheme } = useMap();\n  const autoId = useId();\n  const id = propId ?? autoId;\n  const sourceId = `geojson-source-${id}`;\n  const fillLayerId = `geojson-fill-${id}`;\n  const lineLayerId = `geojson-line-${id}`;\n\n  const defaults = GEOJSON_DEFAULT_COLORS[resolvedTheme];\n\n  const showFill = fillPaint !== false;\n  const showLine = linePaint !== false;\n\n  const mergedFillPaint = useMemo(\n    () =>\n      mergeHoverPaint(\n        { \"fill-color\": defaults.fill, ...(fillPaint || {}) },\n        fillHoverPaint,\n      ),\n    [defaults.fill, fillPaint, fillHoverPaint],\n  );\n  const mergedLinePaint = useMemo(\n    () => ({\n      \"line-color\": defaults.line,\n      \"line-width\": 0.5,\n      ...(linePaint || {}),\n    }),\n    [defaults.line, linePaint],\n  );\n  const latestRef = useRef({ onClick, onHover });\n  latestRef.current = { onClick, onHover };\n\n  // Add source on mount.\n  useEffect(() => {\n    if (!isLoaded || !map) return;\n\n    map.addSource(sourceId, {\n      type: \"geojson\",\n      data,\n      ...(promoteId ? { promoteId } : {}),\n    });\n\n    return () => {\n      try {\n        if (map.getLayer(lineLayerId)) map.removeLayer(lineLayerId);\n        if (map.getLayer(fillLayerId)) map.removeLayer(fillLayerId);\n        if (map.getSource(sourceId)) map.removeSource(sourceId);\n      } catch {\n        // style may be mid-reload\n      }\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isLoaded, map]);\n\n  // Sync data when it changes.\n  useEffect(() => {\n    if (!isLoaded || !map) return;\n    const source = map.getSource(sourceId) as\n      | MapLibreGL.GeoJSONSource\n      | undefined;\n    source?.setData(data as never);\n  }, [isLoaded, map, data, sourceId]);\n\n  // Sync layers and paint when visibility or styling changes.\n  useEffect(() => {\n    if (!isLoaded || !map) return;\n\n    const source = map.getSource(sourceId);\n    if (!source) return;\n\n    if (showFill && !map.getLayer(fillLayerId)) {\n      map.addLayer(\n        {\n          id: fillLayerId,\n          type: \"fill\",\n          source: sourceId,\n          paint: mergedFillPaint,\n        },\n        beforeId,\n      );\n    } else if (!showFill && map.getLayer(fillLayerId)) {\n      map.removeLayer(fillLayerId);\n    }\n\n    if (showLine && !map.getLayer(lineLayerId)) {\n      map.addLayer(\n        {\n          id: lineLayerId,\n          type: \"line\",\n          source: sourceId,\n          paint: mergedLinePaint,\n        },\n        beforeId,\n      );\n    } else if (!showLine && map.getLayer(lineLayerId)) {\n      map.removeLayer(lineLayerId);\n    }\n\n    if (showFill && map.getLayer(fillLayerId)) {\n      for (const [key, value] of Object.entries(mergedFillPaint)) {\n        map.setPaintProperty(\n          fillLayerId,\n          key as keyof MapFillPaint,\n          value as never,\n        );\n      }\n    }\n    if (showLine && map.getLayer(lineLayerId)) {\n      for (const [key, value] of Object.entries(mergedLinePaint)) {\n        map.setPaintProperty(\n          lineLayerId,\n          key as keyof MapLinePaint,\n          value as never,\n        );\n      }\n    }\n  }, [\n    isLoaded,\n    map,\n    sourceId,\n    fillLayerId,\n    lineLayerId,\n    showFill,\n    showLine,\n    mergedFillPaint,\n    mergedLinePaint,\n    beforeId,\n  ]);\n\n  // Interaction handlers (bound to the fill layer).\n  useEffect(() => {\n    if (!isLoaded || !map || !interactive || !showFill) return;\n\n    let hoveredId: string | number | null = null;\n\n    const setHover = (next: string | number | null) => {\n      if (next === hoveredId) return;\n      const sourceExists = !!map.getSource(sourceId);\n      if (hoveredId != null && sourceExists) {\n        map.setFeatureState(\n          { source: sourceId, id: hoveredId },\n          { hover: false },\n        );\n      }\n      hoveredId = next;\n      if (next != null && sourceExists) {\n        map.setFeatureState({ source: sourceId, id: next }, { hover: true });\n      }\n    };\n\n    const handleMouseMove = (e: MapLibreGL.MapLayerMouseEvent) => {\n      const feature = e.features?.[0];\n      if (!feature) return;\n      map.getCanvas().style.cursor = \"pointer\";\n\n      const featureId = feature.id;\n      if (featureId === hoveredId) return;\n      setHover(featureId ?? null);\n      latestRef.current.onHover?.({\n        feature: feature as unknown as MapGeoJSONFeature<P>,\n        longitude: e.lngLat.lng,\n        latitude: e.lngLat.lat,\n        originalEvent: e,\n      });\n    };\n\n    const handleMouseLeave = () => {\n      setHover(null);\n      map.getCanvas().style.cursor = \"\";\n      latestRef.current.onHover?.(null);\n    };\n\n    const handleClick = (e: MapLibreGL.MapLayerMouseEvent) => {\n      const feature = e.features?.[0];\n      if (!feature) return;\n      latestRef.current.onClick?.({\n        feature: feature as unknown as MapGeoJSONFeature<P>,\n        longitude: e.lngLat.lng,\n        latitude: e.lngLat.lat,\n        originalEvent: e,\n      });\n    };\n\n    map.on(\"mousemove\", fillLayerId, handleMouseMove);\n    map.on(\"mouseleave\", fillLayerId, handleMouseLeave);\n    map.on(\"click\", fillLayerId, handleClick);\n\n    return () => {\n      map.off(\"mousemove\", fillLayerId, handleMouseMove);\n      map.off(\"mouseleave\", fillLayerId, handleMouseLeave);\n      map.off(\"click\", fillLayerId, handleClick);\n      setHover(null);\n      map.getCanvas().style.cursor = \"\";\n    };\n  }, [isLoaded, map, fillLayerId, sourceId, interactive, showFill]);\n\n  return null;\n}\n\n/** A single arc to render inside <MapArc data={...}>. */\ntype MapArcDatum = {\n  /** Unique identifier for this arc. Required for hover state tracking and event payloads. */\n  id: string | number;\n  /** Start coordinate as [longitude, latitude]. */\n  from: [number, number];\n  /** End coordinate as [longitude, latitude]. */\n  to: [number, number];\n};\n\n/** Event payload passed to MapArc interaction callbacks. */\ntype MapArcEvent<T extends MapArcDatum = MapArcDatum> = {\n  /** The arc datum that was hovered or clicked. */\n  arc: T;\n  /** Longitude of the cursor at the time of the event. */\n  longitude: number;\n  /** Latitude of the cursor at the time of the event. */\n  latitude: number;\n  /** The underlying MapLibre mouse event for advanced use cases. */\n  originalEvent: MapLibreGL.MapMouseEvent;\n};\n\ntype MapArcLinePaint = NonNullable<MapLibreGL.LineLayerSpecification[\"paint\"]>;\ntype MapArcLineLayout = NonNullable<\n  MapLibreGL.LineLayerSpecification[\"layout\"]\n>;\n\ntype MapArcProps<T extends MapArcDatum = MapArcDatum> = {\n  /** Array of arcs to render. Each arc must have a unique `id`. */\n  data: T[];\n  /** Optional unique identifier prefix for the arc source/layers. Auto-generated if not provided. */\n  id?: string;\n  /**\n   * How far each arc bows away from a straight line. `0` renders straight\n   * lines; higher values bend further. Negative values bend to the opposite\n   * side. Arcs are computed as a quadratic Bézier in lng/lat space; the\n   * destination longitude is unwrapped relative to the origin so that arcs\n   * cross the antimeridian via the shorter great-circle direction. (default: 0.2)\n   */\n  curvature?: number;\n  /** Number of samples used to render each curve. Higher = smoother. (default: 64) */\n  samples?: number;\n  /**\n   * MapLibre paint properties for the arc layer. Merged on top of sensible\n   * defaults (`line-color: #4285F4`, `line-width: 2`, `line-opacity: 0.85`).\n   * Any value can be a MapLibre expression for per-feature styling, every\n   * field on each arc datum (besides `from`/`to`) is exposed via `[\"get\", ...]`.\n   */\n  paint?: MapArcLinePaint;\n  /** MapLibre layout properties for the arc layer. Defaults to rounded joins/caps. */\n  layout?: MapArcLineLayout;\n  /**\n   * Paint properties applied to the arc currently under the cursor. Each key\n   * is merged into `paint` as a `case` expression keyed on per-feature hover\n   * state, so only the hovered arc changes appearance.\n   */\n  hoverPaint?: MapArcLinePaint;\n  /** Callback when an arc is clicked. */\n  onClick?: (e: MapArcEvent<T>) => void;\n  /**\n   * Callback fired when the hovered arc changes. Receives the cursor's\n   * lng/lat at the moment of entry, and `null` when the cursor leaves the\n   * last hovered arc.\n   */\n  onHover?: (e: MapArcEvent<T> | null) => void;\n  /** Whether arcs respond to mouse events (default: true). */\n  interactive?: boolean;\n  /** Optional MapLibre layer id to insert the arc layers before (z-order control). */\n  beforeId?: string;\n};\n\nconst DEFAULT_ARC_CURVATURE = 0.2;\nconst DEFAULT_ARC_SAMPLES = 64;\nconst ARC_HIT_MIN_WIDTH = 12;\nconst ARC_HIT_PADDING = 6;\n\nconst DEFAULT_ARC_PAINT: MapArcLinePaint = {\n  \"line-color\": \"#4285F4\",\n  \"line-width\": 2,\n  \"line-opacity\": 0.85,\n};\n\nconst DEFAULT_ARC_LAYOUT: MapArcLineLayout = {\n  \"line-join\": \"round\",\n  \"line-cap\": \"round\",\n};\n\nfunction buildArcCoordinates(\n  from: [number, number],\n  to: [number, number],\n  curvature: number,\n  samples: number,\n): [number, number][] {\n  const [x0, y0] = from;\n  const [xTo, y2] = to;\n  // Unwrap the destination longitude so |dx| <= 180. This makes arcs that\n  // straddle the antimeridian (e.g. Tokyo -> San Francisco) bow the short way\n  // across the Pacific instead of the long way around the globe. Resulting\n  // longitudes may fall outside [-180, 180]; MapLibre renders them correctly\n  // on the globe projection, and on mercator when world copies are enabled.\n  const rawDx = xTo - x0;\n  const x2 = rawDx > 180 ? xTo - 360 : rawDx < -180 ? xTo + 360 : xTo;\n  const dx = x2 - x0;\n  const dy = y2 - y0;\n  const distance = Math.hypot(dx, dy);\n\n  if (distance === 0 || curvature === 0) return [from, [x2, y2]];\n\n  const mx = (x0 + x2) / 2;\n  const my = (y0 + y2) / 2;\n  const nx = -dy / distance;\n  const ny = dx / distance;\n  const offset = distance * curvature;\n  const cx = mx + nx * offset;\n  const cy = my + ny * offset;\n\n  const points: [number, number][] = [];\n  const segments = Math.max(2, Math.floor(samples));\n  for (let i = 0; i <= segments; i += 1) {\n    const t = i / segments;\n    const inv = 1 - t;\n    const x = inv * inv * x0 + 2 * inv * t * cx + t * t * x2;\n    const y = inv * inv * y0 + 2 * inv * t * cy + t * t * y2;\n    points.push([x, y]);\n  }\n  return points;\n}\n\nfunction MapArc<T extends MapArcDatum = MapArcDatum>({\n  data,\n  id: propId,\n  curvature = DEFAULT_ARC_CURVATURE,\n  samples = DEFAULT_ARC_SAMPLES,\n  paint,\n  layout,\n  hoverPaint,\n  onClick,\n  onHover,\n  interactive = true,\n  beforeId,\n}: MapArcProps<T>) {\n  const { map, isLoaded } = useMap();\n  const autoId = useId();\n  const id = propId ?? autoId;\n  const sourceId = `arc-source-${id}`;\n  const layerId = `arc-layer-${id}`;\n  const hitLayerId = `arc-hit-layer-${id}`;\n\n  const mergedPaint = useMemo(\n    () => mergeHoverPaint({ ...DEFAULT_ARC_PAINT, ...paint }, hoverPaint),\n    [paint, hoverPaint],\n  );\n  const mergedLayout = useMemo(\n    () => ({ ...DEFAULT_ARC_LAYOUT, ...layout }),\n    [layout],\n  );\n\n  const hitWidth = useMemo(() => {\n    const w = paint?.[\"line-width\"] ?? DEFAULT_ARC_PAINT[\"line-width\"];\n    const base = typeof w === \"number\" ? w : ARC_HIT_MIN_WIDTH;\n    return Math.max(base + ARC_HIT_PADDING, ARC_HIT_MIN_WIDTH);\n  }, [paint]);\n\n  const geoJSON = useMemo<GeoJSON.FeatureCollection<GeoJSON.LineString>>(\n    () => ({\n      type: \"FeatureCollection\",\n      features: data.map((arc) => {\n        const { from, to, ...properties } = arc;\n        return {\n          type: \"Feature\",\n          properties,\n          geometry: {\n            type: \"LineString\",\n            coordinates: buildArcCoordinates(from, to, curvature, samples),\n          },\n        };\n      }),\n    }),\n    [data, curvature, samples],\n  );\n\n  const latestRef = useRef({ data, onClick, onHover });\n  latestRef.current = { data, onClick, onHover };\n\n  // Add source and layers on mount.\n  useEffect(() => {\n    if (!isLoaded || !map) return;\n\n    map.addSource(sourceId, {\n      type: \"geojson\",\n      data: geoJSON,\n      promoteId: \"id\",\n    });\n\n    map.addLayer(\n      {\n        id: hitLayerId,\n        type: \"line\",\n        source: sourceId,\n        layout: DEFAULT_ARC_LAYOUT,\n        paint: {\n          \"line-color\": \"rgba(0, 0, 0, 0)\",\n          \"line-width\": hitWidth,\n          \"line-opacity\": 1,\n        },\n      },\n      beforeId,\n    );\n\n    map.addLayer(\n      {\n        id: layerId,\n        type: \"line\",\n        source: sourceId,\n        layout: mergedLayout,\n        paint: mergedPaint,\n      },\n      beforeId,\n    );\n\n    return () => {\n      try {\n        if (map.getLayer(layerId)) map.removeLayer(layerId);\n        if (map.getLayer(hitLayerId)) map.removeLayer(hitLayerId);\n        if (map.getSource(sourceId)) map.removeSource(sourceId);\n      } catch {\n        // ignore\n      }\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isLoaded, map]);\n\n  // Sync features when data / curvature / samples change.\n  useEffect(() => {\n    if (!isLoaded || !map) return;\n    const source = map.getSource(sourceId) as\n      | MapLibreGL.GeoJSONSource\n      | undefined;\n    source?.setData(geoJSON);\n  }, [isLoaded, map, geoJSON, sourceId]);\n\n  // Sync paint/layout when they change.\n  useEffect(() => {\n    if (!isLoaded || !map || !map.getLayer(layerId)) return;\n    for (const [key, value] of Object.entries(mergedPaint)) {\n      map.setPaintProperty(\n        layerId,\n        key as keyof MapArcLinePaint,\n        value as never,\n      );\n    }\n    for (const [key, value] of Object.entries(mergedLayout)) {\n      map.setLayoutProperty(\n        layerId,\n        key as keyof MapArcLineLayout,\n        value as never,\n      );\n    }\n    if (map.getLayer(hitLayerId)) {\n      map.setPaintProperty(hitLayerId, \"line-width\", hitWidth);\n    }\n  }, [isLoaded, map, layerId, hitLayerId, mergedPaint, mergedLayout, hitWidth]);\n\n  // Interaction handlers\n  useEffect(() => {\n    if (!isLoaded || !map || !interactive) return;\n\n    let hoveredId: string | number | null = null;\n\n    const setHover = (next: string | number | null) => {\n      if (next === hoveredId) return;\n      const sourceExists = !!map.getSource(sourceId);\n      if (hoveredId != null && sourceExists) {\n        map.setFeatureState(\n          { source: sourceId, id: hoveredId },\n          { hover: false },\n        );\n      }\n      hoveredId = next;\n      if (next != null && sourceExists) {\n        map.setFeatureState({ source: sourceId, id: next }, { hover: true });\n      }\n    };\n\n    const findArc = (featureId: string | number | undefined) =>\n      featureId == null\n        ? undefined\n        : latestRef.current.data.find(\n            (arc) => String(arc.id) === String(featureId),\n          );\n\n    const handleMouseMove = (e: MapLibreGL.MapLayerMouseEvent) => {\n      const featureId = e.features?.[0]?.id as string | number | undefined;\n      if (featureId == null || featureId === hoveredId) return;\n\n      setHover(featureId);\n      map.getCanvas().style.cursor = \"pointer\";\n\n      const arc = findArc(featureId);\n      if (arc) {\n        latestRef.current.onHover?.({\n          arc: arc as T,\n          longitude: e.lngLat.lng,\n          latitude: e.lngLat.lat,\n          originalEvent: e,\n        });\n      }\n    };\n\n    const handleMouseLeave = () => {\n      setHover(null);\n      map.getCanvas().style.cursor = \"\";\n      latestRef.current.onHover?.(null);\n    };\n\n    const handleClick = (e: MapLibreGL.MapLayerMouseEvent) => {\n      const arc = findArc(e.features?.[0]?.id as string | number | undefined);\n      if (!arc) return;\n      latestRef.current.onClick?.({\n        arc: arc as T,\n        longitude: e.lngLat.lng,\n        latitude: e.lngLat.lat,\n        originalEvent: e,\n      });\n    };\n\n    map.on(\"mousemove\", hitLayerId, handleMouseMove);\n    map.on(\"mouseleave\", hitLayerId, handleMouseLeave);\n    map.on(\"click\", hitLayerId, handleClick);\n\n    return () => {\n      map.off(\"mousemove\", hitLayerId, handleMouseMove);\n      map.off(\"mouseleave\", hitLayerId, handleMouseLeave);\n      map.off(\"click\", hitLayerId, handleClick);\n      setHover(null);\n      map.getCanvas().style.cursor = \"\";\n    };\n  }, [isLoaded, map, hitLayerId, sourceId, interactive]);\n\n  return null;\n}\n\ntype MapClusterLayerProps<\n  P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties,\n> = {\n  /** GeoJSON FeatureCollection data or URL to fetch GeoJSON from */\n  data: string | GeoJSON.FeatureCollection<GeoJSON.Point, P>;\n  /** Maximum zoom level to cluster points on (default: 14) */\n  clusterMaxZoom?: number;\n  /** Radius of each cluster when clustering points in pixels (default: 50) */\n  clusterRadius?: number;\n  /** Colors for cluster circles: [small, medium, large] based on point count (default: [\"#22c55e\", \"#eab308\", \"#ef4444\"]) */\n  clusterColors?: [string, string, string];\n  /** Point count thresholds for color/size steps: [medium, large] (default: [100, 750]) */\n  clusterThresholds?: [number, number];\n  /** Color for unclustered individual points (default: \"#3b82f6\") */\n  pointColor?: string;\n  /** Callback when an unclustered point is clicked */\n  onPointClick?: (\n    feature: GeoJSON.Feature<GeoJSON.Point, P>,\n    coordinates: [number, number],\n  ) => void;\n  /** Callback when a cluster is clicked. If not provided, zooms into the cluster */\n  onClusterClick?: (\n    clusterId: number,\n    coordinates: [number, number],\n    pointCount: number,\n  ) => void;\n};\n\nconst DEFAULT_CLUSTER_COLORS: [string, string, string] = [\n  \"#22c55e\",\n  \"#eab308\",\n  \"#ef4444\",\n];\nconst DEFAULT_CLUSTER_THRESHOLDS: [number, number] = [100, 750];\n\nfunction MapClusterLayer<\n  P extends GeoJSON.GeoJsonProperties = GeoJSON.GeoJsonProperties,\n>({\n  data,\n  clusterMaxZoom = 14,\n  clusterRadius = 50,\n  clusterColors = DEFAULT_CLUSTER_COLORS,\n  clusterThresholds = DEFAULT_CLUSTER_THRESHOLDS,\n  pointColor = \"#3b82f6\",\n  onPointClick,\n  onClusterClick,\n}: MapClusterLayerProps<P>) {\n  const { map, isLoaded } = useMap();\n  const id = useId();\n  const sourceId = `cluster-source-${id}`;\n  const clusterLayerId = `clusters-${id}`;\n  const clusterCountLayerId = `cluster-count-${id}`;\n  const unclusteredLayerId = `unclustered-point-${id}`;\n\n  const stylePropsRef = useRef({\n    clusterColors,\n    clusterThresholds,\n    pointColor,\n  });\n\n  // Add source and layers on mount\n  useEffect(() => {\n    if (!isLoaded || !map) return;\n\n    // Add clustered GeoJSON source\n    map.addSource(sourceId, {\n      type: \"geojson\",\n      data,\n      cluster: true,\n      clusterMaxZoom,\n      clusterRadius,\n    });\n\n    // Add cluster circles layer\n    map.addLayer({\n      id: clusterLayerId,\n      type: \"circle\",\n      source: sourceId,\n      filter: [\"has\", \"point_count\"],\n      paint: {\n        \"circle-color\": [\n          \"step\",\n          [\"get\", \"point_count\"],\n          clusterColors[0],\n          clusterThresholds[0],\n          clusterColors[1],\n          clusterThresholds[1],\n          clusterColors[2],\n        ],\n        \"circle-radius\": [\n          \"step\",\n          [\"get\", \"point_count\"],\n          20,\n          clusterThresholds[0],\n          30,\n          clusterThresholds[1],\n          40,\n        ],\n        \"circle-stroke-width\": 1,\n        \"circle-stroke-color\": \"#fff\",\n        \"circle-opacity\": 0.85,\n      },\n    });\n\n    // Add cluster count text layer\n    map.addLayer({\n      id: clusterCountLayerId,\n      type: \"symbol\",\n      source: sourceId,\n      filter: [\"has\", \"point_count\"],\n      layout: {\n        \"text-field\": \"{point_count_abbreviated}\",\n        \"text-font\": [\"Open Sans\"],\n        \"text-size\": 12,\n      },\n      paint: {\n        \"text-color\": \"#fff\",\n      },\n    });\n\n    // Add unclustered point layer\n    map.addLayer({\n      id: unclusteredLayerId,\n      type: \"circle\",\n      source: sourceId,\n      filter: [\"!\", [\"has\", \"point_count\"]],\n      paint: {\n        \"circle-color\": pointColor,\n        \"circle-radius\": 5,\n        \"circle-stroke-width\": 2,\n        \"circle-stroke-color\": \"#fff\",\n      },\n    });\n\n    return () => {\n      try {\n        if (map.getLayer(clusterCountLayerId))\n          map.removeLayer(clusterCountLayerId);\n        if (map.getLayer(unclusteredLayerId))\n          map.removeLayer(unclusteredLayerId);\n        if (map.getLayer(clusterLayerId)) map.removeLayer(clusterLayerId);\n        if (map.getSource(sourceId)) map.removeSource(sourceId);\n      } catch {\n        // ignore\n      }\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [isLoaded, map, sourceId]);\n\n  // Update source data when data prop changes (only for non-URL data)\n  useEffect(() => {\n    if (!isLoaded || !map || typeof data === \"string\") return;\n\n    const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource;\n    if (source) {\n      source.setData(data);\n    }\n  }, [isLoaded, map, data, sourceId]);\n\n  // Update layer styles when props change\n  useEffect(() => {\n    if (!isLoaded || !map) return;\n\n    const prev = stylePropsRef.current;\n    const colorsChanged =\n      prev.clusterColors !== clusterColors ||\n      prev.clusterThresholds !== clusterThresholds;\n\n    // Update cluster layer colors and sizes\n    if (map.getLayer(clusterLayerId) && colorsChanged) {\n      map.setPaintProperty(clusterLayerId, \"circle-color\", [\n        \"step\",\n        [\"get\", \"point_count\"],\n        clusterColors[0],\n        clusterThresholds[0],\n        clusterColors[1],\n        clusterThresholds[1],\n        clusterColors[2],\n      ]);\n      map.setPaintProperty(clusterLayerId, \"circle-radius\", [\n        \"step\",\n        [\"get\", \"point_count\"],\n        20,\n        clusterThresholds[0],\n        30,\n        clusterThresholds[1],\n        40,\n      ]);\n    }\n\n    // Update unclustered point layer color\n    if (map.getLayer(unclusteredLayerId) && prev.pointColor !== pointColor) {\n      map.setPaintProperty(unclusteredLayerId, \"circle-color\", pointColor);\n    }\n\n    stylePropsRef.current = { clusterColors, clusterThresholds, pointColor };\n  }, [\n    isLoaded,\n    map,\n    clusterLayerId,\n    unclusteredLayerId,\n    clusterColors,\n    clusterThresholds,\n    pointColor,\n  ]);\n\n  // Handle click events\n  useEffect(() => {\n    if (!isLoaded || !map) return;\n\n    // Cluster click handler - zoom into cluster\n    const handleClusterClick = async (\n      e: MapLibreGL.MapMouseEvent & {\n        features?: MapLibreGL.MapGeoJSONFeature[];\n      },\n    ) => {\n      const features = map.queryRenderedFeatures(e.point, {\n        layers: [clusterLayerId],\n      });\n      if (!features.length) return;\n\n      const feature = features[0];\n      const clusterId = feature.properties?.cluster_id as number;\n      const pointCount = feature.properties?.point_count as number;\n      const coordinates = (feature.geometry as GeoJSON.Point).coordinates as [\n        number,\n        number,\n      ];\n\n      if (onClusterClick) {\n        onClusterClick(clusterId, coordinates, pointCount);\n      } else {\n        // Default behavior: zoom to cluster expansion zoom\n        const source = map.getSource(sourceId) as MapLibreGL.GeoJSONSource;\n        const zoom = await source.getClusterExpansionZoom(clusterId);\n        map.easeTo({\n          center: coordinates,\n          zoom,\n        });\n      }\n    };\n\n    // Unclustered point click handler\n    const handlePointClick = (\n      e: MapLibreGL.MapMouseEvent & {\n        features?: MapLibreGL.MapGeoJSONFeature[];\n      },\n    ) => {\n      if (!onPointClick || !e.features?.length) return;\n\n      const feature = e.features[0];\n      const coordinates = (\n        feature.geometry as GeoJSON.Point\n      ).coordinates.slice() as [number, number];\n\n      // Handle world copies\n      while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {\n        coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;\n      }\n\n      onPointClick(\n        feature as unknown as GeoJSON.Feature<GeoJSON.Point, P>,\n        coordinates,\n      );\n    };\n\n    // Cursor style handlers\n    const handleMouseEnterCluster = () => {\n      map.getCanvas().style.cursor = \"pointer\";\n    };\n    const handleMouseLeaveCluster = () => {\n      map.getCanvas().style.cursor = \"\";\n    };\n    const handleMouseEnterPoint = () => {\n      if (onPointClick) {\n        map.getCanvas().style.cursor = \"pointer\";\n      }\n    };\n    const handleMouseLeavePoint = () => {\n      map.getCanvas().style.cursor = \"\";\n    };\n\n    map.on(\"click\", clusterLayerId, handleClusterClick);\n    map.on(\"click\", unclusteredLayerId, handlePointClick);\n    map.on(\"mouseenter\", clusterLayerId, handleMouseEnterCluster);\n    map.on(\"mouseleave\", clusterLayerId, handleMouseLeaveCluster);\n    map.on(\"mouseenter\", unclusteredLayerId, handleMouseEnterPoint);\n    map.on(\"mouseleave\", unclusteredLayerId, handleMouseLeavePoint);\n\n    return () => {\n      map.off(\"click\", clusterLayerId, handleClusterClick);\n      map.off(\"click\", unclusteredLayerId, handlePointClick);\n      map.off(\"mouseenter\", clusterLayerId, handleMouseEnterCluster);\n      map.off(\"mouseleave\", clusterLayerId, handleMouseLeaveCluster);\n      map.off(\"mouseenter\", unclusteredLayerId, handleMouseEnterPoint);\n      map.off(\"mouseleave\", unclusteredLayerId, handleMouseLeavePoint);\n    };\n  }, [\n    isLoaded,\n    map,\n    clusterLayerId,\n    unclusteredLayerId,\n    sourceId,\n    onClusterClick,\n    onPointClick,\n  ]);\n\n  return null;\n}\n\nexport {\n  Map,\n  useMap,\n  MapMarker,\n  MarkerContent,\n  MarkerPopup,\n  MarkerTooltip,\n  MarkerLabel,\n  MapPopup,\n  MapControls,\n  MapRoute,\n  MapArc,\n  MapGeoJSON,\n  MapClusterLayer,\n};\n\nexport type { MapRef, MapViewport, MapArcDatum, MapArcEvent, MapGeoJSONEvent };\n",
      "type": "registry:ui",
      "target": "components/ui/map.tsx"
    }
  ],
  "css": {
    "@layer base": {
      ".maplibregl-popup-content": {
        "@apply bg-transparent! shadow-none! p-0! rounded-none!": {}
      },
      ".maplibregl-popup-tip": {
        "@apply hidden!": {}
      }
    }
  },
  "meta": {
    "components": [
      {
        "title": "Map",
        "href": "/llm/map",
        "description": "Base MapLibre-powered map component."
      },
      {
        "title": "Controls",
        "href": "/docs/controls",
        "description": "Zoom, geolocate, fullscreen, and compass controls."
      },
      {
        "title": "Markers",
        "href": "/docs/markers",
        "description": "Map markers with labels, tooltips, and popup composition."
      },
      {
        "title": "Popups",
        "href": "/docs/popups",
        "description": "Standalone and marker-attached popup content."
      },
      {
        "title": "Routes",
        "href": "/docs/routes",
        "description": "Line routes with optional markers and interaction handlers."
      },
      {
        "title": "Arcs",
        "href": "/docs/arcs",
        "description": "Curved connection arcs between coordinates."
      },
      {
        "title": "GeoJSON",
        "href": "/docs/geojson",
        "description": "GeoJSON sources and layers for custom map data."
      },
      {
        "title": "Clusters",
        "href": "/docs/clusters",
        "description": "Clustered point layers for dense datasets."
      }
    ]
  },
  "type": "registry:ui"
}