react-native-ruler-date-picker 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/README.md +186 -60
  2. package/lib/commonjs/LensTick.js +83 -0
  3. package/lib/commonjs/LensTick.js.map +1 -0
  4. package/lib/commonjs/Ruler.js +38 -0
  5. package/lib/commonjs/Ruler.js.map +1 -0
  6. package/lib/commonjs/RulerContext.js +17 -0
  7. package/lib/commonjs/RulerContext.js.map +1 -0
  8. package/lib/commonjs/RulerDatePicker.js +38 -351
  9. package/lib/commonjs/RulerDatePicker.js.map +1 -1
  10. package/lib/commonjs/RulerDateRangePicker.js +56 -0
  11. package/lib/commonjs/RulerDateRangePicker.js.map +1 -0
  12. package/lib/commonjs/RulerHeader.js +106 -0
  13. package/lib/commonjs/RulerHeader.js.map +1 -0
  14. package/lib/commonjs/RulerIndicator.js +67 -0
  15. package/lib/commonjs/RulerIndicator.js.map +1 -0
  16. package/lib/commonjs/RulerLensTicks.js +55 -0
  17. package/lib/commonjs/RulerLensTicks.js.map +1 -0
  18. package/lib/commonjs/RulerLensTrack.js +52 -0
  19. package/lib/commonjs/RulerLensTrack.js.map +1 -0
  20. package/lib/commonjs/RulerRange.js +39 -0
  21. package/lib/commonjs/RulerRange.js.map +1 -0
  22. package/lib/commonjs/RulerRangeContext.js +17 -0
  23. package/lib/commonjs/RulerRangeContext.js.map +1 -0
  24. package/lib/commonjs/RulerRangeHeader.js +154 -0
  25. package/lib/commonjs/RulerRangeHeader.js.map +1 -0
  26. package/lib/commonjs/RulerRangeHighlight.js +50 -0
  27. package/lib/commonjs/RulerRangeHighlight.js.map +1 -0
  28. package/lib/commonjs/RulerRangeIndicator.js +71 -0
  29. package/lib/commonjs/RulerRangeIndicator.js.map +1 -0
  30. package/lib/commonjs/RulerRangeRoot.js +80 -0
  31. package/lib/commonjs/RulerRangeRoot.js.map +1 -0
  32. package/lib/commonjs/RulerRangeTicks.js +57 -0
  33. package/lib/commonjs/RulerRangeTicks.js.map +1 -0
  34. package/lib/commonjs/RulerRangeTrack.js +46 -0
  35. package/lib/commonjs/RulerRangeTrack.js.map +1 -0
  36. package/lib/commonjs/RulerResetButton.js +46 -0
  37. package/lib/commonjs/RulerResetButton.js.map +1 -0
  38. package/lib/commonjs/RulerRoot.js +87 -0
  39. package/lib/commonjs/RulerRoot.js.map +1 -0
  40. package/lib/commonjs/RulerTicks.js +51 -0
  41. package/lib/commonjs/RulerTicks.js.map +1 -0
  42. package/lib/commonjs/RulerTrack.js +46 -0
  43. package/lib/commonjs/RulerTrack.js.map +1 -0
  44. package/lib/commonjs/Tick.js +41 -36
  45. package/lib/commonjs/Tick.js.map +1 -1
  46. package/lib/commonjs/defaults.js +20 -19
  47. package/lib/commonjs/defaults.js.map +1 -1
  48. package/lib/commonjs/index.js +287 -1
  49. package/lib/commonjs/index.js.map +1 -1
  50. package/lib/commonjs/presets.js +108 -0
  51. package/lib/commonjs/presets.js.map +1 -0
  52. package/lib/commonjs/tickResolver.js +101 -0
  53. package/lib/commonjs/tickResolver.js.map +1 -0
  54. package/lib/commonjs/useHaptics.js +1 -1
  55. package/lib/commonjs/useHaptics.web.js +43 -0
  56. package/lib/commonjs/useHaptics.web.js.map +1 -0
  57. package/lib/commonjs/useRulerDatePicker.js +264 -0
  58. package/lib/commonjs/useRulerDatePicker.js.map +1 -0
  59. package/lib/commonjs/useRulerDateRangePicker.js +313 -0
  60. package/lib/commonjs/useRulerDateRangePicker.js.map +1 -0
  61. package/lib/commonjs/utils.js +61 -47
  62. package/lib/commonjs/utils.js.map +1 -1
  63. package/lib/module/LensTick.js +77 -0
  64. package/lib/module/LensTick.js.map +1 -0
  65. package/lib/module/Ruler.js +35 -0
  66. package/lib/module/Ruler.js.map +1 -0
  67. package/lib/module/RulerContext.js +12 -0
  68. package/lib/module/RulerContext.js.map +1 -0
  69. package/lib/module/RulerDatePicker.js +38 -350
  70. package/lib/module/RulerDatePicker.js.map +1 -1
  71. package/lib/module/RulerDateRangePicker.js +52 -0
  72. package/lib/module/RulerDateRangePicker.js.map +1 -0
  73. package/lib/module/RulerHeader.js +101 -0
  74. package/lib/module/RulerHeader.js.map +1 -0
  75. package/lib/module/RulerIndicator.js +62 -0
  76. package/lib/module/RulerIndicator.js.map +1 -0
  77. package/lib/module/RulerLensTicks.js +50 -0
  78. package/lib/module/RulerLensTicks.js.map +1 -0
  79. package/lib/module/RulerLensTrack.js +47 -0
  80. package/lib/module/RulerLensTrack.js.map +1 -0
  81. package/lib/module/RulerRange.js +36 -0
  82. package/lib/module/RulerRange.js.map +1 -0
  83. package/lib/module/RulerRangeContext.js +12 -0
  84. package/lib/module/RulerRangeContext.js.map +1 -0
  85. package/lib/module/RulerRangeHeader.js +149 -0
  86. package/lib/module/RulerRangeHeader.js.map +1 -0
  87. package/lib/module/RulerRangeHighlight.js +45 -0
  88. package/lib/module/RulerRangeHighlight.js.map +1 -0
  89. package/lib/module/RulerRangeIndicator.js +66 -0
  90. package/lib/module/RulerRangeIndicator.js.map +1 -0
  91. package/lib/module/RulerRangeRoot.js +75 -0
  92. package/lib/module/RulerRangeRoot.js.map +1 -0
  93. package/lib/module/RulerRangeTicks.js +51 -0
  94. package/lib/module/RulerRangeTicks.js.map +1 -0
  95. package/lib/module/RulerRangeTrack.js +41 -0
  96. package/lib/module/RulerRangeTrack.js.map +1 -0
  97. package/lib/module/RulerResetButton.js +41 -0
  98. package/lib/module/RulerResetButton.js.map +1 -0
  99. package/lib/module/RulerRoot.js +82 -0
  100. package/lib/module/RulerRoot.js.map +1 -0
  101. package/lib/module/RulerTicks.js +45 -0
  102. package/lib/module/RulerTicks.js.map +1 -0
  103. package/lib/module/RulerTrack.js +41 -0
  104. package/lib/module/RulerTrack.js.map +1 -0
  105. package/lib/module/Tick.js +41 -36
  106. package/lib/module/Tick.js.map +1 -1
  107. package/lib/module/defaults.js +19 -18
  108. package/lib/module/defaults.js.map +1 -1
  109. package/lib/module/index.js +46 -1
  110. package/lib/module/index.js.map +1 -1
  111. package/lib/module/presets.js +103 -0
  112. package/lib/module/presets.js.map +1 -0
  113. package/lib/module/tickResolver.js +97 -0
  114. package/lib/module/tickResolver.js.map +1 -0
  115. package/lib/module/useHaptics.js +1 -1
  116. package/lib/module/useHaptics.js.map +1 -1
  117. package/lib/module/useHaptics.web.js +39 -0
  118. package/lib/module/useHaptics.web.js.map +1 -0
  119. package/lib/module/useRulerDatePicker.js +260 -0
  120. package/lib/module/useRulerDatePicker.js.map +1 -0
  121. package/lib/module/useRulerDateRangePicker.js +310 -0
  122. package/lib/module/useRulerDateRangePicker.js.map +1 -0
  123. package/lib/module/utils.js +59 -45
  124. package/lib/module/utils.js.map +1 -1
  125. package/lib/typescript/LensTick.d.ts +20 -0
  126. package/lib/typescript/LensTick.d.ts.map +1 -0
  127. package/lib/typescript/Ruler.d.ts +31 -0
  128. package/lib/typescript/Ruler.d.ts.map +1 -0
  129. package/lib/typescript/RulerContext.d.ts +11 -0
  130. package/lib/typescript/RulerContext.d.ts.map +1 -0
  131. package/lib/typescript/RulerDatePicker.d.ts +10 -7
  132. package/lib/typescript/RulerDatePicker.d.ts.map +1 -1
  133. package/lib/typescript/RulerDateRangePicker.d.ts +26 -0
  134. package/lib/typescript/RulerDateRangePicker.d.ts.map +1 -0
  135. package/lib/typescript/RulerHeader.d.ts +14 -0
  136. package/lib/typescript/RulerHeader.d.ts.map +1 -0
  137. package/lib/typescript/RulerIndicator.d.ts +17 -0
  138. package/lib/typescript/RulerIndicator.d.ts.map +1 -0
  139. package/lib/typescript/RulerLensTicks.d.ts +12 -0
  140. package/lib/typescript/RulerLensTicks.d.ts.map +1 -0
  141. package/lib/typescript/RulerLensTrack.d.ts +13 -0
  142. package/lib/typescript/RulerLensTrack.d.ts.map +1 -0
  143. package/lib/typescript/RulerRange.d.ts +32 -0
  144. package/lib/typescript/RulerRange.d.ts.map +1 -0
  145. package/lib/typescript/RulerRangeContext.d.ts +11 -0
  146. package/lib/typescript/RulerRangeContext.d.ts.map +1 -0
  147. package/lib/typescript/RulerRangeHeader.d.ts +12 -0
  148. package/lib/typescript/RulerRangeHeader.d.ts.map +1 -0
  149. package/lib/typescript/RulerRangeHighlight.d.ts +8 -0
  150. package/lib/typescript/RulerRangeHighlight.d.ts.map +1 -0
  151. package/lib/typescript/RulerRangeIndicator.d.ts +17 -0
  152. package/lib/typescript/RulerRangeIndicator.d.ts.map +1 -0
  153. package/lib/typescript/RulerRangeRoot.d.ts +7 -0
  154. package/lib/typescript/RulerRangeRoot.d.ts.map +1 -0
  155. package/lib/typescript/RulerRangeTicks.d.ts +14 -0
  156. package/lib/typescript/RulerRangeTicks.d.ts.map +1 -0
  157. package/lib/typescript/RulerRangeTrack.d.ts +8 -0
  158. package/lib/typescript/RulerRangeTrack.d.ts.map +1 -0
  159. package/lib/typescript/RulerResetButton.d.ts +10 -0
  160. package/lib/typescript/RulerResetButton.d.ts.map +1 -0
  161. package/lib/typescript/RulerRoot.d.ts +7 -0
  162. package/lib/typescript/RulerRoot.d.ts.map +1 -0
  163. package/lib/typescript/RulerTicks.d.ts +9 -0
  164. package/lib/typescript/RulerTicks.d.ts.map +1 -0
  165. package/lib/typescript/RulerTrack.d.ts +8 -0
  166. package/lib/typescript/RulerTrack.d.ts.map +1 -0
  167. package/lib/typescript/Tick.d.ts +3 -16
  168. package/lib/typescript/Tick.d.ts.map +1 -1
  169. package/lib/typescript/defaults.d.ts +6 -9
  170. package/lib/typescript/defaults.d.ts.map +1 -1
  171. package/lib/typescript/index.d.ts +37 -1
  172. package/lib/typescript/index.d.ts.map +1 -1
  173. package/lib/typescript/presets.d.ts +27 -0
  174. package/lib/typescript/presets.d.ts.map +1 -0
  175. package/lib/typescript/tickResolver.d.ts +10 -0
  176. package/lib/typescript/tickResolver.d.ts.map +1 -0
  177. package/lib/typescript/types.d.ts +302 -85
  178. package/lib/typescript/types.d.ts.map +1 -1
  179. package/lib/typescript/useHaptics.web.d.ts +17 -0
  180. package/lib/typescript/useHaptics.web.d.ts.map +1 -0
  181. package/lib/typescript/useRulerDatePicker.d.ts +26 -0
  182. package/lib/typescript/useRulerDatePicker.d.ts.map +1 -0
  183. package/lib/typescript/useRulerDateRangePicker.d.ts +40 -0
  184. package/lib/typescript/useRulerDateRangePicker.d.ts.map +1 -0
  185. package/lib/typescript/utils.d.ts +14 -12
  186. package/lib/typescript/utils.d.ts.map +1 -1
  187. package/package.json +25 -7
  188. package/src/LensTick.tsx +90 -0
  189. package/src/Ruler.ts +32 -0
  190. package/src/RulerContext.tsx +20 -0
  191. package/src/RulerDatePicker.tsx +41 -439
  192. package/src/RulerDateRangePicker.tsx +53 -0
  193. package/src/RulerHeader.tsx +125 -0
  194. package/src/RulerIndicator.tsx +84 -0
  195. package/src/RulerLensTicks.tsx +50 -0
  196. package/src/RulerLensTrack.tsx +45 -0
  197. package/src/RulerRange.ts +33 -0
  198. package/src/RulerRangeContext.tsx +20 -0
  199. package/src/RulerRangeHeader.tsx +180 -0
  200. package/src/RulerRangeHighlight.tsx +47 -0
  201. package/src/RulerRangeIndicator.tsx +85 -0
  202. package/src/RulerRangeRoot.tsx +87 -0
  203. package/src/RulerRangeTicks.tsx +68 -0
  204. package/src/RulerRangeTrack.tsx +39 -0
  205. package/src/RulerResetButton.tsx +59 -0
  206. package/src/RulerRoot.tsx +93 -0
  207. package/src/RulerTicks.tsx +57 -0
  208. package/src/RulerTrack.tsx +39 -0
  209. package/src/Tick.tsx +60 -49
  210. package/src/defaults.ts +21 -19
  211. package/src/index.ts +83 -1
  212. package/src/presets.ts +108 -0
  213. package/src/tickResolver.ts +123 -0
  214. package/src/types.ts +354 -102
  215. package/src/useHaptics.web.ts +55 -0
  216. package/src/useRulerDatePicker.ts +373 -0
  217. package/src/useRulerDateRangePicker.ts +468 -0
  218. package/src/utils.ts +67 -68
  219. package/lib/commonjs/package.json +0 -1
  220. package/lib/module/package.json +0 -1
