import { useRef, useState } from 'react'; import type { TouchEvent } from 'react'; import { cn } from '@/lib/utils'; interface Props { label: string; isActive: boolean; onTap: () => void; onClose: () => void; canClose: boolean; } const CLOSE_THRESHOLD = 60; const MAX_TRAVEL = 120; const VERTICAL_BAIL = 30; // Pane tab with horizontal swipe-to-close (mobile only). Tracks horizontal // finger movement; if vertical exceeds VERTICAL_BAIL the gesture is cancelled // (so vertical scroll still works). On release past CLOSE_THRESHOLD, the // onClose callback fires. Otherwise the tab snaps back. Hand-rolled per spec. export function SwipeablePaneTab({ label, isActive, onTap, onClose, canClose }: Props) { const [translateX, setTranslateX] = useState(0); const [dragging, setDragging] = useState(false); const startRef = useRef<{ x: number; y: number; bailed: boolean } | null>(null); const onTouchStart = (e: TouchEvent) => { if (!canClose) return; const t = e.touches[0]; if (!t) return; startRef.current = { x: t.clientX, y: t.clientY, bailed: false }; setDragging(true); }; const onTouchMove = (e: TouchEvent) => { const start = startRef.current; if (!start || start.bailed) return; const t = e.touches[0]; if (!t) return; const dx = t.clientX - start.x; const dy = t.clientY - start.y; if (Math.abs(dy) > VERTICAL_BAIL) { start.bailed = true; setTranslateX(0); setDragging(false); return; } if (dx < 0) { setTranslateX(Math.max(dx, -MAX_TRAVEL)); } else { setTranslateX(0); } }; const onTouchEnd = () => { const start = startRef.current; startRef.current = null; setDragging(false); if (!start || start.bailed) { setTranslateX(0); return; } const tx = translateX; if (tx <= -CLOSE_THRESHOLD) { onClose(); // Don't reset translateX; the parent will unmount this tab. } else { setTranslateX(0); } }; // Opacity fades from 1 -> 0.4 as the tab approaches the close threshold. const opacity = translateX < 0 ? Math.max(0.4, 1 - (Math.abs(translateX) / CLOSE_THRESHOLD) * 0.6) : 1; return ( ); }