import { useState, useCallback, useRef } from 'react' interface UseShiftSelectOptions { items: T[] getItemId: (item: T) => string canSelect?: (item: T) => boolean onSelectionChange?: (selectedIds: string[]) => void } interface UseShiftSelectReturn { selectedItems: string[] setSelectedItems: (items: string[]) => void handleItemClick: (item: T, event?: React.MouseEvent) => void handleSelectAll: () => void isSelected: (itemId: string) => boolean clearSelection: () => void } export function useShiftSelect({ items, getItemId, canSelect = () => true, onSelectionChange }: UseShiftSelectOptions): UseShiftSelectReturn { const [selectedItems, setSelectedItemsState] = useState([]) const lastClickedIndexRef = useRef(null) const setSelectedItems = useCallback((items: string[]) => { setSelectedItemsState(items) onSelectionChange?.(items) }, [onSelectionChange]) const isSelected = useCallback((itemId: string) => { return selectedItems.includes(itemId) }, [selectedItems]) const clearSelection = useCallback(() => { setSelectedItems([]) lastClickedIndexRef.current = null }, [setSelectedItems]) const handleItemClick = useCallback((item: T, event?: React.MouseEvent) => { if (!canSelect(item)) return const itemId = getItemId(item) const currentIndex = items.findIndex(i => getItemId(i) === itemId) if (currentIndex === -1) return // Handle shift+click for range selection if (event?.shiftKey && lastClickedIndexRef.current !== null) { const lastIndex = lastClickedIndexRef.current const start = Math.min(currentIndex, lastIndex) const end = Math.max(currentIndex, lastIndex) // Get all selectable items in the range const rangeItems = items.slice(start, end + 1) const selectableRangeIds = rangeItems .filter(canSelect) .map(getItemId) // Add range to current selection (union) const newSelection = Array.from(new Set([...selectedItems, ...selectableRangeIds])) setSelectedItems(newSelection) } // Handle ctrl/cmd+click for individual toggle else if (event?.ctrlKey || event?.metaKey) { if (isSelected(itemId)) { setSelectedItems(selectedItems.filter(id => id !== itemId)) } else { setSelectedItems([...selectedItems, itemId]) } lastClickedIndexRef.current = currentIndex } // Handle regular click - toggle individual item else { if (isSelected(itemId)) { setSelectedItems(selectedItems.filter(id => id !== itemId)) } else { setSelectedItems([...selectedItems, itemId]) } lastClickedIndexRef.current = currentIndex } }, [items, selectedItems, canSelect, getItemId, isSelected, setSelectedItems]) const handleSelectAll = useCallback(() => { const selectableItems = items.filter(canSelect) const selectableIds = selectableItems.map(getItemId) // If all selectable items are selected, deselect all if (selectedItems.length === selectableIds.length && selectableIds.every(id => selectedItems.includes(id))) { setSelectedItems([]) lastClickedIndexRef.current = null } else { // Otherwise, select all selectable items setSelectedItems(selectableIds) lastClickedIndexRef.current = null } }, [items, selectedItems, canSelect, getItemId, setSelectedItems]) return { selectedItems, setSelectedItems, handleItemClick, handleSelectAll, isSelected, clearSelection } }