import { useRef, useState, useCallback } from 'react'; import { useIsMobile } from '../hooks/useIsMobile'; import './PullToRefresh.css'; const THRESHOLD = 60; const MAX_PULL = 120; const RESISTANCE = 0.5; const CONTENT_SHIFT_FACTOR = 0.3; export default function PullToRefresh({ onRefresh, children, className = '' }) { const isMobile = useIsMobile(); const [pullY, setPullY] = useState(0); const [state, setState] = useState('idle'); // idle | pulling | ready | refreshing const startYRef = useRef(null); const containerRef = useRef(null); const handleTouchStart = useCallback((e) => { const el = containerRef.current; if (!el) return; if (el.scrollTop > 0) return; // only trigger at top startYRef.current = e.touches[0].clientY; }, []); const handleTouchMove = useCallback((e) => { if (startYRef.current === null) return; const delta = e.touches[0].clientY - startYRef.current; if (delta <= 0) { setPullY(0); setState('idle'); return; } const clamped = Math.min(delta * RESISTANCE, MAX_PULL); setPullY(clamped); setState(clamped >= THRESHOLD ? 'ready' : 'pulling'); }, []); const handleTouchEnd = useCallback(async () => { if (startYRef.current === null) return; startYRef.current = null; if (state === 'ready') { setState('refreshing'); setPullY(THRESHOLD); try { await onRefresh?.(); } finally { setState('idle'); setPullY(0); } } else { setState('idle'); setPullY(0); } }, [state, onRefresh]); if (!isMobile) { return
{children}
; } const indicatorVisible = state !== 'idle'; const contentShift = pullY * CONTENT_SHIFT_FACTOR; return (
{children}
); }