Add swipe-to-dismiss gesture for mobile stat block drawer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-11 22:48:48 +01:00
parent 2ce0ff50b9
commit 1cf30b3622
2 changed files with 82 additions and 1 deletions

View File

@@ -0,0 +1,72 @@
import { useCallback, useRef, useState } from "react";
const DISMISS_THRESHOLD = 0.35;
const VELOCITY_THRESHOLD = 0.5;
interface SwipeState {
offsetX: number;
isSwiping: boolean;
}
export function useSwipeToDismiss(onDismiss: () => void) {
const [swipe, setSwipe] = useState<SwipeState>({
offsetX: 0,
isSwiping: false,
});
const startX = useRef(0);
const startY = useRef(0);
const startTime = useRef(0);
const panelWidth = useRef(0);
const directionLocked = useRef<"horizontal" | "vertical" | null>(null);
const onTouchStart = useCallback((e: React.TouchEvent) => {
const touch = e.touches[0];
startX.current = touch.clientX;
startY.current = touch.clientY;
startTime.current = Date.now();
directionLocked.current = null;
const el = e.currentTarget as HTMLElement;
panelWidth.current = el.getBoundingClientRect().width;
}, []);
const onTouchMove = useCallback((e: React.TouchEvent) => {
const touch = e.touches[0];
const dx = touch.clientX - startX.current;
const dy = touch.clientY - startY.current;
if (!directionLocked.current) {
if (Math.abs(dx) < 10 && Math.abs(dy) < 10) return;
directionLocked.current =
Math.abs(dx) > Math.abs(dy) ? "horizontal" : "vertical";
}
if (directionLocked.current === "vertical") return;
const clampedX = Math.max(0, dx);
setSwipe({ offsetX: clampedX, isSwiping: true });
}, []);
const onTouchEnd = useCallback(() => {
if (directionLocked.current !== "horizontal") {
setSwipe({ offsetX: 0, isSwiping: false });
return;
}
const elapsed = (Date.now() - startTime.current) / 1000;
const velocity = swipe.offsetX / elapsed / panelWidth.current;
const ratio =
panelWidth.current > 0 ? swipe.offsetX / panelWidth.current : 0;
if (ratio > DISMISS_THRESHOLD || velocity > VELOCITY_THRESHOLD) {
onDismiss();
}
setSwipe({ offsetX: 0, isSwiping: false });
}, [swipe.offsetX, onDismiss]);
return {
offsetX: swipe.offsetX,
isSwiping: swipe.isSwiping,
handlers: { onTouchStart, onTouchMove, onTouchEnd },
};
}