From d7de535e25236a80c1ffacaef8dcbb4eeb6f9e66 Mon Sep 17 00:00:00 2001 From: Anmoldeep Singh Date: Thu, 1 Jan 2026 23:05:00 +0530 Subject: [PATCH] Updated registry --- public/maps/map.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/maps/map.json b/public/maps/map.json index 84b8e35..b871a3b 100644 --- a/public/maps/map.json +++ b/public/maps/map.json @@ -12,7 +12,7 @@ "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 { useTheme } from \"next-themes\";\nimport {\n createContext,\n useContext,\n useEffect,\n useId,\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\ntype MapContextValue = {\n map: MapLibreGL.Map | null;\n isLoaded: boolean;\n};\n\nconst MapContext = createContext(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\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\ntype MapStyleOption = string | MapLibreGL.StyleSpecification;\n\ntype MapProps = {\n children?: ReactNode;\n /** Custom map styles for light and dark themes. Overrides the default Carto styles. */\n styles?: {\n light?: MapStyleOption;\n dark?: MapStyleOption;\n };\n} & Omit;\n\nconst DefaultLoader = () => (\n
\n
\n \n \n \n
\n
\n);\n\nfunction Map({ children, styles, ...props }: MapProps) {\n const containerRef = useRef(null);\n const mapRef = useRef(null);\n const [isMounted, setIsMounted] = useState(false);\n const [isLoaded, setIsLoaded] = useState(false);\n const [isStyleLoaded, setIsStyleLoaded] = useState(false);\n const { resolvedTheme } = useTheme();\n\n const mapStyles = useMemo(\n () => ({\n dark: styles?.dark ?? defaultStyles.dark,\n light: styles?.light ?? defaultStyles.light,\n }),\n [styles]\n );\n\n useEffect(() => {\n setIsMounted(true);\n }, []);\n\n useEffect(() => {\n if (!isMounted || !containerRef.current) return;\n\n const mapStyle =\n resolvedTheme === \"dark\" ? mapStyles.dark : mapStyles.light;\n\n const mapInstance = new MapLibreGL.Map({\n container: containerRef.current,\n style: mapStyle,\n attributionControl: false,\n ...props,\n });\n\n const styleDataHandler = () => setIsStyleLoaded(true);\n const loadHandler = () => setIsLoaded(true);\n\n mapInstance.on(\"load\", loadHandler);\n mapInstance.on(\"styledata\", styleDataHandler);\n mapRef.current = mapInstance;\n\n return () => {\n mapInstance.off(\"load\", loadHandler);\n mapInstance.off(\"styledata\", styleDataHandler);\n mapInstance.remove();\n mapRef.current = null;\n };\n }, [isMounted]);\n\n useEffect(() => {\n if (mapRef.current) {\n setIsStyleLoaded(false);\n mapRef.current.setStyle(\n resolvedTheme === \"dark\" ? mapStyles.dark : mapStyles.light,\n { diff: true }\n );\n }\n }, [resolvedTheme]);\n\n const isLoading = !isMounted || !isLoaded || !isStyleLoaded;\n\n return (\n \n
\n {isLoading && }\n {/* guard against hydration error */}\n {isMounted && children}\n
\n \n );\n}\n\ntype MarkerContextValue = {\n markerRef: React.RefObject;\n markerElementRef: React.RefObject;\n map: MapLibreGL.Map | null;\n isReady: boolean;\n};\n\nconst MarkerContext = createContext(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: number;\n latitude: number;\n children: ReactNode;\n onClick?: (e: MouseEvent) => void;\n onMouseEnter?: (e: MouseEvent) => void;\n onMouseLeave?: (e: MouseEvent) => void;\n onDragStart?: (lngLat: { lng: number; lat: number }) => void;\n onDrag?: (lngLat: { lng: number; lat: number }) => void;\n onDragEnd?: (lngLat: { lng: number; lat: number }) => void;\n} & Omit;\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, isLoaded } = useMap();\n const markerRef = useRef(null);\n const markerElementRef = useRef(null);\n const [isReady, setIsReady] = useState(false);\n\n useEffect(() => {\n if (!isLoaded || !map) return;\n\n const container = document.createElement(\"div\");\n markerElementRef.current = container;\n\n const marker = new MapLibreGL.Marker({\n ...markerOptions,\n element: container,\n draggable,\n })\n .setLngLat([longitude, latitude])\n .addTo(map);\n\n markerRef.current = marker;\n\n if (onClick) container.addEventListener(\"click\", onClick);\n if (onMouseEnter) container.addEventListener(\"mouseenter\", onMouseEnter);\n if (onMouseLeave) container.addEventListener(\"mouseleave\", onMouseLeave);\n\n const handleDragStart = () => {\n const lngLat = marker.getLngLat();\n onDragStart?.({ lng: lngLat.lng, lat: lngLat.lat });\n };\n const handleDrag = () => {\n const lngLat = marker.getLngLat();\n onDrag?.({ lng: lngLat.lng, lat: lngLat.lat });\n };\n const handleDragEnd = () => {\n const lngLat = marker.getLngLat();\n onDragEnd?.({ lng: lngLat.lng, lat: lngLat.lat });\n };\n\n if (draggable) {\n marker.on(\"dragstart\", handleDragStart);\n marker.on(\"drag\", handleDrag);\n marker.on(\"dragend\", handleDragEnd);\n }\n\n setIsReady(true);\n\n return () => {\n if (onClick) container.removeEventListener(\"click\", onClick);\n if (onMouseEnter)\n container.removeEventListener(\"mouseenter\", onMouseEnter);\n if (onMouseLeave)\n container.removeEventListener(\"mouseleave\", onMouseLeave);\n if (draggable) {\n marker.off(\"dragstart\", handleDragStart);\n marker.off(\"drag\", handleDrag);\n marker.off(\"dragend\", handleDragEnd);\n }\n marker.remove();\n markerRef.current = null;\n markerElementRef.current = null;\n setIsReady(false);\n };\n }, [isLoaded]);\n\n useEffect(() => {\n markerRef.current?.setLngLat([longitude, latitude]);\n }, [longitude, latitude]);\n\n useEffect(() => {\n markerRef.current?.setDraggable(draggable);\n }, [draggable]);\n\n return (\n \n {children}\n \n );\n}\n\ntype MarkerContentProps = {\n children?: ReactNode;\n className?: string;\n};\n\nfunction MarkerContent({ children, className }: MarkerContentProps) {\n const { markerElementRef, isReady } = useMarkerContext();\n\n if (!isReady || !markerElementRef.current) return null;\n\n return createPortal(\n
\n {children || }\n
,\n markerElementRef.current\n );\n}\n\nfunction DefaultMarkerIcon() {\n return (\n
\n );\n}\n\ntype MarkerPopupProps = {\n children: ReactNode;\n className?: string;\n closeButton?: boolean;\n} & Omit;\n\nfunction MarkerPopup({\n children,\n className,\n closeButton = false,\n ...popupOptions\n}: MarkerPopupProps) {\n const { markerRef, isReady } = useMarkerContext();\n const containerRef = useRef(null);\n const popupRef = useRef(null);\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n if (!isReady || !markerRef.current) return;\n\n const container = document.createElement(\"div\");\n containerRef.current = container;\n\n const popup = new MapLibreGL.Popup({\n offset: 16,\n ...popupOptions,\n closeButton: false,\n })\n .setMaxWidth(\"none\")\n .setDOMContent(container);\n\n popupRef.current = popup;\n markerRef.current.setPopup(popup);\n setMounted(true);\n\n return () => {\n popup.remove();\n popupRef.current = null;\n containerRef.current = null;\n setMounted(false);\n };\n }, [isReady]);\n\n const handleClose = () => popupRef.current?.remove();\n\n if (!mounted || !containerRef.current) return null;\n\n return createPortal(\n \n {closeButton && (\n \n \n Close\n \n )}\n {children}\n
,\n containerRef.current\n );\n}\n\ntype MarkerTooltipProps = {\n children: ReactNode;\n className?: string;\n} & Omit;\n\nfunction MarkerTooltip({\n children,\n className,\n ...popupOptions\n}: MarkerTooltipProps) {\n const { markerRef, markerElementRef, map, isReady } = useMarkerContext();\n const containerRef = useRef(null);\n const popupRef = useRef(null);\n const [mounted, setMounted] = useState(false);\n\n useEffect(() => {\n if (!isReady || !markerRef.current || !markerElementRef.current || !map)\n return;\n\n const container = document.createElement(\"div\");\n containerRef.current = container;\n\n const popup = new MapLibreGL.Popup({\n offset: 16,\n ...popupOptions,\n closeOnClick: true,\n closeButton: false,\n })\n .setMaxWidth(\"none\")\n .setDOMContent(container);\n\n popupRef.current = popup;\n\n const markerElement = markerElementRef.current;\n const marker = markerRef.current;\n\n const handleMouseEnter = () => {\n popup.setLngLat(marker.getLngLat()).addTo(map);\n };\n const handleMouseLeave = () => popup.remove();\n\n markerElement.addEventListener(\"mouseenter\", handleMouseEnter);\n markerElement.addEventListener(\"mouseleave\", handleMouseLeave);\n setMounted(true);\n\n return () => {\n markerElement.removeEventListener(\"mouseenter\", handleMouseEnter);\n markerElement.removeEventListener(\"mouseleave\", handleMouseLeave);\n popup.remove();\n popupRef.current = null;\n containerRef.current = null;\n setMounted(false);\n };\n }, [isReady, map]);\n\n if (!mounted || !containerRef.current) return null;\n\n return createPortal(\n \n {children}\n ,\n containerRef.current\n );\n}\n\ntype MarkerLabelProps = {\n children: ReactNode;\n className?: string;\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 \n {children}\n \n );\n}\n\ntype MapControlsProps = {\n position?: \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\";\n showZoom?: boolean;\n showCompass?: boolean;\n showLocate?: boolean;\n showFullscreen?: boolean;\n className?: string;\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-2 right-2\",\n};\n\nfunction ControlGroup({ children }: { children: React.ReactNode }) {\n return (\n
button:not(:last-child)]:border-b [&>button:not(:last-child)]:border-border\">\n {children}\n
\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 \n {children}\n \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, isLoaded } = useMap();\n const [waitingForLocation, setWaitingForLocation] = useState(false);\n\n if (!isLoaded) return null;\n\n const handleZoomIn = () => map?.zoomTo(map.getZoom() + 1, { duration: 300 });\n const handleZoomOut = () => map?.zoomTo(map.getZoom() - 1, { duration: 300 });\n const handleResetBearing = () => map?.resetNorthPitch({ duration: 300 });\n\n const handleLocate = () => {\n setWaitingForLocation(true);\n if (\"geolocation\" in navigator) {\n navigator.geolocation.getCurrentPosition(\n (position) => {\n const coords = {\n longitude: position.coords.longitude,\n latitude: position.coords.latitude,\n };\n map?.flyTo({\n center: [coords.longitude, coords.latitude],\n zoom: 14,\n duration: 1500,\n });\n onLocate?.({\n longitude: coords.longitude,\n latitude: coords.latitude,\n });\n setWaitingForLocation(false);\n },\n (error) => {\n console.error(\"Error getting location:\", error);\n setWaitingForLocation(false);\n }\n );\n }\n };\n\n const handleFullscreen = () => {\n const container = map?.getContainer();\n if (!container) return;\n if (document.fullscreenElement) {\n document.exitFullscreen();\n } else {\n container.requestFullscreen();\n }\n };\n\n return (\n \n {showZoom && (\n \n \n \n \n \n \n \n \n )}\n {showCompass && (\n \n \n \n )}\n {showLocate && (\n \n \n {waitingForLocation ? (\n \n ) : (\n \n )}\n \n \n )}\n {showFullscreen && (\n \n \n \n \n \n )}\n \n );\n}\n\nfunction CompassButton({ onClick }: { onClick?: () => void }) {\n const { isLoaded, map } = useMap();\n const compassRef = useRef(null);\n\n useEffect(() => {\n if (!isLoaded || !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 }, [isLoaded, map]);\n\n return (\n \n \n \n \n \n \n \n \n );\n}\n\ntype MapPopupProps = {\n longitude: number;\n latitude: number;\n onClose?: () => void;\n children: ReactNode;\n className?: string;\n closeButton?: boolean;\n} & Omit;\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 popupRef = useRef(null);\n\n const container = useMemo(() => document.createElement(\"div\"), []);\n\n useEffect(() => {\n if (!map) return;\n\n const popup = new MapLibreGL.Popup({\n offset: 12,\n ...popupOptions,\n closeButton: false,\n })\n .setMaxWidth(\"none\")\n .setDOMContent(container)\n .setLngLat([longitude, latitude])\n .addTo(map);\n\n const onCloseProp = () => onClose?.();\n\n popup.on(\"close\", onCloseProp);\n\n popupRef.current = popup;\n\n return () => {\n popup.off(\"close\", onCloseProp);\n if (popup.isOpen()) {\n popup.remove();\n }\n popupRef.current = null;\n };\n }, [map]);\n\n useEffect(() => {\n popupRef.current?.setLngLat([longitude, latitude]);\n }, [longitude, latitude]);\n\n const handleClose = () => {\n popupRef.current?.remove();\n onClose?.();\n };\n\n return createPortal(\n \n {closeButton && (\n \n \n Close\n \n )}\n {children}\n ,\n container\n );\n}\n\ntype MapRouteProps = {\n coordinates: [number, number][];\n color?: string;\n width?: number;\n opacity?: number;\n dashArray?: [number, number];\n};\n\nfunction MapRoute({\n coordinates,\n color = \"#4285F4\",\n width = 3,\n opacity = 0.8,\n dashArray,\n}: MapRouteProps) {\n const { map, isLoaded } = useMap();\n const id = useId();\n const sourceId = `route-source-${id}`;\n const layerId = `route-layer-${id}`;\n\n useEffect(() => {\n if (!isLoaded || !map || coordinates.length < 2) return;\n\n const addRoute = () => {\n if (map.getLayer(layerId)) map.removeLayer(layerId);\n if (map.getSource(sourceId)) map.removeSource(sourceId);\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\n addRoute();\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 }, [\n isLoaded,\n map,\n coordinates,\n color,\n width,\n opacity,\n dashArray,\n sourceId,\n layerId,\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};\n", + "content": "\"use client\";\n\nimport MapLibreGL, { type PopupOptions, type MarkerOptions } from \"maplibre-gl\";\nimport \"maplibre-gl/dist/maplibre-gl.css\";\nimport { useTheme } from \"next-themes\";\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useId,\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\ntype MapContextValue = {\n map: MapLibreGL.Map | null;\n isLoaded: boolean;\n};\n\nconst MapContext = createContext(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\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\ntype MapStyleOption = string | MapLibreGL.StyleSpecification;\n\ntype MapProps = {\n children?: ReactNode;\n /** Custom map styles for light and dark themes. Overrides the default Carto styles. */\n styles?: {\n light?: MapStyleOption;\n dark?: MapStyleOption;\n };\n} & Omit;\n\nconst DefaultLoader = () => (\n
\n
\n \n \n \n
\n
\n);\n\nfunction Map({ children, styles, ...props }: MapProps) {\n const containerRef = useRef(null);\n const mapRef = useRef(null);\n const [isMounted, setIsMounted] = useState(false);\n const [isLoaded, setIsLoaded] = useState(false);\n const [isStyleLoaded, setIsStyleLoaded] = useState(false);\n const { resolvedTheme } = useTheme();\n\n const mapStyles = useMemo(\n () => ({\n dark: styles?.dark ?? defaultStyles.dark,\n light: styles?.light ?? defaultStyles.light,\n }),\n [styles]\n );\n\n useEffect(() => {\n setIsMounted(true);\n }, []);\n\n useEffect(() => {\n if (!isMounted || !containerRef.current) return;\n\n const mapStyle =\n resolvedTheme === \"dark\" ? mapStyles.dark : mapStyles.light;\n\n const mapInstance = new MapLibreGL.Map({\n container: containerRef.current,\n style: mapStyle,\n attributionControl: false,\n ...props,\n });\n\n const styleDataHandler = () => setIsStyleLoaded(true);\n const loadHandler = () => setIsLoaded(true);\n\n mapInstance.on(\"load\", loadHandler);\n mapInstance.on(\"styledata\", styleDataHandler);\n mapRef.current = mapInstance;\n\n return () => {\n mapInstance.off(\"load\", loadHandler);\n mapInstance.off(\"styledata\", styleDataHandler);\n mapInstance.remove();\n mapRef.current = null;\n };\n }, [isMounted]);\n\n useEffect(() => {\n if (mapRef.current) {\n setIsStyleLoaded(false);\n mapRef.current.setStyle(\n resolvedTheme === \"dark\" ? mapStyles.dark : mapStyles.light,\n { diff: true }\n );\n }\n }, [resolvedTheme]);\n\n const isLoading = !isMounted || !isLoaded || !isStyleLoaded;\n\n return (\n \n
\n {isLoading && }\n {/* guard against hydration error */}\n {isMounted && children}\n
\n \n );\n}\n\ntype MarkerContextValue = {\n markerRef: React.RefObject;\n markerElementRef: React.RefObject;\n map: MapLibreGL.Map | null;\n isReady: boolean;\n};\n\nconst MarkerContext = createContext(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: number;\n latitude: number;\n children: ReactNode;\n onClick?: (e: MouseEvent) => void;\n onMouseEnter?: (e: MouseEvent) => void;\n onMouseLeave?: (e: MouseEvent) => void;\n onDragStart?: (lngLat: { lng: number; lat: number }) => void;\n onDrag?: (lngLat: { lng: number; lat: number }) => void;\n onDragEnd?: (lngLat: { lng: number; lat: number }) => void;\n} & Omit;\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, isLoaded } = useMap();\n const markerRef = useRef(null);\n const markerElementRef = useRef(null);\n const [isReady, setIsReady] = useState(false);\n const markerOptionsRef = useRef(markerOptions);\n\n useEffect(() => {\n if (!isLoaded || !map) return;\n\n const container = document.createElement(\"div\");\n markerElementRef.current = container;\n\n const marker = new MapLibreGL.Marker({\n ...markerOptions,\n element: container,\n draggable,\n })\n .setLngLat([longitude, latitude])\n .addTo(map);\n\n markerRef.current = marker;\n\n const handleClick = (e: MouseEvent) => onClick?.(e);\n const handleMouseEnter = (e: MouseEvent) => onMouseEnter?.(e);\n const handleMouseLeave = (e: MouseEvent) => onMouseLeave?.(e);\n\n container.addEventListener(\"click\", handleClick);\n container.addEventListener(\"mouseenter\", handleMouseEnter);\n container.addEventListener(\"mouseleave\", handleMouseLeave);\n\n const handleDragStart = () => {\n const lngLat = marker.getLngLat();\n onDragStart?.({ lng: lngLat.lng, lat: lngLat.lat });\n };\n const handleDrag = () => {\n const lngLat = marker.getLngLat();\n onDrag?.({ lng: lngLat.lng, lat: lngLat.lat });\n };\n const handleDragEnd = () => {\n const lngLat = marker.getLngLat();\n onDragEnd?.({ lng: lngLat.lng, lat: lngLat.lat });\n };\n\n marker.on(\"dragstart\", handleDragStart);\n marker.on(\"drag\", handleDrag);\n marker.on(\"dragend\", handleDragEnd);\n\n setIsReady(true);\n\n return () => {\n container.removeEventListener(\"click\", handleClick);\n container.removeEventListener(\"mouseenter\", handleMouseEnter);\n container.removeEventListener(\"mouseleave\", handleMouseLeave);\n\n marker.off(\"dragstart\", handleDragStart);\n marker.off(\"drag\", handleDrag);\n marker.off(\"dragend\", handleDragEnd);\n\n marker.remove();\n markerRef.current = null;\n markerElementRef.current = null;\n setIsReady(false);\n };\n }, [map, isLoaded]);\n\n useEffect(() => {\n markerRef.current?.setLngLat([longitude, latitude]);\n }, [longitude, latitude]);\n\n useEffect(() => {\n markerRef.current?.setDraggable(draggable);\n }, [draggable]);\n\n useEffect(() => {\n if (!markerRef.current) return;\n const prev = markerOptionsRef.current;\n\n if (prev.offset !== markerOptions.offset) {\n markerRef.current.setOffset(markerOptions.offset ?? [0, 0]);\n }\n if (prev.rotation !== markerOptions.rotation) {\n markerRef.current.setRotation(markerOptions.rotation ?? 0);\n }\n if (prev.rotationAlignment !== markerOptions.rotationAlignment) {\n markerRef.current.setRotationAlignment(\n markerOptions.rotationAlignment ?? \"auto\"\n );\n }\n if (prev.pitchAlignment !== markerOptions.pitchAlignment) {\n markerRef.current.setPitchAlignment(\n markerOptions.pitchAlignment ?? \"auto\"\n );\n }\n\n markerOptionsRef.current = markerOptions;\n }, [markerOptions]);\n\n return (\n \n {children}\n \n );\n}\n\ntype MarkerContentProps = {\n children?: ReactNode;\n className?: string;\n};\n\nfunction MarkerContent({ children, className }: MarkerContentProps) {\n const { markerElementRef, isReady } = useMarkerContext();\n\n if (!isReady || !markerElementRef.current) return null;\n\n return createPortal(\n
\n {children || }\n
,\n markerElementRef.current\n );\n}\n\nfunction DefaultMarkerIcon() {\n return (\n
\n );\n}\n\ntype MarkerPopupProps = {\n children: ReactNode;\n className?: string;\n closeButton?: boolean;\n} & Omit;\n\nfunction MarkerPopup({\n children,\n className,\n closeButton = false,\n ...popupOptions\n}: MarkerPopupProps) {\n const { markerRef, isReady } = useMarkerContext();\n const containerRef = useRef(null);\n const popupRef = useRef(null);\n const [mounted, setMounted] = useState(false);\n const popupOptionsRef = useRef(popupOptions);\n\n useEffect(() => {\n if (!isReady || !markerRef.current) return;\n\n const container = document.createElement(\"div\");\n containerRef.current = container;\n\n const popup = new MapLibreGL.Popup({\n offset: 16,\n ...popupOptions,\n closeButton: false,\n })\n .setMaxWidth(\"none\")\n .setDOMContent(container);\n\n popupRef.current = popup;\n markerRef.current.setPopup(popup);\n setMounted(true);\n\n return () => {\n popup.remove();\n popupRef.current = null;\n containerRef.current = null;\n setMounted(false);\n };\n }, [isReady]);\n\n useEffect(() => {\n if (!popupRef.current) return;\n const prev = popupOptionsRef.current;\n\n if (prev.offset !== popupOptions.offset) {\n popupRef.current.setOffset(popupOptions.offset ?? 16);\n }\n if (prev.maxWidth !== popupOptions.maxWidth && popupOptions.maxWidth) {\n popupRef.current.setMaxWidth(popupOptions.maxWidth ?? \"none\");\n }\n\n popupOptionsRef.current = popupOptions;\n }, [popupOptions]);\n\n const handleClose = () => popupRef.current?.remove();\n\n if (!mounted || !containerRef.current) return null;\n\n return createPortal(\n \n {closeButton && (\n \n \n Close\n \n )}\n {children}\n
,\n containerRef.current\n );\n}\n\ntype MarkerTooltipProps = {\n children: ReactNode;\n className?: string;\n} & Omit;\n\nfunction MarkerTooltip({\n children,\n className,\n ...popupOptions\n}: MarkerTooltipProps) {\n const { markerRef, markerElementRef, map, isReady } = useMarkerContext();\n const containerRef = useRef(null);\n const popupRef = useRef(null);\n const [mounted, setMounted] = useState(false);\n const popupOptionsRef = useRef(popupOptions);\n\n useEffect(() => {\n if (!isReady || !markerRef.current || !markerElementRef.current || !map)\n return;\n\n const container = document.createElement(\"div\");\n containerRef.current = container;\n\n const popup = new MapLibreGL.Popup({\n offset: 16,\n ...popupOptions,\n closeOnClick: true,\n closeButton: false,\n })\n .setMaxWidth(\"none\")\n .setDOMContent(container);\n\n popupRef.current = popup;\n\n const markerElement = markerElementRef.current;\n const marker = markerRef.current;\n\n const handleMouseEnter = () => {\n popup.setLngLat(marker.getLngLat()).addTo(map);\n };\n const handleMouseLeave = () => popup.remove();\n\n markerElement.addEventListener(\"mouseenter\", handleMouseEnter);\n markerElement.addEventListener(\"mouseleave\", handleMouseLeave);\n setMounted(true);\n\n return () => {\n markerElement.removeEventListener(\"mouseenter\", handleMouseEnter);\n markerElement.removeEventListener(\"mouseleave\", handleMouseLeave);\n popup.remove();\n popupRef.current = null;\n containerRef.current = null;\n setMounted(false);\n };\n }, [isReady, map]);\n\n useEffect(() => {\n if (!popupRef.current) return;\n const prev = popupOptionsRef.current;\n\n if (prev.offset !== popupOptions.offset) {\n popupRef.current.setOffset(popupOptions.offset ?? 16);\n }\n if (prev.maxWidth !== popupOptions.maxWidth && popupOptions.maxWidth) {\n popupRef.current.setMaxWidth(popupOptions.maxWidth ?? \"none\");\n }\n\n popupOptionsRef.current = popupOptions;\n }, [popupOptions]);\n\n if (!mounted || !containerRef.current) return null;\n\n return createPortal(\n \n {children}\n ,\n containerRef.current\n );\n}\n\ntype MarkerLabelProps = {\n children: ReactNode;\n className?: string;\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 \n {children}\n \n );\n}\n\ntype MapControlsProps = {\n position?: \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\";\n showZoom?: boolean;\n showCompass?: boolean;\n showLocate?: boolean;\n showFullscreen?: boolean;\n className?: string;\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-2 right-2\",\n};\n\nfunction ControlGroup({ children }: { children: React.ReactNode }) {\n return (\n
button:not(:last-child)]:border-b [&>button:not(:last-child)]:border-border\">\n {children}\n
\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 \n {children}\n \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, isLoaded } = 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 if (!isLoaded) return null;\n\n return (\n \n {showZoom && (\n \n \n \n \n \n \n \n \n )}\n {showCompass && (\n \n \n \n )}\n {showLocate && (\n \n \n {waitingForLocation ? (\n \n ) : (\n \n )}\n \n \n )}\n {showFullscreen && (\n \n \n \n \n \n )}\n \n );\n}\n\nfunction CompassButton({ onClick }: { onClick: () => void }) {\n const { isLoaded, map } = useMap();\n const compassRef = useRef(null);\n\n useEffect(() => {\n if (!isLoaded || !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 }, [isLoaded, map]);\n\n return (\n \n \n \n \n \n \n \n \n );\n}\n\ntype MapPopupProps = {\n longitude: number;\n latitude: number;\n onClose?: () => void;\n children: ReactNode;\n className?: string;\n closeButton?: boolean;\n} & Omit;\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 popupRef = useRef(null);\n const popupOptionsRef = useRef(popupOptions);\n\n const container = useMemo(() => document.createElement(\"div\"), []);\n\n useEffect(() => {\n if (!map) return;\n\n const popup = new MapLibreGL.Popup({\n offset: 16,\n ...popupOptions,\n closeButton: false,\n })\n .setMaxWidth(\"none\")\n .setDOMContent(container)\n .setLngLat([longitude, latitude])\n .addTo(map);\n\n const onCloseProp = () => onClose?.();\n\n popup.on(\"close\", onCloseProp);\n\n popupRef.current = popup;\n\n return () => {\n popup.off(\"close\", onCloseProp);\n if (popup.isOpen()) {\n popup.remove();\n }\n popupRef.current = null;\n };\n }, [map]);\n\n useEffect(() => {\n popupRef.current?.setLngLat([longitude, latitude]);\n }, [longitude, latitude]);\n\n useEffect(() => {\n if (!popupRef.current) return;\n const prev = popupOptionsRef.current;\n\n if (prev.offset !== popupOptions.offset) {\n popupRef.current.setOffset(popupOptions.offset ?? 16);\n }\n if (prev.maxWidth !== popupOptions.maxWidth && popupOptions.maxWidth) {\n popupRef.current.setMaxWidth(popupOptions.maxWidth ?? \"none\");\n }\n\n popupOptionsRef.current = popupOptions;\n }, [popupOptions]);\n\n const handleClose = () => {\n popupRef.current?.remove();\n onClose?.();\n };\n\n return createPortal(\n \n {closeButton && (\n \n \n Close\n \n )}\n {children}\n ,\n container\n );\n}\n\ntype MapRouteProps = {\n coordinates: [number, number][];\n color?: string;\n width?: number;\n opacity?: number;\n dashArray?: [number, number];\n};\n\nfunction MapRoute({\n coordinates,\n color = \"#4285F4\",\n width = 3,\n opacity = 0.8,\n dashArray,\n}: MapRouteProps) {\n const { map, isLoaded } = useMap();\n const id = useId();\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 }, [isLoaded, map, sourceId, layerId]);\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 if (dashArray) {\n map.setPaintProperty(layerId, \"line-dasharray\", dashArray);\n }\n }, [isLoaded, map, layerId, color, width, opacity, dashArray]);\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};\n", "type": "registry:ui", "target": "components/ui/map.tsx" }