import { useCallback, useMemo, useRef, useState } from 'react'; import type { MapMarkerData } from './map-marker'; import type { MapFeatureRef } from './map.types'; export function buildMarkerFeatureRef(marker: MapMarkerData, index: number): MapFeatureRef { return { id: marker.id ?? String(index), kind: 'marker', position: marker.position, index, data: marker.data, }; } export interface UseMapSelectionOptions { markers: MapMarkerData[]; selectedId?: string | null; onFeatureSelect?: (id: string | null, feature: MapFeatureRef | null) => void; onFeatureHover?: (id: string | null, feature: MapFeatureRef | null) => void; } /** * Selection and hover state for a set of markers. * * @description * Shared by both renderers — the Google-backed map and the keyless one — so a * marker behaves identically whether or not the Maps script ever loaded. * * Supports controlled and uncontrolled use: a `selectedId` prop wins when * present, otherwise the hook tracks its own index. That is what keeps the * pre-existing click-to-open-InfoWindow behavior working for consumers who * never adopt the selection API. */ export function useMapSelection({ markers, selectedId, onFeatureSelect, onFeatureHover, }: UseMapSelectionOptions) { const [internalIndex, setInternalIndex] = useState(null); const selectionEnabled = selectedId !== undefined || Boolean(onFeatureSelect); const selectedIndex = useMemo(() => { if (selectedId === undefined) return internalIndex; if (selectedId === null) return null; const found = markers.findIndex(marker => marker.id === selectedId); return found === -1 ? null : found; }, [selectedId, internalIndex, markers]); // Refs keep the returned callbacks stable across parent renders, which is what // lets marker effects subscribe once instead of on every render. const onSelectRef = useRef(onFeatureSelect); const onHoverRef = useRef(onFeatureHover); onSelectRef.current = onFeatureSelect; onHoverRef.current = onFeatureHover; const markersRef = useRef(markers); markersRef.current = markers; const selectFeature = useCallback((index: number | null) => { if (index === null) { setInternalIndex(null); onSelectRef.current?.(null, null); return; } const marker = markersRef.current[index]; if (!marker) return; setInternalIndex(index); onSelectRef.current?.(marker.id ?? String(index), buildMarkerFeatureRef(marker, index)); }, []); const hoverFeature = useCallback((index: number, hovering: boolean) => { if (!hovering) { onHoverRef.current?.(null, null); return; } const marker = markersRef.current[index]; if (!marker) return; onHoverRef.current?.(marker.id ?? String(index), buildMarkerFeatureRef(marker, index)); }, []); return { selectionEnabled, selectedIndex, selectFeature, hoverFeature }; }