import { useCallback, useRef, useEffect } from 'react'; import { useDragState } from '../../contexts/DragStateContext'; import { resetShiftedOverlayState, resetDropState as resetDropStateUtil, resetOverlayState, } from '../../utils/dragStateReset'; /** * Return value from useDragCleanup hook. */ export interface UseDragCleanupResult { /** Clear the safety timeout (call when starting new drag) */ clearSafetyTimeout: () => void; /** Start failsafe cleanup timeout */ failsafeCleanup: () => void; /** Ref to the safety timeout for external management */ safetyTimeoutRef: React.MutableRefObject | null>; } /** * Hook that manages timeout-based cleanup and failsafe mechanisms. * * This hook is responsible for: * - Managing safety timeout that ensures state is reset after drop * - Failsafe cleanup that force-clears stuck state after registry changes * - Cleanup on unmount to prevent writes to stale SharedValues * * The safety timeout is a last-resort mechanism that fires if the normal * animation-based cleanup doesn't complete. This prevents the drag system * from getting stuck in an inconsistent state. * * @returns Functions for clearing timeouts and the safety timeout ref */ export function useDragCleanup(): UseDragCleanupResult { const { reorderRequested, pendingOverlayCount, shiftedOverlaysActive, shiftedOverlays, isDropping, expectedDropIndex, reorderInFlight, dropCommitVersion, draggedIndex, draggedItemId, overlayActive, } = useDragState(); // Track the safety timeout so it can be cleaned up on unmount or new drag const safetyTimeoutRef = useRef | null>(null); // v40.3: Failsafe cleanup - force overlay cleanup after timeout // This handles edge cases where registry changes don't fire for all items const failsafeCleanupRef = useRef | null>(null); const failsafeCleanup = useCallback(() => { // Clear any existing timeout if (failsafeCleanupRef.current !== null) { clearTimeout(failsafeCleanupRef.current); } failsafeCleanupRef.current = setTimeout(() => { failsafeCleanupRef.current = null; // Check if still stuck (use .value reads from JS thread) if (reorderRequested.value && pendingOverlayCount.value > 0) { // Force cleanup on UI thread using reset utilities resetShiftedOverlayState({ shiftedOverlaysActive, shiftedOverlays, pendingOverlayCount, reorderRequested, }); resetDropStateUtil({ isDropping, expectedDropIndex, reorderInFlight, dropCommitVersion, }); draggedIndex.value = -1; draggedItemId.value = ''; // Trigger crossfade fade-out. The crossfade completion callback will // clear overlayItemId after the fade completes. resetOverlayState({ overlayActive }); } }, 300); }, [ reorderRequested, pendingOverlayCount, shiftedOverlaysActive, shiftedOverlays, isDropping, expectedDropIndex, reorderInFlight, overlayActive, draggedIndex, dropCommitVersion, draggedItemId, ]); // Clear any pending safety timeout (called from worklet via scheduleOnRN) const clearSafetyTimeout = useCallback(() => { if (safetyTimeoutRef.current !== null) { clearTimeout(safetyTimeoutRef.current); safetyTimeoutRef.current = null; } }, []); // Clean up safety timeout on unmount to prevent writes to stale SharedValues useEffect(() => { return () => { if (safetyTimeoutRef.current !== null) { clearTimeout(safetyTimeoutRef.current); safetyTimeoutRef.current = null; } }; }, []); // Clean up failsafe timeout on unmount useEffect(() => { return () => { if (failsafeCleanupRef.current !== null) { clearTimeout(failsafeCleanupRef.current); failsafeCleanupRef.current = null; } }; }, []); return { clearSafetyTimeout, failsafeCleanup, safetyTimeoutRef, }; }