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; selected?: boolean; className?: string; } const COMMIT_THRESHOLD = 0.32; const MAX_SWIPE = 0.88; const COMMIT_DELAY_MS = 140; export function SwipeToArchive({ children, onArchive, disabled = false, selected = false, className, }: SwipeToArchiveProps) { const containerRef = useRef(null); const startPointRef = useRef<{ x: number; y: number } | null>(null); const widthRef = useRef(0); const archiveTimeoutRef = useRef(null); const suppressClickTimeoutRef = useRef(null); const suppressClickRef = useRef(false); const offsetXRef = useRef(0); const [offsetX, setOffsetX] = useState(0); const [isDragging, setIsDragging] = useState(false); const [isCollapsing, setIsCollapsing] = useState(false); const [lockedHeight, setLockedHeight] = useState(null); useEffect(() => { return () => { if (archiveTimeoutRef.current !== null) window.clearTimeout(archiveTimeoutRef.current); if (suppressClickTimeoutRef.current !== null) window.clearTimeout(suppressClickTimeoutRef.current); archiveTimeoutRef.current = null; suppressClickTimeoutRef.current = null; startPointRef.current = null; suppressClickRef.current = false; }; }, []); const clearArchiveTimeout = () => { if (archiveTimeoutRef.current === null) return; window.clearTimeout(archiveTimeoutRef.current); archiveTimeoutRef.current = null; }; const clearClickSuppressionTimeout = () => { if (suppressClickTimeoutRef.current === null) return; window.clearTimeout(suppressClickTimeoutRef.current); suppressClickTimeoutRef.current = null; }; const clearClickSuppression = () => { clearClickSuppressionTimeout(); suppressClickRef.current = false; }; const releaseClickSuppressionSoon = () => { clearClickSuppressionTimeout(); suppressClickTimeoutRef.current = window.setTimeout(() => { suppressClickRef.current = false; suppressClickTimeoutRef.current = null; }, 350); }; const reset = () => { startPointRef.current = null; offsetXRef.current = 0; setIsDragging(false); setIsCollapsing(false); setLockedHeight(null); setOffsetX(0); }; const commitArchive = () => { clearArchiveTimeout(); const node = containerRef.current; if (!node) { onArchive(); return; } startPointRef.current = null; setIsDragging(false); setLockedHeight(node.offsetHeight); const commitOffset = -Math.max(widthRef.current, node.offsetWidth); offsetXRef.current = commitOffset; setOffsetX(commitOffset); window.requestAnimationFrame(() => { window.requestAnimationFrame(() => { setIsCollapsing(true); }); }); archiveTimeoutRef.current = window.setTimeout(() => { archiveTimeoutRef.current = null; onArchive(); }, COMMIT_DELAY_MS); }; const handleTouchStart = (event: React.TouchEvent) => { if (disabled || isCollapsing || archiveTimeoutRef.current !== null || event.touches.length !== 1) return; clearArchiveTimeout(); clearClickSuppression(); const touch = event.touches[0]; const node = containerRef.current; widthRef.current = node?.offsetWidth ?? 0; setLockedHeight(node?.offsetHeight ?? null); setIsCollapsing(false); suppressClickRef.current = 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; } suppressClickRef.current = true; } if (deltaX >= 0) { event.preventDefault(); setIsDragging(true); offsetXRef.current = 0; setOffsetX(0); return; } const maxSwipe = widthRef.current > 0 ? widthRef.current * MAX_SWIPE : Number.POSITIVE_INFINITY; event.preventDefault(); setIsDragging(true); const nextOffset = Math.max(deltaX, -maxSwipe); offsetXRef.current = nextOffset; setOffsetX(nextOffset); }; const handleTouchEnd = () => { if (disabled || isCollapsing || archiveTimeoutRef.current !== null) return; const shouldCommit = widthRef.current > 0 && Math.abs(offsetXRef.current) >= widthRef.current * COMMIT_THRESHOLD; if (shouldCommit) { releaseClickSuppressionSoon(); commitArchive(); return; } if (isDragging) releaseClickSuppressionSoon(); reset(); }; const handleTouchCancel = () => { if (disabled || isCollapsing || archiveTimeoutRef.current !== null) return; clearClickSuppression(); reset(); }; const archiveReveal = widthRef.current > 0 ? Math.min(Math.abs(offsetX) / widthRef.current, 1) : 0; return (
{ if (!suppressClickRef.current) return; event.preventDefault(); event.stopPropagation(); clearClickSuppression(); }} >
{children}
); }