import React from 'react'; import { ArrowDown, Loader2 } from 'lucide-react'; import { cn } from '@/lib/utils'; const MAX_PULL = 96; const TRIGGER_PULL = 72; const DAMPING = 0.55; type PullToRefreshProps = { onRefresh: () => Promise | void; disabled?: boolean; className?: string; pullLabel?: string; releaseLabel?: string; refreshingLabel?: string; children: React.ReactNode; }; export default function PullToRefresh({ onRefresh, disabled = false, className, pullLabel = 'Pull to refresh', releaseLabel = 'Release to refresh', refreshingLabel = 'Refreshing…', children, }: PullToRefreshProps) { const containerRef = React.useRef(null); const startYRef = React.useRef(null); const pullDistanceRef = React.useRef(0); const [pullDistance, setPullDistance] = React.useState(0); const [dragging, setDragging] = React.useState(false); const [refreshing, setRefreshing] = React.useState(false); const updatePull = React.useCallback((value: number) => { pullDistanceRef.current = value; setPullDistance(value); }, []); React.useEffect(() => { const container = containerRef.current; if (!container || disabled) { return; } const handleStart = (event: TouchEvent) => { if (refreshing || window.scrollY > 0) { return; } startYRef.current = event.touches[0]?.clientY ?? null; setDragging(true); }; const handleMove = (event: TouchEvent) => { if (refreshing || startYRef.current === null) { return; } if (window.scrollY > 0) { startYRef.current = null; updatePull(0); setDragging(false); return; } const currentY = event.touches[0]?.clientY ?? 0; const delta = currentY - startYRef.current; if (delta <= 0) { updatePull(0); return; } event.preventDefault(); const next = Math.min(MAX_PULL, delta * DAMPING); updatePull(next); }; const handleEnd = async () => { if (startYRef.current === null) { return; } startYRef.current = null; setDragging(false); if (pullDistanceRef.current >= TRIGGER_PULL) { setRefreshing(true); updatePull(TRIGGER_PULL); try { await onRefresh(); } finally { setRefreshing(false); updatePull(0); } return; } updatePull(0); }; container.addEventListener('touchstart', handleStart, { passive: true }); container.addEventListener('touchmove', handleMove, { passive: false }); container.addEventListener('touchend', handleEnd); container.addEventListener('touchcancel', handleEnd); return () => { container.removeEventListener('touchstart', handleStart); container.removeEventListener('touchmove', handleMove); container.removeEventListener('touchend', handleEnd); container.removeEventListener('touchcancel', handleEnd); }; }, [disabled, onRefresh, refreshing, updatePull]); const progress = Math.min(pullDistance / TRIGGER_PULL, 1); const ready = pullDistance >= TRIGGER_PULL; const indicatorLabel = refreshing ? refreshingLabel : ready ? releaseLabel : pullLabel; return (
{refreshing ? ( ) : ( )} {indicatorLabel}
{children}
); }