@@ -0,0 +1,373 @@
1
+ import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
2
+ import { useWindowDimensions } from 'react-native';
3
+ import {
4
+ useSharedValue,
5
+ withDecay,
6
+ withTiming,
7
+ useAnimatedReaction,
8
+ runOnJS,
9
+ type SharedValue,
10
+ } from 'react-native-reanimated';
11
+ import { Gesture, type PanGesture } from 'react-native-gesture-handler';
12
+
13
+ import type {
14
+ RulerDatePickerProps,
15
+ ResolvedTick,
16
+ ResolvedRulerTheme,
17
+ } from './types';
18
+ import { DEFAULTS, MS_PER_DAY } from './defaults';
19
+ import { resolveTickRules } from './presets';
20
+ import { resolveTick } from './tickResolver';
21
+ import {
22
+ formatDateString,
23
+ normalizeDate,
24
+ dateToDayOffset,
25
+ dayOffsetToDate,
26
+ computeDayRange,
27
+ useResolvedTheme,
28
+ } from './utils';
29
+ import { useHaptics } from './useHaptics';
30
+
31
+ export interface UseRulerDatePickerReturn {
32
+ // Animated values
33
+ translateX: SharedValue<number>;
34
+ panGesture: PanGesture;
35
+
36
+ // Selected State
37
+ selectedDate: Date;
38
+ selectedDateStr: string;
39
+ isTodaySelected: boolean;
40
+
41
+ // Resolved ticks
42
+ visibleTicks: ResolvedTick[];
43
+
44
+ // Resolved Theme & Layout
45
+ theme: ResolvedRulerTheme;
46
+ tickSpacing: number;
47
+ rulerHeight: number;
48
+ height: number;
49
+ width: number;
50
+ minOffset: number;
51
+ maxOffset: number;
52
+
53
+ // Actions
54
+ scrollToDate: (targetDate: Date | string, animated?: boolean) => void;
55
+ resetToToday: (animated?: boolean) => void;
56
+ }
57
+
58
+ /**
59
+ * Headless Hook encapsulating gesture physics, Reanimated 3 decay/snapping,
60
+ * windowed tick resolution, and haptics for the Ruler Date Picker.
61
+ */
62
+ export function useRulerDatePicker(
63
+ props: RulerDatePickerProps = {}
64
+ ): UseRulerDatePickerReturn {
65
+ const {
66
+ value,
67
+ defaultValue,
68
+ onChange,
69
+ minDate,
70
+ maxDate,
71
+ ticks = 'calendar',
72
+ tickSpacing = DEFAULTS.tickSpacing,
73
+ tickWidth = DEFAULTS.tickWidth,
74
+ rulerHeight = DEFAULTS.rulerHeight,
75
+ theme: propTheme,
76
+ debounceMs = DEFAULTS.debounceMs,
77
+ haptics = true,
78
+ style,
79
+ } = props;
80
+
81
+ const bufferTicks = DEFAULTS.bufferTicks;
82
+ const isHapticsEnabled = typeof haptics === 'boolean' ? haptics : (haptics?.enabled ?? true);
83
+ const resolvedHapticTrigger = typeof haptics === 'object' ? haptics.onTrigger : undefined;
84
+ const fastSwipeTickThreshold = (typeof haptics === 'object' ? haptics.fastSwipeTickThreshold : undefined) ?? DEFAULTS.fastSwipeTickThreshold;
85
+ const continuousHapticIntervalMs = (typeof haptics === 'object' ? haptics.continuousHapticIntervalMs : undefined) ?? DEFAULTS.continuousHapticIntervalMs;
86
+ const height = (style as any)?.height ?? DEFAULTS.height;
87
+
88
+ // ── Resolved Theme ────────────────────────────────────────────────────────
89
+ const theme = useResolvedTheme(propTheme);
90
+
91
+ // ── Resolved Rules ────────────────────────────────────────────────────────
92
+ const rules = useMemo(() => resolveTickRules(ticks), [ticks]);
93
+
94
+ // ── Haptics ───────────────────────────────────────────────────────────────
95
+ const { fireDiscreteHaptic, startContinuousHaptic, stopContinuousHaptic } =
96
+ useHaptics({
97
+ enableHaptics: isHapticsEnabled,
98
+ onHapticTrigger: resolvedHapticTrigger,
99
+ continuousHapticIntervalMs,
100
+ });
101
+
102
+ // ── Reference Time (Midnight Today) ───────────────────────────────────────
103
+ const todayTime = useMemo(() => {
104
+ const d = new Date();
105
+ d.setHours(0, 0, 0, 0);
106
+ return d.getTime();
107
+ }, []);
108
+
109
+ // ── Bounds ────────────────────────────────────────────────────────────────
110
+ const { minOffset, maxOffset } = useMemo(
111
+ () => computeDayRange(todayTime, minDate, maxDate),
112
+ [todayTime, minDate, maxDate]
113
+ );
114
+
115
+ // ── Initial Date Calculation ──────────────────────────────────────────────
116
+ const initialDate = useMemo(() => {
117
+ return normalizeDate(value ?? defaultValue ?? new Date());
118
+ // eslint-disable-next-line react-hooks/exhaustive-deps
119
+ }, []);
120
+
121
+ const initialOffsetDays = useMemo(() => {
122
+ return dateToDayOffset(initialDate, todayTime);
123
+ // eslint-disable-next-line react-hooks/exhaustive-deps
124
+ }, []);
125
+
126
+ // ── Animated Values ───────────────────────────────────────────────────────
127
+ const translateX = useSharedValue(-initialOffsetDays * tickSpacing);
128
+ const contextX = useSharedValue(0);
129
+ const previousIndex = useSharedValue(-initialOffsetDays);
130
+ const isFastMode = useSharedValue(false);
131
+
132
+ // ── State ─────────────────────────────────────────────────────────────────
133
+ const [selectedDate, setSelectedDate] = useState<Date>(initialDate);
134
+ const [selectedDateStr, setSelectedDateStr] = useState<string>(
135
+ formatDateString(initialDate)
136
+ );
137
+ const [windowCenter, setWindowCenter] = useState<number>(-initialOffsetDays);
138
+ const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
139
+ const windowDimensions = useWindowDimensions();
140
+ const width = props.width ?? windowDimensions.width;
141
+
142
+ const halfVisible = Math.ceil(width / 2 / tickSpacing);
143
+ const windowSize = halfVisible + bufferTicks;
144
+
145
+ // ── Visible Ticks Resolution ──────────────────────────────────────────────
146
+ const visibleTicks = useMemo(() => {
147
+ const resolved: ResolvedTick[] = [];
148
+ const start = Math.max(windowCenter - windowSize, minOffset);
149
+ const end = Math.min(windowCenter + windowSize, maxOffset);
150
+
151
+ for (let i = start; i <= end; i++) {
152
+ const d = dayOffsetToDate(i, todayTime);
153
+ const isToday = i === 0;
154
+ const isSelected = formatDateString(d) === selectedDateStr;
155
+
156
+ resolved.push(
157
+ resolveTick(
158
+ {
159
+ date: d,
160
+ offset: i,
161
+ isToday,
162
+ isSelected,
163
+ },
164
+ rules,
165
+ theme,
166
+ tickWidth
167
+ )
168
+ );
169
+ }
170
+ return resolved;
171
+ }, [
172
+ windowCenter,
173
+ windowSize,
174
+ minOffset,
175
+ maxOffset,
176
+ todayTime,
177
+ selectedDateStr,
178
+ rules,
179
+ theme,
180
+ tickWidth,
181
+ ]);
182
+
183
+ // ── Tick Changed Callback ─────────────────────────────────────────────────
184
+ const onTickChanged = useCallback(
185
+ (index: number, enteringFastMode: boolean) => {
186
+ if (enteringFastMode) {
187
+ startContinuousHaptic();
188
+ } else {
189
+ stopContinuousHaptic();
190
+ fireDiscreteHaptic();
191
+ }
192
+
193
+ const d = dayOffsetToDate(index, todayTime);
194
+ const dateStr = formatDateString(d);
195
+
196
+ setSelectedDate(d);
197
+ setSelectedDateStr(dateStr);
198
+
199
+ setWindowCenter((prev) => {
200
+ if (Math.abs(index - prev) >= 5) {
201
+ return index;
202
+ }
203
+ return prev;
204
+ });
205
+
206
+ if (debounceTimerRef.current) {
207
+ clearTimeout(debounceTimerRef.current);
208
+ }
209
+ debounceTimerRef.current = setTimeout(() => {
210
+ onChange?.(d, dateStr);
211
+ }, debounceMs);
212
+ },
213
+ [
214
+ todayTime,
215
+ onChange,
216
+ debounceMs,
217
+ startContinuousHaptic,
218
+ stopContinuousHaptic,
219
+ fireDiscreteHaptic,
220
+ ]
221
+ );
222
+
223
+ // ── Animation Settled Callback ────────────────────────────────────────────
224
+ const onAnimationSettled = useCallback(
225
+ (index: number) => {
226
+ stopContinuousHaptic();
227
+ fireDiscreteHaptic();
228
+
229
+ const d = dayOffsetToDate(index, todayTime);
230
+ const dateStr = formatDateString(d);
231
+
232
+ setSelectedDate(d);
233
+ setSelectedDateStr(dateStr);
234
+ setWindowCenter(index);
235
+
236
+ onChange?.(d, dateStr);
237
+ },
238
+ [todayTime, onChange, stopContinuousHaptic, fireDiscreteHaptic]
239
+ );
240
+
241
+ // ── Animated Reaction ─────────────────────────────────────────────────────
242
+ useAnimatedReaction(
243
+ () => Math.round(-translateX.value / tickSpacing),
244
+ (index, prev) => {
245
+ if (index !== prev && index >= minOffset && index <= maxOffset) {
246
+ previousIndex.value = index;
247
+ const ticksSkipped = prev !== null ? Math.abs(index - prev) : 1;
248
+ const shouldBeFast = ticksSkipped > fastSwipeTickThreshold;
249
+
250
+ if (shouldBeFast && !isFastMode.value) {
251
+ isFastMode.value = true;
252
+ runOnJS(onTickChanged)(index, true);
253
+ } else if (!shouldBeFast && isFastMode.value) {
254
+ isFastMode.value = false;
255
+ runOnJS(onTickChanged)(index, false);
256
+ } else if (!shouldBeFast) {
257
+ runOnJS(onTickChanged)(index, false);
258
+ }
259
+ if (shouldBeFast && isFastMode.value) {
260
+ runOnJS(onTickChanged)(index, true);
261
+ }
262
+ }
263
+ },
264
+ [tickSpacing, minOffset, maxOffset, fastSwipeTickThreshold]
265
+ );
266
+
267
+ // ── Pan Gesture ───────────────────────────────────────────────────────────
268
+ const panGesture = useMemo(() => {
269
+ return Gesture.Pan()
270
+ .onStart(() => {
271
+ contextX.value = translateX.value;
272
+ })
273
+ .onUpdate((event) => {
274
+ translateX.value = contextX.value + event.translationX;
275
+ })
276
+ .onEnd((event) => {
277
+ translateX.value = withDecay(
278
+ {
279
+ velocity: event.velocityX,
280
+ deceleration: 0.99,
281
+ },
282
+ (finished) => {
283
+ if (finished) {
284
+ const maxTranslateX = -minOffset * tickSpacing;
285
+ const minTranslateX = -maxOffset * tickSpacing;
286
+ const clampedX = Math.min(
287
+ Math.max(translateX.value, minTranslateX),
288
+ maxTranslateX
289
+ );
290
+ const snappedIndex = Math.round(clampedX / tickSpacing);
291
+ const snappedX = snappedIndex * tickSpacing;
292
+
293
+ translateX.value = withTiming(
294
+ snappedX,
295
+ { duration: 150 },
296
+ (done) => {
297
+ if (done) {
298
+ isFastMode.value = false;
299
+ const index = Math.round(-snappedX / tickSpacing);
300
+ runOnJS(onAnimationSettled)(index);
301
+ }
302
+ }
303
+ );
304
+ }
305
+ }
306
+ );
307
+ });
308
+ }, [contextX, translateX, minOffset, maxOffset, tickSpacing, onAnimationSettled, isFastMode]);
309
+
310
+ // ── Programmatic Control ──────────────────────────────────────────────────
311
+ const scrollToDate = useCallback(
312
+ (targetDate: Date | string, animated: boolean = true) => {
313
+ const d = normalizeDate(targetDate);
314
+ const offset = dateToDayOffset(d, todayTime);
315
+ const clampedOffset = Math.max(minOffset, Math.min(offset, maxOffset));
316
+ const targetX = -clampedOffset * tickSpacing;
317
+
318
+ stopContinuousHaptic();
319
+ if (animated) {
320
+ translateX.value = withTiming(targetX, { duration: 250 }, (done) => {
321
+ if (done) {
322
+ runOnJS(onAnimationSettled)(clampedOffset);
323
+ }
324
+ });
325
+ } else {
326
+ translateX.value = targetX;
327
+ onAnimationSettled(clampedOffset);
328
+ }
329
+ },
330
+ [todayTime, minOffset, maxOffset, tickSpacing, translateX, stopContinuousHaptic, onAnimationSettled]
331
+ );
332
+
333
+ const resetToToday = useCallback(
334
+ (animated: boolean = true) => {
335
+ scrollToDate(new Date(todayTime), animated);
336
+ },
337
+ [scrollToDate, todayTime]
338
+ );
339
+
340
+ // ── Sync with external controlled value ───────────────────────────────────
341
+ const prevValuePropRef = useRef(value);
342
+ useEffect(() => {
343
+ if (value !== undefined && value !== prevValuePropRef.current) {
344
+ prevValuePropRef.current = value;
345
+ const target = normalizeDate(value);
346
+ if (formatDateString(target) !== formatDateString(selectedDate)) {
347
+ scrollToDate(target, true);
348
+ }
349
+ }
350
+ }, [value, selectedDate, scrollToDate]);
351
+
352
+ const isTodaySelected = useMemo(() => {
353
+ return selectedDateStr === formatDateString(new Date(todayTime));
354
+ }, [selectedDateStr, todayTime]);
355
+
356
+ return {
357
+ translateX,
358
+ panGesture,
359
+ selectedDate,
360
+ selectedDateStr,
361
+ isTodaySelected,
362
+ visibleTicks,
363
+ theme,
364
+ tickSpacing,
365
+ rulerHeight,
366
+ height,
367
+ width,
368
+ minOffset,
369
+ maxOffset,
370
+ scrollToDate,
371
+ resetToToday,
372
+ };
373
+ }