import { useCallback, useMemo, useRef, useState } from 'react'; /** * Multi-select over an ordered list of asset ids. * * Shift-click selects a range, which is why the hook needs the ORDER of everything currently on screen and not * just the set of chosen ids. The order comes from the view, so a timeline that has loaded three months and an * album that has loaded one page both get the same behaviour without either knowing how the other paginates. */ export function useSelection(orderedIds: string[]) { const [selected, setSelected] = useState>(new Set()); const anchor = useRef(null); const order = useMemo(() => new Map(orderedIds.map((id, i) => [id, i])), [orderedIds]); const toggle = useCallback( (id: string, ev?: { shiftKey?: boolean; preventDefault?: () => void; stopPropagation?: () => void }) => { ev?.preventDefault?.(); ev?.stopPropagation?.(); setSelected((prev) => { const next = new Set(prev); const from = anchor.current; if (ev?.shiftKey && from && order.has(from) && order.has(id)) { const a = order.get(from) ?? 0; const b = order.get(id) ?? 0; for (let i = Math.min(a, b); i <= Math.max(a, b); i++) { const between = orderedIds[i]; if (between) next.add(between); } return next; } if (next.has(id)) next.delete(id); else next.add(id); anchor.current = id; return next; }); }, [order, orderedIds], ); const clear = useCallback(() => { anchor.current = null; setSelected(new Set()); }, []); const selectAll = useCallback(() => setSelected(new Set(orderedIds)), [orderedIds]); const ids = useMemo(() => orderedIds.filter((id) => selected.has(id)), [orderedIds, selected]); return { selected, /** In document order, so "add to album" preserves what the eye saw rather than insertion order. */ ids, count: selected.size, isSelecting: selected.size > 0, toggle, clear, selectAll, }; } export type Selection = ReturnType;