import { useCallback, useRef, useState } from "react"; const DISMISS_THRESHOLD = 0.35; const VELOCITY_THRESHOLD = 0.5; interface SwipeState { offsetX: number; isSwiping: boolean; } export function useSwipeToDismiss(onDismiss: () => void) { const [swipe, setSwipe] = useState({ offsetX: 0, isSwiping: false, }); const startX = useRef(0); const startY = useRef(0); const startTime = useRef(0); const panelWidth = useRef(0); const directionLocked = useRef<"horizontal" | "vertical" | null>(null); const onTouchStart = useCallback((e: React.TouchEvent) => { const touch = e.touches[0]; startX.current = touch.clientX; startY.current = touch.clientY; startTime.current = Date.now(); directionLocked.current = null; const el = e.currentTarget as HTMLElement; panelWidth.current = el.getBoundingClientRect().width; }, []); const onTouchMove = useCallback((e: React.TouchEvent) => { const touch = e.touches[0]; const dx = touch.clientX - startX.current; const dy = touch.clientY - startY.current; if (!directionLocked.current) { if (Math.abs(dx) < 10 && Math.abs(dy) < 10) return; directionLocked.current = Math.abs(dx) > Math.abs(dy) ? "horizontal" : "vertical"; } if (directionLocked.current === "vertical") return; const clampedX = Math.max(0, dx); setSwipe({ offsetX: clampedX, isSwiping: true }); }, []); const onTouchEnd = useCallback(() => { if (directionLocked.current !== "horizontal") { setSwipe({ offsetX: 0, isSwiping: false }); return; } const elapsed = (Date.now() - startTime.current) / 1000; const velocity = swipe.offsetX / elapsed / panelWidth.current; const ratio = panelWidth.current > 0 ? swipe.offsetX / panelWidth.current : 0; if (ratio > DISMISS_THRESHOLD || velocity > VELOCITY_THRESHOLD) { onDismiss(); } setSwipe({ offsetX: 0, isSwiping: false }); }, [swipe.offsetX, onDismiss]); return { offsetX: swipe.offsetX, isSwiping: swipe.isSwiping, handlers: { onTouchStart, onTouchMove, onTouchEnd }, }; }