import React from 'react'; import { Crosshair, LoaderCircle } from 'lucide-react'; import { cn } from '../../shared/utils'; import { Button } from '../button'; import { mapOverlayPositionClasses } from '../map/map-overlay-position'; import { useDeviceLocation, type DeviceLocationReading, type DeviceLocationStatus, } from '../map/use-device-location'; import type { MapOverlayPosition } from '../map/map.types'; export interface MapLocateControlProps extends Omit< React.HTMLAttributes, 'onError' > { onLocate: (position: DeviceLocationReading['position'], meta: DeviceLocationReading) => void; /** Called on every failure, with the reason already classified. */ onError?: (status: DeviceLocationStatus, message: string | null) => void; /** @default "Use my location" */ label?: string; /** @default "Location permission denied" */ deniedLabel?: string; /** @default 'overlay' */ variant?: 'overlay' | 'inline'; /** @default 'bottom-right' */ position?: MapOverlayPosition; enableHighAccuracy?: boolean; } /** * Centres the map on the device's position. * * @description * Reports the reading plus its accuracy so the caller can draw the precision * halo, and classifies failures instead of swallowing them — a denied * permission is a different message from a timeout, and only one of them is * worth a retry button. * * @ai-rules * 1. Feed the result into `` so the accuracy halo is drawn. * 2. Handle `onError` — a silent failure leaves the user tapping a dead button. * 3. Never treat `mocked === false` as proof the coordinate is genuine. */ export const MapLocateControl = React.forwardRef( ( { onLocate, onError, label = 'Use my location', deniedLabel = 'Location permission denied', variant = 'overlay', position = 'bottom-right', enableHighAccuracy = true, className, ...props }, ref ) => { const { locate, status, error } = useDeviceLocation({ enableHighAccuracy }); const handleClick = async () => { const reading = await locate(); if (reading) onLocate(reading.position, reading); }; const isBusy = status === 'locating'; const isDenied = status === 'denied'; React.useEffect(() => { if (status === 'denied' || status === 'timeout' || status === 'unavailable') { onError?.(status, error); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [status, error]); return (
); } ); MapLocateControl.displayName = 'MapLocateControl';