"use client"; /** * useTapGuard — tells a genuine tap apart from a swipe on a natively-scrolling * strip (CSS scroll-snap carousels). * * Browsers still fire a `click` after some swipe gestures, so a plain onClick * on a slide would trigger mid-swipe. Snapshot the pointer position AND the * scroller's offset on pointerdown, then check both when the click arrives: * * • pointer travel — catches a drag that didn't scroll (e.g. hit the end) * • scroll movement — catches an inertial swipe, where the pointer barely * moves before `click` fires but the strip is already gliding * * Either signal alone misses cases, hence both. No staleness risk: a click is * always immediately preceded by a pointerdown on the same element. */ import { useRef, type RefObject } from "react"; /** Pointer travel (px) still considered a tap rather than a drag. */ const MOVE_TOLERANCE = 8; /** Scroll movement (px) still considered "didn't scroll". */ const SCROLL_TOLERANCE = 4; type PointerLike = { clientX: number; clientY: number }; export function useTapGuard(scrollerRef: RefObject) { const start = useRef({ x: 0, y: 0, scrollLeft: 0 }); function onPointerDown(e: PointerLike) { start.current = { x: e.clientX, y: e.clientY, scrollLeft: scrollerRef.current?.scrollLeft ?? 0, }; } function wasTap(e: PointerLike) { const moved = Math.abs(e.clientX - start.current.x) > MOVE_TOLERANCE || Math.abs(e.clientY - start.current.y) > MOVE_TOLERANCE; const scrolled = Math.abs( (scrollerRef.current?.scrollLeft ?? 0) - start.current.scrollLeft, ) > SCROLL_TOLERANCE; return !moved && !scrolled; } return { onPointerDown, wasTap }; }