{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "map",
  "title": "Map",
  "author": "vorhdam <https://github.com/vorhdam>",
  "description": "A modular context-based map with built in controls such as projection, zoom, rotation and fullscreen.",
  "dependencies": [
    "lucide-react",
    "maplibre-gl"
  ],
  "registryDependencies": [
    "button",
    "button-group"
  ],
  "files": [
    {
      "path": "src/components/ui/loading.tsx",
      "content": "import { cn } from \"@/lib/utils\";\r\n\r\nfunction Loading({ className, ...props }: React.ComponentProps<\"div\">) {\r\n  return (\r\n    <div\r\n      className={cn(\r\n        \"absolute inset-0 z-10 flex items-center justify-center bg-background/50 backdrop-blur-xs\",\r\n        className,\r\n      )}\r\n      {...props}\r\n    >\r\n      <div className=\"flex gap-1\">\r\n        <span className=\"size-1.5 animate-pulse rounded-full bg-primary/75\" />\r\n        <span className=\"size-1.5 animate-pulse rounded-full bg-primary/75 [animation-delay:150ms]\" />\r\n        <span className=\"size-1.5 animate-pulse rounded-full bg-primary/75 [animation-delay:300ms]\" />\r\n      </div>\r\n    </div>\r\n  );\r\n}\r\n\r\nexport { Loading };\r\n",
      "type": "registry:ui"
    },
    {
      "path": "src/components/ui/map.tsx",
      "content": "\"use client\";\r\n\r\nimport {\r\n  MapLibreMap,\r\n  MapOptions,\r\n  Marker,\r\n  MarkerOptions,\r\n  Popup,\r\n  PopupOptions,\r\n  StyleSpecification,\r\n} from \"maplibre-gl\";\r\nimport \"maplibre-gl/dist/maplibre-gl.css\";\r\nimport * as React from \"react\";\r\nimport { createPortal } from \"react-dom\";\r\n\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { ButtonGroup } from \"@/components/ui/button-group\";\r\nimport { Loading } from \"@/components/ui/loading\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport {\r\n  ChevronDown,\r\n  ChevronUp,\r\n  Globe,\r\n  Loader2,\r\n  Locate,\r\n  Map as MapIcon,\r\n  MapPin,\r\n  Maximize,\r\n  Minimize,\r\n  Minus,\r\n  Plus,\r\n} from \"lucide-react\";\r\n\r\ntype Theme = \"light\" | \"dark\";\r\ntype Coordinate = { latitude: number; longitude: number };\r\ntype Projection = \"mercator\" | \"globe\";\r\ntype Style = string | StyleSpecification;\r\n\r\nconst defaultStyles: Record<Theme, Style> = {\r\n  light: \"https://basemaps.cartocdn.com/gl/positron-gl-style/style.json\",\r\n  dark: \"https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json\",\r\n};\r\n\r\nfunction getDocumentTheme(): Theme | null {\r\n  if (typeof document === \"undefined\") return null;\r\n  if (document.documentElement.classList.contains(\"dark\")) return \"dark\";\r\n  if (document.documentElement.classList.contains(\"light\")) return \"light\";\r\n  return null;\r\n}\r\n\r\nfunction getSystemTheme(): Theme {\r\n  if (typeof window === \"undefined\") return \"light\";\r\n  return window.matchMedia(\"(prefers-color-scheme: dark)\").matches\r\n    ? \"dark\"\r\n    : \"light\";\r\n}\r\n\r\nfunction useTheme(defaultTheme?: Theme): Theme {\r\n  const [theme, setTheme] = React.useState<Theme>(\r\n    () => getDocumentTheme() ?? getSystemTheme(),\r\n  );\r\n\r\n  React.useEffect(() => {\r\n    if (defaultTheme) return;\r\n\r\n    const observer = new MutationObserver(() => {\r\n      const docTheme = getDocumentTheme();\r\n      if (docTheme) setTheme(docTheme);\r\n    });\r\n\r\n    observer.observe(document.documentElement, {\r\n      attributes: true,\r\n      attributeFilter: [\"class\"],\r\n    });\r\n\r\n    const mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\");\r\n    const handleSystemChange = (e: MediaQueryListEvent) => {\r\n      if (!getDocumentTheme()) setTheme(e.matches ? \"dark\" : \"light\");\r\n    };\r\n    mediaQuery.addEventListener(\"change\", handleSystemChange);\r\n\r\n    return () => {\r\n      observer.disconnect();\r\n      mediaQuery.removeEventListener(\"change\", handleSystemChange);\r\n    };\r\n  }, [defaultTheme]);\r\n\r\n  return defaultTheme ?? theme;\r\n}\r\n\r\ntype MapContext = {\r\n  mapRef: React.RefObject<MapLibreMap | null>;\r\n  containerRef: React.RefObject<HTMLDivElement | null>;\r\n  loaded: boolean;\r\n  setLoaded: React.Dispatch<React.SetStateAction<boolean>>;\r\n  projection: Projection;\r\n  setProjection: React.Dispatch<React.SetStateAction<Projection>>;\r\n  center: Coordinate;\r\n  setCenter: React.Dispatch<React.SetStateAction<Coordinate>>;\r\n  zoom: number;\r\n  setZoom: React.Dispatch<React.SetStateAction<number>>;\r\n  bearing: number;\r\n  setBearing: React.Dispatch<React.SetStateAction<number>>;\r\n  pitch: number;\r\n  setPitch: React.Dispatch<React.SetStateAction<number>>;\r\n  fullscreen: boolean;\r\n  setFullscreen: React.Dispatch<React.SetStateAction<boolean>>;\r\n};\r\n\r\nconst DefaultMapContext: MapContext = {\r\n  mapRef: { current: null },\r\n  containerRef: { current: null },\r\n  loaded: false,\r\n  setLoaded: () => {},\r\n  projection: \"mercator\",\r\n  setProjection: () => {},\r\n  center: { latitude: 47.4979, longitude: 19.0404 },\r\n  setCenter: () => {},\r\n  zoom: 1,\r\n  setZoom: () => {},\r\n  bearing: 0,\r\n  setBearing: () => {},\r\n  pitch: 0,\r\n  setPitch: () => {},\r\n  fullscreen: false,\r\n  setFullscreen: () => {},\r\n};\r\n\r\nconst Context = React.createContext<MapContext>(DefaultMapContext);\r\n\r\ntype MapProviderProps = {\r\n  defaultCenter?: Coordinate;\r\n  defaultZoom?: number;\r\n  defaultBearing?: number;\r\n  defaultPitch?: number;\r\n  defaultProjection?: Projection;\r\n  children: React.ReactNode;\r\n};\r\n\r\nfunction MapProvider({\r\n  defaultCenter = { latitude: 47.4979, longitude: 19.0404 },\r\n  defaultZoom = 8,\r\n  defaultBearing = 0,\r\n  defaultPitch = 0,\r\n  defaultProjection = \"mercator\",\r\n  children,\r\n}: MapProviderProps) {\r\n  const mapRef = React.useRef<MapLibreMap>(null);\r\n  const containerRef = React.useRef<HTMLDivElement>(null);\r\n\r\n  const [loaded, setLoaded] = React.useState<boolean>(false);\r\n  const [zoom, setZoom] = React.useState<number>(defaultZoom);\r\n  const [bearing, setBearing] = React.useState<number>(defaultBearing);\r\n  const [pitch, setPitch] = React.useState<number>(defaultPitch);\r\n  const [fullscreen, setFullscreen] = React.useState<boolean>(false);\r\n  const [center, setCenter] = React.useState<Coordinate>(defaultCenter);\r\n  const [projection, setProjection] =\r\n    React.useState<Projection>(defaultProjection);\r\n\r\n  const values: MapContext = {\r\n    mapRef,\r\n    containerRef,\r\n    projection,\r\n    setProjection,\r\n    loaded,\r\n    setLoaded,\r\n    center,\r\n    setCenter,\r\n    zoom,\r\n    setZoom,\r\n    bearing,\r\n    setBearing,\r\n    pitch,\r\n    setPitch,\r\n    fullscreen,\r\n    setFullscreen,\r\n  };\r\n\r\n  return <Context.Provider value={values}>{children}</Context.Provider>;\r\n}\r\n\r\ntype MapProps = {\r\n  children?: React.ReactNode;\r\n  className?: string;\r\n  theme?: Theme;\r\n  styles?: Record<Theme, Style>;\r\n  onViewportChange?: (viewport: {\r\n    center: Coordinate;\r\n    zoom: number;\r\n    bearing: number;\r\n    pitch: number;\r\n  }) => void;\r\n} & Omit<MapOptions, \"container\" | \"style\">;\r\n\r\nfunction Map({\r\n  children,\r\n  className,\r\n  theme: themeProp,\r\n  styles,\r\n  onViewportChange,\r\n  ...props\r\n}: MapProps) {\r\n  const {\r\n    mapRef,\r\n    containerRef,\r\n    loaded,\r\n    setLoaded,\r\n    projection,\r\n    center: initialCenter,\r\n    setCenter,\r\n    zoom: initialZoom,\r\n    setZoom,\r\n    bearing: initialBearing,\r\n    setBearing,\r\n    pitch: initialPitch,\r\n    setPitch,\r\n  } = React.useContext(Context);\r\n\r\n  const resolvedTheme = useTheme(themeProp);\r\n  const [isStyleLoaded, setIsStyleLoaded] = React.useState(false);\r\n\r\n  const mapStyles = React.useMemo(\r\n    () => ({\r\n      dark: styles?.dark ?? defaultStyles.dark,\r\n      light: styles?.light ?? defaultStyles.light,\r\n    }),\r\n    [styles],\r\n  );\r\n\r\n  const currentStyleRef = React.useRef<Style | null>(null);\r\n  const styleTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(\r\n    null,\r\n  );\r\n  const onViewportChangeRef = React.useRef(onViewportChange);\r\n  const [initialMapConfig] = React.useState<{\r\n    style: Style;\r\n    center: Coordinate;\r\n    zoom: number;\r\n    bearing: number;\r\n    pitch: number;\r\n    projection: Projection;\r\n    options: Omit<MapOptions, \"container\" | \"style\">;\r\n  }>(() => {\r\n    const initialStyle =\r\n      resolvedTheme === \"dark\" ? mapStyles.dark : mapStyles.light;\r\n    return {\r\n      style: initialStyle,\r\n      center: initialCenter,\r\n      zoom: initialZoom,\r\n      bearing: initialBearing,\r\n      pitch: initialPitch,\r\n      projection,\r\n      options: props,\r\n    };\r\n  });\r\n\r\n  const clearStyleTimeout = React.useCallback(() => {\r\n    if (styleTimeoutRef.current) {\r\n      clearTimeout(styleTimeoutRef.current);\r\n      styleTimeoutRef.current = null;\r\n    }\r\n  }, []);\r\n\r\n  React.useEffect(() => {\r\n    onViewportChangeRef.current = onViewportChange;\r\n  }, [onViewportChange]);\r\n\r\n  React.useEffect(() => {\r\n    if (!containerRef.current) return;\r\n    currentStyleRef.current = initialMapConfig.style;\r\n\r\n    const map = new MapLibreMap({\r\n      container: containerRef.current,\r\n      style: initialMapConfig.style,\r\n      center: [\r\n        initialMapConfig.center.longitude,\r\n        initialMapConfig.center.latitude,\r\n      ],\r\n      zoom: initialMapConfig.zoom,\r\n      bearing: initialMapConfig.bearing,\r\n      pitch: initialMapConfig.pitch,\r\n      renderWorldCopies: true,\r\n      attributionControl: false,\r\n      ...initialMapConfig.options,\r\n    });\r\n\r\n    mapRef.current = map;\r\n\r\n    const handleStyleData = () => {\r\n      clearStyleTimeout();\r\n      styleTimeoutRef.current = setTimeout(() => {\r\n        setIsStyleLoaded(true);\r\n        if (initialMapConfig.projection)\r\n          map.setProjection({ type: initialMapConfig.projection });\r\n      }, 100);\r\n    };\r\n\r\n    const handleLoad = () => setLoaded(true);\r\n\r\n    const handleMove = () => {\r\n      const c = map.getCenter();\r\n      const viewport = {\r\n        center: { latitude: c.lat, longitude: c.lng } as Coordinate,\r\n        zoom: map.getZoom(),\r\n        bearing: map.getBearing(),\r\n        pitch: map.getPitch(),\r\n      };\r\n      setCenter(viewport.center);\r\n      setZoom(viewport.zoom);\r\n      setBearing(viewport.bearing);\r\n      setPitch(viewport.pitch);\r\n      onViewportChangeRef.current?.(viewport);\r\n    };\r\n\r\n    map.on(\"load\", handleLoad);\r\n    map.on(\"styledata\", handleStyleData);\r\n    map.on(\"move\", handleMove);\r\n\r\n    return () => {\r\n      clearStyleTimeout();\r\n      map.off(\"load\", handleLoad);\r\n      map.off(\"styledata\", handleStyleData);\r\n      map.off(\"move\", handleMove);\r\n      map.remove();\r\n      mapRef.current = null;\r\n      setLoaded(false);\r\n      setIsStyleLoaded(false);\r\n    };\r\n  }, [\r\n    containerRef,\r\n    mapRef,\r\n    setBearing,\r\n    setCenter,\r\n    setLoaded,\r\n    setPitch,\r\n    setZoom,\r\n    clearStyleTimeout,\r\n    initialMapConfig,\r\n  ]);\r\n\r\n  React.useEffect(() => {\r\n    const map = mapRef.current;\r\n    if (!map) return;\r\n\r\n    const newStyle =\r\n      resolvedTheme === \"dark\" ? mapStyles.dark : mapStyles.light;\r\n    if (currentStyleRef.current === newStyle) return;\r\n\r\n    clearStyleTimeout();\r\n    currentStyleRef.current = newStyle;\r\n    setIsStyleLoaded(false);\r\n    map.setStyle(newStyle, { diff: true });\r\n  }, [resolvedTheme, mapStyles, clearStyleTimeout, mapRef]);\r\n\r\n  const isReady = loaded && isStyleLoaded;\r\n\r\n  return (\r\n    <div\r\n      ref={containerRef}\r\n      className={cn(\"relative h-full w-full overflow-hidden\", className)}\r\n    >\r\n      {(!isReady || !loaded) && <Loading />}\r\n      {loaded && children}\r\n    </div>\r\n  );\r\n}\r\n\r\nfunction MapCopyright({ title }: { title?: string }) {\r\n  return (\r\n    <span className=\"absolute bottom-1 right-2 z-20 text-card-foreground/75 bg-card/50 rounded-full px-2 border border-border select-none\">\r\n      {title && `${title} | `}© CARTO, © OpenStreetMap contributors\r\n    </span>\r\n  );\r\n}\r\n\r\ntype MapControlsProps = {\r\n  align?: \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\";\r\n  className?: string;\r\n  children: React.ReactNode;\r\n};\r\n\r\nconst controlPositionClasses: Record<\r\n  NonNullable<MapControlsProps[\"align\"]>,\r\n  string\r\n> = {\r\n  \"top-left\": \"top-2 left-2\",\r\n  \"top-right\": \"top-2 right-2\",\r\n  \"bottom-left\": \"bottom-10 left-2\",\r\n  \"bottom-right\": \"bottom-10 right-2\",\r\n};\r\n\r\nfunction MapControls({\r\n  align = \"bottom-right\",\r\n  className,\r\n  children,\r\n}: MapControlsProps) {\r\n  return (\r\n    <div\r\n      className={cn(\r\n        \"absolute z-10 flex flex-col gap-2\",\r\n        controlPositionClasses[align],\r\n        className,\r\n      )}\r\n    >\r\n      {children}\r\n    </div>\r\n  );\r\n}\r\n\r\nfunction MapControlGroup({\r\n  children,\r\n  className,\r\n}: {\r\n  children: React.ReactNode;\r\n  className?: string;\r\n}) {\r\n  return (\r\n    <ButtonGroup\r\n      orientation={\"vertical\"}\r\n      className={cn(\"bg-card rounded-lg min-h-9 flex\", className)}\r\n    >\r\n      {children}\r\n    </ButtonGroup>\r\n  );\r\n}\r\n\r\nfunction MapControlZoomIn({ className }: { className?: string }) {\r\n  const { mapRef } = React.useContext(Context);\r\n\r\n  const handleZoomIn = () => {\r\n    mapRef.current?.zoomTo((mapRef.current.getZoom() ?? 0) + 1, {\r\n      duration: 300,\r\n    });\r\n  };\r\n\r\n  return (\r\n    <Button\r\n      onClick={handleZoomIn}\r\n      className={cn(\"min-h-8 grow max-w-9\", className)}\r\n      size=\"sm\"\r\n      variant={\"outline\"}\r\n    >\r\n      <Plus />\r\n    </Button>\r\n  );\r\n}\r\n\r\nfunction MapControlZoomOut({ className }: { className?: string }) {\r\n  const { mapRef } = React.useContext(Context);\r\n\r\n  const handleZoomOut = () => {\r\n    mapRef.current?.zoomTo((mapRef.current.getZoom() ?? 0) - 1, {\r\n      duration: 300,\r\n    });\r\n  };\r\n\r\n  return (\r\n    <Button\r\n      onClick={handleZoomOut}\r\n      className={cn(\"min-h-8 grow max-w-9\", className)}\r\n      size=\"sm\"\r\n      variant={\"outline\"}\r\n    >\r\n      <Minus />\r\n    </Button>\r\n  );\r\n}\r\n\r\nfunction MapControlPitchUp({ className }: { className?: string }) {\r\n  const { mapRef, pitch, setPitch } = React.useContext(Context);\r\n\r\n  const handlePitchUp = () => {\r\n    const currentPitch = mapRef.current?.getPitch() ?? pitch ?? 0;\r\n    const newPitch = Math.min(currentPitch + 15, 85);\r\n    mapRef.current?.flyTo({\r\n      pitch: newPitch,\r\n      duration: 500,\r\n    });\r\n    setPitch(newPitch);\r\n  };\r\n\r\n  return (\r\n    <Button\r\n      onClick={handlePitchUp}\r\n      className={cn(\"min-h-8 grow max-w-9\", className)}\r\n      size=\"sm\"\r\n      variant={\"outline\"}\r\n    >\r\n      <ChevronUp />\r\n    </Button>\r\n  );\r\n}\r\n\r\nfunction MapControlPitchDown({ className }: { className?: string }) {\r\n  const { mapRef, pitch, setPitch } = React.useContext(Context);\r\n\r\n  const handlePitchDown = () => {\r\n    const currentPitch = mapRef.current?.getPitch() ?? pitch ?? 0;\r\n    const newPitch = Math.max(currentPitch - 15, 0);\r\n    mapRef.current?.flyTo({\r\n      pitch: newPitch,\r\n      duration: 500,\r\n    });\r\n    setPitch(newPitch);\r\n  };\r\n\r\n  return (\r\n    <Button\r\n      onClick={handlePitchDown}\r\n      className={cn(\"min-h-8 grow max-w-9\", className)}\r\n      size=\"sm\"\r\n      variant={\"outline\"}\r\n    >\r\n      <ChevronDown />\r\n    </Button>\r\n  );\r\n}\r\n\r\nfunction MapControlRotate({ className }: { className?: string }) {\r\n  const { mapRef } = React.useContext(Context);\r\n  const compassRef = React.useRef<SVGSVGElement>(null);\r\n  const currentRef = React.useRef<number>(0);\r\n  const previousRef = React.useRef<number>(0);\r\n\r\n  React.useEffect(() => {\r\n    const map = mapRef.current;\r\n    if (!map || !compassRef.current) return;\r\n\r\n    const compass = compassRef.current;\r\n    const update = () => {\r\n      const b = map.getBearing();\r\n      const p = map.getPitch();\r\n\r\n      let delta = b - previousRef.current;\r\n      if (delta > 180) delta -= 360;\r\n      if (delta < -180) delta += 360;\r\n\r\n      currentRef.current += delta;\r\n      previousRef.current = b;\r\n\r\n      compass.style.transform = `rotateX(${p}deg) rotateZ(${-currentRef.current}deg)`;\r\n    };\r\n\r\n    map.on(\"rotate\", update);\r\n    map.on(\"pitch\", update);\r\n    update();\r\n\r\n    return () => {\r\n      map.off(\"rotate\", update);\r\n      map.off(\"pitch\", update);\r\n    };\r\n  }, [mapRef]);\r\n\r\n  const handleReset = () => {\r\n    mapRef.current?.resetNorthPitch({ duration: 300 });\r\n  };\r\n\r\n  return (\r\n    <Button\r\n      onClick={handleReset}\r\n      className={cn(\"min-h-8 grow max-w-9\", className)}\r\n      size=\"sm\"\r\n      variant={\"outline\"}\r\n    >\r\n      <svg\r\n        ref={compassRef}\r\n        viewBox=\"0 0 24 24\"\r\n        className=\"size-5 transition-transform duration-500\"\r\n        style={{ transformStyle: \"preserve-3d\" }}\r\n        aria-hidden\r\n      >\r\n        <path d=\"M12 2L16 12H12V2Z\" className=\"fill-primary\" />\r\n        <path d=\"M12 2L8 12H12V2Z\" className=\"fill-primary/60\" />\r\n        <path d=\"M12 22L16 12H12V22Z\" className=\"fill-muted-foreground/60\" />\r\n        <path d=\"M12 22L8 12H12V22Z\" className=\"fill-muted-foreground/30\" />\r\n      </svg>\r\n    </Button>\r\n  );\r\n}\r\n\r\nfunction MapControlFullscreen({ className }: { className?: string }) {\r\n  const { mapRef, fullscreen, setFullscreen } = React.useContext(Context);\r\n\r\n  const handleFullscreen = () => {\r\n    const container = mapRef.current?.getContainer();\r\n    if (!container) return;\r\n    if (document.fullscreenElement) {\r\n      document.exitFullscreen();\r\n      setFullscreen(false);\r\n    } else {\r\n      container.requestFullscreen();\r\n      setFullscreen(true);\r\n    }\r\n  };\r\n\r\n  React.useEffect(() => {\r\n    const handler = () => {\r\n      if (!document.fullscreenElement) setFullscreen(false);\r\n    };\r\n    document.addEventListener(\"fullscreenchange\", handler);\r\n    return () => document.removeEventListener(\"fullscreenchange\", handler);\r\n  }, [setFullscreen]);\r\n\r\n  return (\r\n    <Button\r\n      onClick={handleFullscreen}\r\n      className={cn(\"min-h-8 grow max-w-9\", className)}\r\n      size=\"sm\"\r\n      variant={\"outline\"}\r\n    >\r\n      {fullscreen ? <Minimize /> : <Maximize />}\r\n    </Button>\r\n  );\r\n}\r\n\r\nfunction MapControlProjection({ className }: { className?: string }) {\r\n  const { mapRef, projection, setProjection } = React.useContext(Context);\r\n\r\n  const handleProjection = () => {\r\n    const currentProjection: Projection =\r\n      (mapRef.current?.getProjection().type as Projection) ??\r\n      projection ??\r\n      \"mercator\";\r\n    const newProjection =\r\n      currentProjection === \"mercator\" ? \"globe\" : \"mercator\";\r\n    mapRef.current?.setProjection({\r\n      type: newProjection,\r\n    });\r\n    setProjection(newProjection);\r\n  };\r\n\r\n  return (\r\n    <Button\r\n      onClick={handleProjection}\r\n      className={cn(\"min-h-8 grow max-w-9\", className)}\r\n      size=\"sm\"\r\n      variant={\"outline\"}\r\n    >\r\n      {projection === \"mercator\" ? <MapIcon /> : <Globe />}\r\n    </Button>\r\n  );\r\n}\r\n\r\ntype MapControlLocateProps = {\r\n  className?: string;\r\n  onLocate?: (coordinates: Coordinate) => void;\r\n};\r\n\r\nfunction MapControlLocate({ className, onLocate }: MapControlLocateProps) {\r\n  const { mapRef } = React.useContext(Context);\r\n  const [loading, setLoading] = React.useState(false);\r\n\r\n  const handleLocate = () => {\r\n    if (!(\"geolocation\" in navigator)) return;\r\n    setLoading(true);\r\n    navigator.geolocation.getCurrentPosition(\r\n      (pos) => {\r\n        const coordinates = {\r\n          longitude: pos.coords.longitude,\r\n          latitude: pos.coords.latitude,\r\n        };\r\n        mapRef.current?.flyTo({\r\n          center: [coordinates.longitude, coordinates.latitude],\r\n          zoom: 14,\r\n          duration: 1500,\r\n        });\r\n        onLocate?.(coordinates);\r\n        setLoading(false);\r\n      },\r\n      (err) => {\r\n        console.error(\"Geolocation error:\", err);\r\n        setLoading(false);\r\n      },\r\n    );\r\n  };\r\n\r\n  return (\r\n    <Button\r\n      onClick={handleLocate}\r\n      disabled={loading}\r\n      className={cn(\"min-h-8 grow max-w-9\", className)}\r\n      size=\"sm\"\r\n      variant={\"outline\"}\r\n    >\r\n      {loading ? <Loader2 className=\"animate-spin\" /> : <Locate />}\r\n    </Button>\r\n  );\r\n}\r\n\r\nfunction MapMarkers({\r\n  children,\r\n  className,\r\n}: {\r\n  children: React.ReactNode;\r\n  className?: string;\r\n}) {\r\n  return <div className={cn(\"contents\", className)}>{children}</div>;\r\n}\r\n\r\ntype MarkerContextValue = { marker: Marker };\r\nconst MarkerContext = React.createContext<MarkerContextValue | null>(null);\r\n\r\ntype MapMarkerProps = {\r\n  coordinates: Coordinate;\r\n  children: React.ReactNode;\r\n  onClick?: (e: MouseEvent) => void;\r\n  onMouseEnter?: (e: MouseEvent) => void;\r\n  onMouseLeave?: (e: MouseEvent) => void;\r\n  onDragStart?: (lngLat: { lng: number; lat: number }) => void;\r\n  onDrag?: (lngLat: { lng: number; lat: number }) => void;\r\n  onDragEnd?: (lngLat: { lng: number; lat: number }) => void;\r\n} & Omit<MarkerOptions, \"element\">;\r\n\r\nfunction MapMarker({\r\n  coordinates,\r\n  children,\r\n  onClick,\r\n  onMouseEnter,\r\n  onMouseLeave,\r\n  onDragStart,\r\n  onDrag,\r\n  onDragEnd,\r\n  draggable = false,\r\n  ...markerOptions\r\n}: MapMarkerProps) {\r\n  const { mapRef, loaded } = React.useContext(Context);\r\n  const [initialMarkerConfig] = React.useState(() => ({\r\n    markerOptions,\r\n    draggable,\r\n    coordinates,\r\n  }));\r\n\r\n  const callbacksRef = React.useRef({\r\n    onClick,\r\n    onMouseEnter,\r\n    onMouseLeave,\r\n    onDragStart,\r\n    onDrag,\r\n    onDragEnd,\r\n  });\r\n  const [marker, setMarker] = React.useState<Marker | null>(null);\r\n\r\n  React.useEffect(() => {\r\n    callbacksRef.current = {\r\n      onClick,\r\n      onMouseEnter,\r\n      onMouseLeave,\r\n      onDragStart,\r\n      onDrag,\r\n      onDragEnd,\r\n    };\r\n  }, [onClick, onMouseEnter, onMouseLeave, onDragStart, onDrag, onDragEnd]);\r\n\r\n  React.useEffect(() => {\r\n    const el = document.createElement(\"div\");\r\n    const instance = new Marker({\r\n      ...initialMarkerConfig.markerOptions,\r\n      element: el,\r\n      draggable: initialMarkerConfig.draggable,\r\n    }).setLngLat([\r\n      initialMarkerConfig.coordinates.longitude,\r\n      initialMarkerConfig.coordinates.latitude,\r\n    ]);\r\n\r\n    const handleClick = (e: MouseEvent) => callbacksRef.current.onClick?.(e);\r\n    const handleMouseEnter = (e: MouseEvent) =>\r\n      callbacksRef.current.onMouseEnter?.(e);\r\n    const handleMouseLeave = (e: MouseEvent) =>\r\n      callbacksRef.current.onMouseLeave?.(e);\r\n    const handleDragStart = () => {\r\n      const ll = instance.getLngLat();\r\n      callbacksRef.current.onDragStart?.({ lng: ll.lng, lat: ll.lat });\r\n    };\r\n    const handleDrag = () => {\r\n      const ll = instance.getLngLat();\r\n      callbacksRef.current.onDrag?.({ lng: ll.lng, lat: ll.lat });\r\n    };\r\n    const handleDragEnd = () => {\r\n      const ll = instance.getLngLat();\r\n      callbacksRef.current.onDragEnd?.({ lng: ll.lng, lat: ll.lat });\r\n    };\r\n\r\n    el.addEventListener(\"click\", handleClick);\r\n    el.addEventListener(\"mouseenter\", handleMouseEnter);\r\n    el.addEventListener(\"mouseleave\", handleMouseLeave);\r\n    instance.on(\"dragstart\", handleDragStart);\r\n    instance.on(\"drag\", handleDrag);\r\n    instance.on(\"dragend\", handleDragEnd);\r\n\r\n    setMarker(instance);\r\n\r\n    return () => {\r\n      el.removeEventListener(\"click\", handleClick);\r\n      el.removeEventListener(\"mouseenter\", handleMouseEnter);\r\n      el.removeEventListener(\"mouseleave\", handleMouseLeave);\r\n      instance.off(\"dragstart\", handleDragStart);\r\n      instance.off(\"drag\", handleDrag);\r\n      instance.off(\"dragend\", handleDragEnd);\r\n      instance.remove();\r\n      setMarker(null);\r\n    };\r\n  }, [initialMarkerConfig]);\r\n\r\n  React.useEffect(() => {\r\n    const map = mapRef.current;\r\n    if (!map || !loaded || !marker) return;\r\n    marker.addTo(map);\r\n    return () => {\r\n      marker.remove();\r\n    };\r\n  }, [mapRef, loaded, marker]);\r\n\r\n  React.useEffect(() => {\r\n    if (!marker) return;\r\n\r\n    const lngLat = marker.getLngLat();\r\n    if (\r\n      lngLat.lng !== coordinates.longitude ||\r\n      lngLat.lat !== coordinates.latitude\r\n    ) {\r\n      marker.setLngLat([coordinates.longitude, coordinates.latitude]);\r\n    }\r\n\r\n    if (marker.isDraggable() !== draggable) marker.setDraggable(draggable);\r\n\r\n    const currentOffset = marker.getOffset();\r\n    const rawOffset = markerOptions.offset ?? [0, 0];\r\n    const [ox, oy] = Array.isArray(rawOffset)\r\n      ? rawOffset\r\n      : [rawOffset.x, rawOffset.y];\r\n    if (currentOffset.x !== ox || currentOffset.y !== oy)\r\n      marker.setOffset(rawOffset);\r\n\r\n    if (marker.getRotation() !== (markerOptions.rotation ?? 0))\r\n      marker.setRotation(markerOptions.rotation ?? 0);\r\n  }, [\r\n    marker,\r\n    coordinates.longitude,\r\n    coordinates.latitude,\r\n    draggable,\r\n    markerOptions.offset,\r\n    markerOptions.rotation,\r\n  ]);\r\n\r\n  if (!marker) return null;\r\n\r\n  return (\r\n    <MarkerContext.Provider value={{ marker }}>\r\n      {children}\r\n    </MarkerContext.Provider>\r\n  );\r\n}\r\n\r\ntype MarkerIconProps = {\r\n  children?: React.ReactNode;\r\n  className?: string;\r\n};\r\n\r\nfunction MapMarkerIcon({ children, className }: MarkerIconProps) {\r\n  const context = React.useContext(MarkerContext);\r\n  if (!context?.marker)\r\n    throw new Error(\"MapMarkerIcon must be used in a MapMarker Component\");\r\n  return createPortal(\r\n    <div className={cn(\"relative cursor-pointer\", className)}>\r\n      {children ?? (\r\n        <div className=\"flex items-center justify-center relative size-6 rounded-full bg-primary\">\r\n          <MapPin className=\"size-5 text-primary-foreground stroke-2.5\" />\r\n        </div>\r\n      )}\r\n    </div>,\r\n    context.marker.getElement(),\r\n  );\r\n}\r\n\r\ntype MarkerPopupProps = {\r\n  children: React.ReactNode;\r\n  className?: string;\r\n  closeButton?: boolean;\r\n} & Omit<PopupOptions, \"className\" | \"closeButton\">;\r\n\r\nfunction MapMarkerPopup({ children, ...popupOptions }: MarkerPopupProps) {\r\n  const context = React.useContext(MarkerContext);\r\n  if (!context?.marker)\r\n    throw new Error(\"MapMarkerPopup must be used in a MapMarker Component\");\r\n  const { mapRef, loaded } = React.useContext(Context);\r\n  const [container] = React.useState(() => document.createElement(\"div\"));\r\n  const [popup] = React.useState(() =>\r\n    new Popup({\r\n      offset: 16,\r\n      ...popupOptions,\r\n      closeButton: false,\r\n    })\r\n      .setMaxWidth(\"none\")\r\n      .setDOMContent(container),\r\n  );\r\n\r\n  React.useEffect(() => {\r\n    if (!mapRef.current || !loaded) return;\r\n    const content = container.parentElement;\r\n    if (content && content.classList.contains(\"maplibregl-popup-content\")) {\r\n      content.style.setProperty(\"background\", \"transparent\");\r\n      content.style.setProperty(\"padding\", \"0\");\r\n      content.style.setProperty(\"box-shadow\", \"none\");\r\n\r\n      const tip = content.parentElement?.querySelector(\r\n        \".maplibregl-popup-tip\",\r\n      ) as HTMLElement;\r\n      if (tip) tip.style.display = \"none\";\r\n    }\r\n\r\n    popup.setDOMContent(container);\r\n    context.marker.setPopup(popup);\r\n\r\n    return () => {\r\n      context.marker.setPopup(null);\r\n    };\r\n  }, [container, context.marker, loaded, mapRef, popup]);\r\n\r\n  React.useEffect(() => {\r\n    popup.setOffset(popupOptions.offset ?? 16);\r\n    if (popupOptions.maxWidth) {\r\n      popup.setMaxWidth(popupOptions.maxWidth);\r\n    }\r\n  }, [container, popup, popupOptions.offset, popupOptions.maxWidth]);\r\n\r\n  return createPortal(children, container);\r\n}\r\n\r\nexport {\r\n  Map,\r\n  MapControlFullscreen,\r\n  MapControlGroup,\r\n  MapControlLocate,\r\n  MapControlPitchDown,\r\n  MapControlPitchUp,\r\n  MapControlProjection,\r\n  MapControlRotate,\r\n  MapControls,\r\n  MapControlZoomIn,\r\n  MapControlZoomOut,\r\n  MapCopyright,\r\n  MapMarker,\r\n  MapMarkerIcon,\r\n  MapMarkerPopup,\r\n  MapMarkers,\r\n  MapProvider,\r\n  type Coordinate,\r\n  type Projection,\r\n  type Style,\r\n};\r\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    ".maplibregl-popup-content": {
      "background": "none !important",
      "padding-bottom": "4px !important",
      "box-shadow": "none !important",
      "border-radius": "0 !important"
    },
    ".maplibregl-popup-tip": {
      "display": "none !important"
    }
  },
  "type": "registry:block"
}