forked from jordanlambrecht/tracker-tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuseCarousel.ts
More file actions
95 lines (81 loc) · 2.68 KB
/
useCarousel.ts
File metadata and controls
95 lines (81 loc) · 2.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// src/hooks/useCarousel.ts
"use client"
import { useCallback, useEffect, useRef, useState } from "react"
interface UseCarouselOptions {
itemCount: number
autoRotateMs?: number
swipeThreshold?: number
}
interface UseCarouselReturn {
activeIndex: number
direction: "left" | "right"
animating: boolean
goTo: (index: number) => void
onPointerDownCapture: (e: React.PointerEvent) => void
onPointerUp: (e: React.PointerEvent) => void
}
function useCarousel({
itemCount,
autoRotateMs,
swipeThreshold = 40,
}: UseCarouselOptions): UseCarouselReturn {
const [activeIndex, setActiveIndex] = useState(0)
const [direction, setDirection] = useState<"left" | "right">("left")
const [animating, setAnimating] = useState(false)
const swipeStartX = useRef(0)
const swipeStartY = useRef(0)
const pointerDown = useRef(false)
// Clamp activeIndex when item count shrinks
useEffect(() => {
if (itemCount > 0 && activeIndex >= itemCount) {
setActiveIndex(0)
}
}, [itemCount, activeIndex])
const goTo = useCallback(
(next: number) => {
setActiveIndex((prev) => {
setDirection(next > prev || (prev === itemCount - 1 && next === 0) ? "left" : "right")
setAnimating(true)
return next
})
},
[itemCount]
)
// Clear animation flag after transition
useEffect(() => {
if (!animating) return
const t = setTimeout(() => setAnimating(false), 300)
return () => clearTimeout(t)
}, [animating])
// Auto-rotate
useEffect(() => {
if (!autoRotateMs || itemCount <= 1) return
const timer = setInterval(() => {
goTo((activeIndex + 1) % itemCount)
}, autoRotateMs)
return () => clearInterval(timer)
}, [itemCount, activeIndex, goTo, autoRotateMs])
// Swipe gesture handlers
const onPointerDownCapture = useCallback((e: React.PointerEvent) => {
;(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId)
pointerDown.current = true
swipeStartX.current = e.clientX
swipeStartY.current = e.clientY
}, [])
const onPointerUp = useCallback(
(e: React.PointerEvent) => {
if (!pointerDown.current) return
pointerDown.current = false
if (itemCount <= 1) return
const dx = e.clientX - swipeStartX.current
const dy = e.clientY - swipeStartY.current
if (Math.abs(dx) < swipeThreshold || Math.abs(dx) < Math.abs(dy)) return
if (dx < 0) goTo((activeIndex + 1) % itemCount)
else goTo((activeIndex - 1 + itemCount) % itemCount)
},
[itemCount, activeIndex, goTo, swipeThreshold]
)
return { activeIndex, direction, animating, goTo, onPointerDownCapture, onPointerUp }
}
export type { UseCarouselOptions, UseCarouselReturn }
export { useCarousel }