import { useEffect, useRef, useState, type ReactNode } from "react"; import { Archive } from "lucide-react"; import { cn } from "../lib/utils"; interface SwipeToArchiveProps { children: ReactNode; onArchive: () => void; disabled?: boolean; className?: string; } const COMMIT_THRESHOLD = 0.4; const MAX_SWIPE = 0.92; const COMMIT_DELAY_MS = 210; export function SwipeToArchive({ children, onArchive, disabled = false, className, }: SwipeToArchiveProps) { const containerRef = useRef(null); const startPointRef = useRef<{ x: number; y: number } | null>(null); const widthRef = useRef(0); const timeoutRef = useRef(null); const [offsetX, setOffsetX] = useState(0); const [isDragging, setIsDragging] = useState(false); const [isCollapsing, setIsCollapsing] = useState(false); const [lockedHeight, setLockedHeight] = useState(null); useEffect(() => { return () => { if (timeoutRef.current !== null) { window.clearTimeout(timeoutRef.current); } }; }, []); const reset = () => { startPointRef.current = null; setIsDragging(false); setOffsetX(0); }; const commitArchive = () => { const node = containerRef.current; if (!node) { onArchive(); return; } setIsDragging(false); setLockedHeight(node.offsetHeight); setOffsetX(-Math.max(widthRef.current, node.offsetWidth)); window.requestAnimationFrame(() => { window.requestAnimationFrame(() => { setIsCollapsing(true); }); }); timeoutRef.current = window.setTimeout(() => { onArchive(); }, COMMIT_DELAY_MS); }; const handleTouchStart = (event: React.TouchEvent) => { if (disabled || event.touches.length !== 1) return; const touch = event.touches[0]; const node = containerRef.current; widthRef.current = node?.offsetWidth ?? 0; setLockedHeight(node?.offsetHeight ?? null); setIsCollapsing(false); startPointRef.current = { x: touch.clientX, y: touch.clientY }; }; const handleTouchMove = (event: React.TouchEvent) => { if (disabled || isCollapsing) return; const startPoint = startPointRef.current; if (!startPoint || event.touches.length !== 1) return; const touch = event.touches[0]; const deltaX = touch.clientX - startPoint.x; const deltaY = touch.clientY - startPoint.y; if (!isDragging) { if (Math.abs(deltaX) < 6) return; if (Math.abs(deltaY) > Math.abs(deltaX)) { startPointRef.current = null; return; } } if (deltaX >= 0) { event.preventDefault(); setIsDragging(true); setOffsetX(0); return; } const maxSwipe = widthRef.current > 0 ? widthRef.current * MAX_SWIPE : Number.POSITIVE_INFINITY; event.preventDefault(); setIsDragging(true); setOffsetX(Math.max(deltaX, -maxSwipe)); }; const handleTouchEnd = () => { if (disabled || isCollapsing) return; const shouldCommit = widthRef.current > 0 && Math.abs(offsetX) >= widthRef.current * COMMIT_THRESHOLD; if (shouldCommit) { commitArchive(); return; } reset(); }; const archiveReveal = widthRef.current > 0 ? Math.min(Math.abs(offsetX) / widthRef.current, 1) : 0; return (
{children}
); }