"use client"; import { useCallback, useEffect, useRef, useState } from "react"; interface BottomSheetProps { open: boolean; onClose: () => void; children: React.ReactNode; } const SNAP_POINTS = { PEEK: 0.4, HALF: 0.55, FULL: 0.92 }; const VELOCITY_THRESHOLD = 0.5; export default function BottomSheet({ open, onClose, children }: BottomSheetProps) { const sheetRef = useRef(null); const contentRef = useRef(null); const [height, setHeight] = useState(SNAP_POINTS.PEEK); const [dragging, setDragging] = useState(false); const dragState = useRef({ startY: 0, startH: 0, lastY: 0, lastTime: 0 }); // Reset to peek when opened useEffect(() => { if (open) setHeight(SNAP_POINTS.PEEK); }, [open]); const snapTo = useCallback((h: number, velocity: number) => { // If fast downward swipe, close if (velocity > VELOCITY_THRESHOLD && h < SNAP_POINTS.HALF) { onClose(); return; } // Snap to nearest point const points = [SNAP_POINTS.PEEK, SNAP_POINTS.HALF, SNAP_POINTS.FULL]; let best = points[0]; let bestDist = Math.abs(h - best); for (const p of points) { const d = Math.abs(h - p); if (d < bestDist) { best = p; bestDist = d; } } // If dragged below peek, close if (h < SNAP_POINTS.PEEK * 0.6) { onClose(); return; } setHeight(best); }, [onClose]); const onTouchStart = useCallback((e: React.TouchEvent) => { // Don't intercept if scrolling inside content that has scrollable area const content = contentRef.current; if (content && content.scrollTop > 0 && height >= SNAP_POINTS.FULL - 0.05) return; const y = e.touches[0].clientY; dragState.current = { startY: y, startH: height, lastY: y, lastTime: Date.now() }; setDragging(true); }, [height]); const onTouchMove = useCallback((e: React.TouchEvent) => { if (!dragging) return; const y = e.touches[0].clientY; const vh = window.innerHeight; const deltaRatio = (dragState.current.startY - y) / vh; const newH = Math.max(0.1, Math.min(SNAP_POINTS.FULL, dragState.current.startH + deltaRatio)); setHeight(newH); dragState.current.lastY = y; dragState.current.lastTime = Date.now(); }, [dragging]); const onTouchEnd = useCallback(() => { if (!dragging) return; setDragging(false); const dt = (Date.now() - dragState.current.lastTime) / 1000 || 0.1; const dy = (dragState.current.startY - dragState.current.lastY) / window.innerHeight; const velocity = -dy / dt; // positive = downward snapTo(height, velocity); }, [dragging, height, snapTo]); if (!open) return null; return ( <> {/* Backdrop */}
{/* Sheet */}
{/* Handle bar */}
{/* Content */}
{children}
); }