{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "use-swipe-navigation", "type": "registry:hook", "title": "useSwipeNavigation", "description": "Swipe between tabs with an axis-locked touch gesture that flips direction under RTL.", "categories": [ "hooks" ], "registryDependencies": [ "https://whiskeyjack.net/r/direction.json", "https://whiskeyjack.net/r/use-reduced-motion.json" ], "files": [ { "path": "hooks/use-swipe-navigation.ts", "type": "registry:hook", "target": "hooks/use-swipe-navigation.ts", "content": "import * as React from 'react'\nimport { useReducedMotion } from '@/hooks/use-reduced-motion'\nimport { isRTL } from '@/lib/direction'\n\nexport interface UseSwipeNavigationOptions {\n /** Total number of tabs/items. */\n count: number\n /** Index of the currently active item. */\n activeIndex: number\n /** Called when the user has committed a swipe. */\n onNavigate: (index: number) => void\n}\n\nexport interface UseSwipeNavigationResult {\n /** Horizontal translate offset while dragging, in pixels. */\n swipeOffset: number\n /** True while the snap-back or slide-in animation is running. */\n isAnimating: boolean\n /** Attach to the outer wrapper div's onTouchStart. */\n handleTouchStart: (e: React.TouchEvent) => void\n /** Attach to the outer wrapper div's onTouchMove. */\n handleTouchMove: (e: React.TouchEvent) => void\n /** Attach to the outer wrapper div's onTouchEnd. */\n handleTouchEnd: () => void\n /**\n * Spread onto the sliding content's `style`. Carries the drag transform, the\n * settle transition and the drag opacity fade, so the motion's feel lives\n * here rather than being restated at each call site.\n *\n * `willChange` is set only while a gesture is in flight; a permanent\n * `will-change-transform` class alongside this would keep the compositing\n * layer promoted at rest.\n */\n slideStyle: React.CSSProperties\n}\n\n/**\n * Swipe-to-switch-tabs touch logic. Extracted verbatim from the Chip Away and\n * Uradi Dashboard pages. Behavioural constants:\n *\n * - 8px direction lock threshold\n * - 0.25 edge dampening factor (when already at the first or last item)\n * - 60px commit threshold\n * - 0.4 opacity floor during swipe\n * - 0.97 scale floor during swipe (1 - |offset| / 4800)\n * - 120px slide-in offset for incoming content\n * - 300ms animation duration; a committed swipe's slide-in settles on the\n * spring curve (a few percent of overshoot, no wind-up), an uncommitted\n * snap-back on ease-out\n *\n * The touch handlers ignore swipes that begin on a `[data-tab-bar]` element so\n * the tab strip can still scroll horizontally without triggering navigation.\n *\n * Usage:\n *\n * ```tsx\n * const { slideStyle, handleTouchStart, handleTouchMove, handleTouchEnd } =\n * useSwipeNavigation({ count: items.length, activeIndex, onNavigate: setActiveIndex })\n *\n *