react-native-ruler-date-picker 0.1.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/lib/commonjs/RulerDatePicker.js +376 -0
  4. package/lib/commonjs/RulerDatePicker.js.map +1 -0
  5. package/lib/commonjs/Tick.js +79 -0
  6. package/lib/commonjs/Tick.js.map +1 -0
  7. package/lib/commonjs/defaults.js +48 -0
  8. package/lib/commonjs/defaults.js.map +1 -0
  9. package/lib/commonjs/index.js +13 -0
  10. package/lib/commonjs/index.js.map +1 -0
  11. package/lib/commonjs/package.json +1 -0
  12. package/lib/commonjs/types.js +6 -0
  13. package/lib/commonjs/types.js.map +1 -0
  14. package/lib/commonjs/useHaptics.js +83 -0
  15. package/lib/commonjs/useHaptics.js.map +1 -0
  16. package/lib/commonjs/utils.js +99 -0
  17. package/lib/commonjs/utils.js.map +1 -0
  18. package/lib/module/RulerDatePicker.js +371 -0
  19. package/lib/module/RulerDatePicker.js.map +1 -0
  20. package/lib/module/Tick.js +74 -0
  21. package/lib/module/Tick.js.map +1 -0
  22. package/lib/module/defaults.js +44 -0
  23. package/lib/module/defaults.js.map +1 -0
  24. package/lib/module/index.js +4 -0
  25. package/lib/module/index.js.map +1 -0
  26. package/lib/module/package.json +1 -0
  27. package/lib/module/types.js +4 -0
  28. package/lib/module/types.js.map +1 -0
  29. package/lib/module/useHaptics.js +80 -0
  30. package/lib/module/useHaptics.js.map +1 -0
  31. package/lib/module/utils.js +90 -0
  32. package/lib/module/utils.js.map +1 -0
  33. package/lib/typescript/RulerDatePicker.d.ts +21 -0
  34. package/lib/typescript/RulerDatePicker.d.ts.map +1 -0
  35. package/lib/typescript/Tick.d.ts +26 -0
  36. package/lib/typescript/Tick.d.ts.map +1 -0
  37. package/lib/typescript/defaults.d.ts +24 -0
  38. package/lib/typescript/defaults.d.ts.map +1 -0
  39. package/lib/typescript/index.d.ts +3 -0
  40. package/lib/typescript/index.d.ts.map +1 -0
  41. package/lib/typescript/types.d.ts +104 -0
  42. package/lib/typescript/types.d.ts.map +1 -0
  43. package/lib/typescript/useHaptics.d.ts +20 -0
  44. package/lib/typescript/useHaptics.d.ts.map +1 -0
  45. package/lib/typescript/utils.d.ts +31 -0
  46. package/lib/typescript/utils.d.ts.map +1 -0
  47. package/package.json +106 -0
  48. package/src/RulerDatePicker.tsx +462 -0
  49. package/src/Tick.tsx +104 -0
  50. package/src/defaults.ts +44 -0
  51. package/src/index.ts +2 -0
  52. package/src/types.ts +129 -0
  53. package/src/useHaptics.ts +107 -0
  54. package/src/utils.ts +115 -0
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.MS_PER_DAY = exports.LIGHT_COLORS = exports.DEFAULT_DAY_RANGE = exports.DEFAULTS = exports.DARK_COLORS = void 0;
7
+ // ── Internal constants (not user-configurable) ───────────────────────────
8
+ const MS_PER_DAY = exports.MS_PER_DAY = 86_400_000;
9
+ const DEFAULT_DAY_RANGE = exports.DEFAULT_DAY_RANGE = 365;
10
+
11
+ // ── User-configurable defaults ───────────────────────────────────────────
12
+ const DEFAULTS = exports.DEFAULTS = {
13
+ tickSpacing: 24,
14
+ tickHeight: 12,
15
+ tickTallHeight: 24,
16
+ tickThickness: 4,
17
+ height: 120,
18
+ rulerHeight: 60,
19
+ indicatorWidth: 2,
20
+ indicatorHeight: 40,
21
+ todayDotSize: 6,
22
+ labelFontSize: 10,
23
+ bufferTicks: 10,
24
+ updateDebounceMs: 100,
25
+ fastSwipeTickThreshold: 1,
26
+ continuousHapticIntervalMs: 20,
27
+ enableHaptics: true,
28
+ showResetToToday: false
29
+ };
30
+
31
+ // ── Default color palettes ───────────────────────────────────────────────
32
+ const LIGHT_COLORS = exports.LIGHT_COLORS = {
33
+ tickColor: '#D1D5DB',
34
+ tickTallColor: '#9CA3AF',
35
+ labelColor: '#6B7280',
36
+ accentColor: '#3B82F6',
37
+ indicatorColor: '#3B82F6',
38
+ backgroundColor: 'transparent'
39
+ };
40
+ const DARK_COLORS = exports.DARK_COLORS = {
41
+ tickColor: '#4B5563',
42
+ tickTallColor: '#6B7280',
43
+ labelColor: '#9CA3AF',
44
+ accentColor: '#60A5FA',
45
+ indicatorColor: '#60A5FA',
46
+ backgroundColor: 'transparent'
47
+ };
48
+ //# sourceMappingURL=defaults.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["MS_PER_DAY","exports","DEFAULT_DAY_RANGE","DEFAULTS","tickSpacing","tickHeight","tickTallHeight","tickThickness","height","rulerHeight","indicatorWidth","indicatorHeight","todayDotSize","labelFontSize","bufferTicks","updateDebounceMs","fastSwipeTickThreshold","continuousHapticIntervalMs","enableHaptics","showResetToToday","LIGHT_COLORS","tickColor","tickTallColor","labelColor","accentColor","indicatorColor","backgroundColor","DARK_COLORS"],"sourceRoot":"../../src","sources":["defaults.ts"],"mappings":";;;;;;AAEA;AACO,MAAMA,UAAU,GAAAC,OAAA,CAAAD,UAAA,GAAG,UAAU;AAC7B,MAAME,iBAAiB,GAAAD,OAAA,CAAAC,iBAAA,GAAG,GAAG;;AAEpC;AACO,MAAMC,QAAQ,GAAAF,OAAA,CAAAE,QAAA,GAAG;EACtBC,WAAW,EAAE,EAAE;EACfC,UAAU,EAAE,EAAE;EACdC,cAAc,EAAE,EAAE;EAClBC,aAAa,EAAE,CAAC;EAChBC,MAAM,EAAE,GAAG;EACXC,WAAW,EAAE,EAAE;EACfC,cAAc,EAAE,CAAC;EACjBC,eAAe,EAAE,EAAE;EACnBC,YAAY,EAAE,CAAC;EACfC,aAAa,EAAE,EAAE;EACjBC,WAAW,EAAE,EAAE;EACfC,gBAAgB,EAAE,GAAG;EACrBC,sBAAsB,EAAE,CAAC;EACzBC,0BAA0B,EAAE,EAAE;EAC9BC,aAAa,EAAE,IAAI;EACnBC,gBAAgB,EAAE;AACpB,CAAU;;AAEV;AACO,MAAMC,YAA4B,GAAAnB,OAAA,CAAAmB,YAAA,GAAG;EAC1CC,SAAS,EAAE,SAAS;EACpBC,aAAa,EAAE,SAAS;EACxBC,UAAU,EAAE,SAAS;EACrBC,WAAW,EAAE,SAAS;EACtBC,cAAc,EAAE,SAAS;EACzBC,eAAe,EAAE;AACnB,CAAC;AAEM,MAAMC,WAA2B,GAAA1B,OAAA,CAAA0B,WAAA,GAAG;EACzCN,SAAS,EAAE,SAAS;EACpBC,aAAa,EAAE,SAAS;EACxBC,UAAU,EAAE,SAAS;EACrBC,WAAW,EAAE,SAAS;EACtBC,cAAc,EAAE,SAAS;EACzBC,eAAe,EAAE;AACnB,CAAC","ignoreList":[]}
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "RulerDatePicker", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _RulerDatePicker.RulerDatePicker;
10
+ }
11
+ });
12
+ var _RulerDatePicker = require("./RulerDatePicker");
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_RulerDatePicker","require"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;;;;;;;;;;AAAA,IAAAA,gBAAA,GAAAC,OAAA","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../src","sources":["types.ts"],"mappings":"","ignoreList":[]}
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.useHaptics = useHaptics;
7
+ var _react = require("react");
8
+ var _reactNative = require("react-native");
9
+ var _defaults = require("./defaults");
10
+ /**
11
+ * Attempts to dynamically require expo-haptics.
12
+ * Returns null if the module is not installed, allowing graceful degradation
13
+ * in pure React Native CLI projects.
14
+ */
15
+ let ExpoHaptics = null;
16
+ try {
17
+ ExpoHaptics = require('expo-haptics');
18
+ } catch {
19
+ // expo-haptics not installed — haptics will be silently disabled
20
+ }
21
+ /**
22
+ * Haptics abstraction hook.
23
+ *
24
+ * - If `onHapticTrigger` is provided, delegates all haptic calls to it.
25
+ * - Otherwise, uses expo-haptics if available.
26
+ * - Falls back to no-op if neither is available or haptics are disabled.
27
+ */
28
+ function useHaptics({
29
+ enableHaptics,
30
+ onHapticTrigger,
31
+ continuousHapticIntervalMs = _defaults.DEFAULTS.continuousHapticIntervalMs
32
+ }) {
33
+ const hapticIntervalRef = (0, _react.useRef)(null);
34
+
35
+ /** Fire a single discrete haptic — used in slow scroll mode */
36
+ const fireDiscreteHaptic = (0, _react.useCallback)(() => {
37
+ if (!enableHaptics) return;
38
+ if (onHapticTrigger) {
39
+ onHapticTrigger('tick');
40
+ return;
41
+ }
42
+ if (!ExpoHaptics) return;
43
+ if (_reactNative.Platform.OS === 'android') {
44
+ ExpoHaptics.impactAsync(ExpoHaptics.ImpactFeedbackStyle.Light).catch(() => {});
45
+ } else {
46
+ ExpoHaptics.selectionAsync().catch(() => {});
47
+ }
48
+ }, [enableHaptics, onHapticTrigger]);
49
+
50
+ /** Start a continuous haptic buzz — used in fast scroll mode */
51
+ const startContinuousHaptic = (0, _react.useCallback)(() => {
52
+ if (!enableHaptics) return;
53
+ if (onHapticTrigger) {
54
+ onHapticTrigger('continuous');
55
+ return;
56
+ }
57
+ if (!ExpoHaptics) return;
58
+
59
+ // Already running — don't double-start
60
+ if (hapticIntervalRef.current) return;
61
+
62
+ // Fire one immediately so the transition isn't silent
63
+ ExpoHaptics.impactAsync(ExpoHaptics.ImpactFeedbackStyle.Light).catch(() => {});
64
+ const interval = _reactNative.Platform.OS === 'android' ? 35 : continuousHapticIntervalMs;
65
+ hapticIntervalRef.current = setInterval(() => {
66
+ ExpoHaptics.impactAsync(ExpoHaptics.ImpactFeedbackStyle.Light).catch(() => {});
67
+ }, interval);
68
+ }, [enableHaptics, onHapticTrigger, continuousHapticIntervalMs]);
69
+
70
+ /** Stop the continuous haptic buzz */
71
+ const stopContinuousHaptic = (0, _react.useCallback)(() => {
72
+ if (hapticIntervalRef.current) {
73
+ clearInterval(hapticIntervalRef.current);
74
+ hapticIntervalRef.current = null;
75
+ }
76
+ }, []);
77
+ return {
78
+ fireDiscreteHaptic,
79
+ startContinuousHaptic,
80
+ stopContinuousHaptic
81
+ };
82
+ }
83
+ //# sourceMappingURL=useHaptics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_react","require","_reactNative","_defaults","ExpoHaptics","useHaptics","enableHaptics","onHapticTrigger","continuousHapticIntervalMs","DEFAULTS","hapticIntervalRef","useRef","fireDiscreteHaptic","useCallback","Platform","OS","impactAsync","ImpactFeedbackStyle","Light","catch","selectionAsync","startContinuousHaptic","current","interval","setInterval","stopContinuousHaptic","clearInterval"],"sourceRoot":"../../src","sources":["useHaptics.ts"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AACA,IAAAE,SAAA,GAAAF,OAAA;AAEA;AACA;AACA;AACA;AACA;AACA,IAAIG,WAAiD,GAAG,IAAI;AAC5D,IAAI;EACFA,WAAW,GAAGH,OAAO,CAAC,cAAc,CAAC;AACvC,CAAC,CAAC,MAAM;EACN;AAAA;AAeF;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASI,UAAUA,CAAC;EACzBC,aAAa;EACbC,eAAe;EACfC,0BAA0B,GAAGC,kBAAQ,CAACD;AACrB,CAAC,EAAc;EAChC,MAAME,iBAAiB,GAAG,IAAAC,aAAM,EAC9B,IACF,CAAC;;EAED;EACA,MAAMC,kBAAkB,GAAG,IAAAC,kBAAW,EAAC,MAAM;IAC3C,IAAI,CAACP,aAAa,EAAE;IAEpB,IAAIC,eAAe,EAAE;MACnBA,eAAe,CAAC,MAAM,CAAC;MACvB;IACF;IAEA,IAAI,CAACH,WAAW,EAAE;IAElB,IAAIU,qBAAQ,CAACC,EAAE,KAAK,SAAS,EAAE;MAC7BX,WAAW,CAACY,WAAW,CAACZ,WAAW,CAACa,mBAAmB,CAACC,KAAK,CAAC,CAACC,KAAK,CAClE,MAAM,CAAC,CACT,CAAC;IACH,CAAC,MAAM;MACLf,WAAW,CAACgB,cAAc,CAAC,CAAC,CAACD,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IAC9C;EACF,CAAC,EAAE,CAACb,aAAa,EAAEC,eAAe,CAAC,CAAC;;EAEpC;EACA,MAAMc,qBAAqB,GAAG,IAAAR,kBAAW,EAAC,MAAM;IAC9C,IAAI,CAACP,aAAa,EAAE;IAEpB,IAAIC,eAAe,EAAE;MACnBA,eAAe,CAAC,YAAY,CAAC;MAC7B;IACF;IAEA,IAAI,CAACH,WAAW,EAAE;;IAElB;IACA,IAAIM,iBAAiB,CAACY,OAAO,EAAE;;IAE/B;IACAlB,WAAW,CAACY,WAAW,CAACZ,WAAW,CAACa,mBAAmB,CAACC,KAAK,CAAC,CAACC,KAAK,CAClE,MAAM,CAAC,CACT,CAAC;IAED,MAAMI,QAAQ,GACZT,qBAAQ,CAACC,EAAE,KAAK,SAAS,GAAG,EAAE,GAAGP,0BAA0B;IAE7DE,iBAAiB,CAACY,OAAO,GAAGE,WAAW,CAAC,MAAM;MAC5CpB,WAAW,CAAEY,WAAW,CAACZ,WAAW,CAAEa,mBAAmB,CAACC,KAAK,CAAC,CAACC,KAAK,CACpE,MAAM,CAAC,CACT,CAAC;IACH,CAAC,EAAEI,QAAQ,CAAC;EACd,CAAC,EAAE,CAACjB,aAAa,EAAEC,eAAe,EAAEC,0BAA0B,CAAC,CAAC;;EAEhE;EACA,MAAMiB,oBAAoB,GAAG,IAAAZ,kBAAW,EAAC,MAAM;IAC7C,IAAIH,iBAAiB,CAACY,OAAO,EAAE;MAC7BI,aAAa,CAAChB,iBAAiB,CAACY,OAAO,CAAC;MACxCZ,iBAAiB,CAACY,OAAO,GAAG,IAAI;IAClC;EACF,CAAC,EAAE,EAAE,CAAC;EAEN,OAAO;IACLV,kBAAkB;IAClBS,qBAAqB;IACrBI;EACF,CAAC;AACH","ignoreList":[]}
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.computeDayRange = computeDayRange;
7
+ exports.dateToDayOffset = dateToDayOffset;
8
+ exports.dayOffsetToDate = dayOffsetToDate;
9
+ exports.formatDateString = formatDateString;
10
+ exports.shouldShowLabel = shouldShowLabel;
11
+ exports.useResolvedColors = useResolvedColors;
12
+ var _reactNative = require("react-native");
13
+ var _defaults = require("./defaults");
14
+ /**
15
+ * Formats a Date object as a local YYYY-MM-DD string,
16
+ * avoiding timezone shifting bugs caused by toISOString().
17
+ */
18
+ function formatDateString(date = new Date()) {
19
+ const year = date.getFullYear();
20
+ const month = String(date.getMonth() + 1).padStart(2, '0');
21
+ const day = String(date.getDate()).padStart(2, '0');
22
+ return `${year}-${month}-${day}`;
23
+ }
24
+
25
+ /**
26
+ * Converts a Date or YYYY-MM-DD string to a day offset relative to a reference time.
27
+ */
28
+ function dateToDayOffset(date, referenceTime) {
29
+ const d = typeof date === 'string' ? new Date(date) : new Date(date);
30
+ d.setHours(0, 0, 0, 0);
31
+ return Math.round((d.getTime() - referenceTime) / _defaults.MS_PER_DAY);
32
+ }
33
+
34
+ /**
35
+ * Converts a day offset back to a Date object.
36
+ */
37
+ function dayOffsetToDate(offset, referenceTime) {
38
+ return new Date(referenceTime + offset * _defaults.MS_PER_DAY);
39
+ }
40
+
41
+ /**
42
+ * Resolves user-provided color props against the system color scheme defaults.
43
+ * User-provided values always take precedence.
44
+ */
45
+ function useResolvedColors(props) {
46
+ const scheme = (0, _reactNative.useColorScheme)();
47
+ const palette = scheme === 'dark' ? _defaults.DARK_COLORS : _defaults.LIGHT_COLORS;
48
+ return {
49
+ tickColor: props.tickColor ?? palette.tickColor,
50
+ tickTallColor: props.tickTallColor ?? props.tickColor ?? palette.tickTallColor,
51
+ labelColor: props.labelColor ?? palette.labelColor,
52
+ accentColor: props.accentColor ?? palette.accentColor,
53
+ indicatorColor: props.indicatorColor ?? palette.indicatorColor,
54
+ backgroundColor: props.backgroundColor ?? palette.backgroundColor
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Determines whether a date should display a label based on the labelInterval setting.
60
+ */
61
+ function shouldShowLabel(date, labelInterval, todayTime) {
62
+ if (!labelInterval || labelInterval === 'weekly') {
63
+ // Default: every 7th day relative to today (matches original Friday-style spacing)
64
+ const dayOffset = Math.round((date.getTime() - todayTime) / _defaults.MS_PER_DAY);
65
+ return dayOffset % 7 === 0;
66
+ }
67
+ if (labelInterval === 'daily') {
68
+ return true;
69
+ }
70
+ if (labelInterval === 'monthly') {
71
+ return date.getDate() === 1;
72
+ }
73
+
74
+ // Custom callback
75
+ return labelInterval(date);
76
+ }
77
+
78
+ /**
79
+ * Computes the min/max day offsets from props or defaults.
80
+ */
81
+ function computeDayRange(todayTime, minDate, maxDate) {
82
+ let minOffset = -_defaults.DEFAULT_DAY_RANGE;
83
+ let maxOffset = _defaults.DEFAULT_DAY_RANGE;
84
+ if (minDate) {
85
+ const d = new Date(minDate);
86
+ d.setHours(0, 0, 0, 0);
87
+ minOffset = Math.round((d.getTime() - todayTime) / _defaults.MS_PER_DAY);
88
+ }
89
+ if (maxDate) {
90
+ const d = new Date(maxDate);
91
+ d.setHours(0, 0, 0, 0);
92
+ maxOffset = Math.round((d.getTime() - todayTime) / _defaults.MS_PER_DAY);
93
+ }
94
+ return {
95
+ minOffset,
96
+ maxOffset
97
+ };
98
+ }
99
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_reactNative","require","_defaults","formatDateString","date","Date","year","getFullYear","month","String","getMonth","padStart","day","getDate","dateToDayOffset","referenceTime","d","setHours","Math","round","getTime","MS_PER_DAY","dayOffsetToDate","offset","useResolvedColors","props","scheme","useColorScheme","palette","DARK_COLORS","LIGHT_COLORS","tickColor","tickTallColor","labelColor","accentColor","indicatorColor","backgroundColor","shouldShowLabel","labelInterval","todayTime","dayOffset","computeDayRange","minDate","maxDate","minOffset","DEFAULT_DAY_RANGE","maxOffset"],"sourceRoot":"../../src","sources":["utils.ts"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,371 @@
1
+ "use strict";
2
+
3
+ import React, { useState, useRef, useCallback, useMemo } from 'react';
4
+ import { View, Text, StyleSheet, Dimensions, TouchableOpacity } from 'react-native';
5
+ import Animated, { useSharedValue, useAnimatedStyle, withDecay, withTiming, useAnimatedReaction, runOnJS } from 'react-native-reanimated';
6
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
7
+ import { DEFAULTS, MS_PER_DAY } from './defaults';
8
+ import { formatDateString, dateToDayOffset, dayOffsetToDate, useResolvedColors, shouldShowLabel, computeDayRange } from './utils';
9
+ import { useHaptics } from './useHaptics';
10
+ import { Tick } from './Tick';
11
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
+ const SCREEN_WIDTH = Dimensions.get('window').width;
13
+
14
+ /**
15
+ * A performant, customizable ruler-style date picker for React Native.
16
+ *
17
+ * Features:
18
+ * - 60 FPS scrolling via Reanimated 3 decay physics + snap alignment
19
+ * - Dual-speed haptic feedback engine (discrete ticks vs continuous buzz)
20
+ * - Windowed tick rendering for memory efficiency
21
+ * - Fully configurable dimensions, colors, and behaviors
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * <RulerDatePicker
26
+ * selectedDate={new Date()}
27
+ * onSelectDate={(date, dateString) => console.log(dateString)}
28
+ * />
29
+ * ```
30
+ */
31
+ export function RulerDatePicker(props) {
32
+ const {
33
+ selectedDate,
34
+ onSelectDate,
35
+ minDate,
36
+ maxDate,
37
+ labelInterval,
38
+ highlightDays,
39
+ tickSpacing = DEFAULTS.tickSpacing,
40
+ tickHeight = DEFAULTS.tickHeight,
41
+ tickTallHeight = DEFAULTS.tickTallHeight,
42
+ tickThickness = DEFAULTS.tickThickness,
43
+ height = DEFAULTS.height,
44
+ rulerHeight = DEFAULTS.rulerHeight,
45
+ indicatorWidth = DEFAULTS.indicatorWidth,
46
+ indicatorHeight = DEFAULTS.indicatorHeight,
47
+ todayDotSize = DEFAULTS.todayDotSize,
48
+ labelFontSize = DEFAULTS.labelFontSize,
49
+ bufferTicks = DEFAULTS.bufferTicks,
50
+ updateDebounceMs = DEFAULTS.updateDebounceMs,
51
+ enableHaptics = DEFAULTS.enableHaptics,
52
+ onHapticTrigger,
53
+ fastSwipeTickThreshold = DEFAULTS.fastSwipeTickThreshold,
54
+ continuousHapticIntervalMs = DEFAULTS.continuousHapticIntervalMs,
55
+ showResetToToday = DEFAULTS.showResetToToday,
56
+ renderHeader
57
+ } = props;
58
+
59
+ // ── Resolve colors ──────────────────────────────────────────────────────
60
+ const colors = useResolvedColors(props);
61
+
62
+ // ── Haptics ─────────────────────────────────────────────────────────────
63
+ const {
64
+ fireDiscreteHaptic,
65
+ startContinuousHaptic,
66
+ stopContinuousHaptic
67
+ } = useHaptics({
68
+ enableHaptics,
69
+ onHapticTrigger,
70
+ continuousHapticIntervalMs
71
+ });
72
+
73
+ // ── Reference time (midnight today) ─────────────────────────────────────
74
+ const todayTime = useMemo(() => {
75
+ const d = new Date();
76
+ d.setHours(0, 0, 0, 0);
77
+ return d.getTime();
78
+ }, []);
79
+
80
+ // ── Date boundaries ─────────────────────────────────────────────────────
81
+ const {
82
+ minOffset,
83
+ maxOffset
84
+ } = useMemo(() => computeDayRange(todayTime, minDate, maxDate), [todayTime, minDate, maxDate]);
85
+
86
+ // ── Initial offset from today ───────────────────────────────────────────
87
+ const initialOffsetDays = useMemo(() => {
88
+ return dateToDayOffset(selectedDate, todayTime);
89
+ // Intentionally only computed once on mount
90
+ // eslint-disable-next-line react-hooks/exhaustive-deps
91
+ }, []);
92
+ const halfVisible = Math.ceil(SCREEN_WIDTH / 2 / tickSpacing);
93
+ const windowSize = halfVisible + bufferTicks;
94
+
95
+ // ── Animated values ─────────────────────────────────────────────────────
96
+ const translateX = useSharedValue(-initialOffsetDays * tickSpacing);
97
+ const contextX = useSharedValue(0);
98
+ const previousIndex = useSharedValue(-initialOffsetDays);
99
+ const isFastMode = useSharedValue(false);
100
+
101
+ // ── State ───────────────────────────────────────────────────────────────
102
+ const initialDateStr = useMemo(() => {
103
+ const d = typeof selectedDate === 'string' ? new Date(selectedDate) : new Date(selectedDate);
104
+ d.setHours(0, 0, 0, 0);
105
+ return formatDateString(d);
106
+ // eslint-disable-next-line react-hooks/exhaustive-deps
107
+ }, []);
108
+ const [displayDateStr, setDisplayDateStr] = useState(initialDateStr);
109
+ const debounceTimerRef = useRef(null);
110
+ const [windowCenter, setWindowCenter] = useState(-initialOffsetDays);
111
+
112
+ // ── Visible ticks (windowed rendering) ──────────────────────────────────
113
+ const visibleTicks = useMemo(() => {
114
+ const ticks = [];
115
+ const start = Math.max(windowCenter - windowSize, minOffset);
116
+ const end = Math.min(windowCenter + windowSize, maxOffset);
117
+ for (let i = start; i <= end; i++) {
118
+ const d = new Date(todayTime + i * MS_PER_DAY);
119
+ ticks.push({
120
+ offset: i,
121
+ isLabeled: shouldShowLabel(d, labelInterval, todayTime),
122
+ isToday: i === 0,
123
+ isHighlighted: highlightDays ? highlightDays.includes(d.getDay()) : false,
124
+ dateStr: d.getDate().toString()
125
+ });
126
+ }
127
+ return ticks;
128
+ }, [windowCenter, windowSize, todayTime, minOffset, maxOffset, labelInterval, highlightDays]);
129
+
130
+ // ── Tick change callbacks ───────────────────────────────────────────────
131
+
132
+ const onTickChanged = useCallback((index, enteringFastMode) => {
133
+ if (enteringFastMode) {
134
+ startContinuousHaptic();
135
+ } else {
136
+ stopContinuousHaptic();
137
+ fireDiscreteHaptic();
138
+ }
139
+
140
+ // Update display date
141
+ const d = dayOffsetToDate(index, todayTime);
142
+ const dateStr = formatDateString(d);
143
+ setWindowCenter(prevCenter => {
144
+ if (Math.abs(index - prevCenter) >= 5) {
145
+ return index;
146
+ }
147
+ return prevCenter;
148
+ });
149
+
150
+ // Debounce the heavy onSelectDate callback
151
+ if (debounceTimerRef.current) {
152
+ clearTimeout(debounceTimerRef.current);
153
+ }
154
+ debounceTimerRef.current = setTimeout(() => {
155
+ onSelectDate(d, dateStr);
156
+ }, updateDebounceMs);
157
+ }, [todayTime, onSelectDate, updateDebounceMs, startContinuousHaptic, stopContinuousHaptic, fireDiscreteHaptic]);
158
+ const onAnimationSettled = useCallback(index => {
159
+ stopContinuousHaptic();
160
+ fireDiscreteHaptic();
161
+ const d = dayOffsetToDate(index, todayTime);
162
+ const dateStr = formatDateString(d);
163
+ setDisplayDateStr(dateStr);
164
+ setWindowCenter(index);
165
+ onSelectDate(d, dateStr);
166
+ }, [todayTime, onSelectDate, stopContinuousHaptic, fireDiscreteHaptic]);
167
+
168
+ // ── Animated reaction ───────────────────────────────────────────────────
169
+ useAnimatedReaction(() => Math.round(-translateX.value / tickSpacing), (index, prev) => {
170
+ if (index !== prev && index >= minOffset && index <= maxOffset) {
171
+ previousIndex.value = index;
172
+ const ticksSkipped = prev !== null ? Math.abs(index - prev) : 1;
173
+ const shouldBeFast = ticksSkipped > fastSwipeTickThreshold;
174
+ if (shouldBeFast && !isFastMode.value) {
175
+ isFastMode.value = true;
176
+ runOnJS(onTickChanged)(index, true);
177
+ } else if (!shouldBeFast && isFastMode.value) {
178
+ isFastMode.value = false;
179
+ runOnJS(onTickChanged)(index, false);
180
+ } else if (!shouldBeFast) {
181
+ runOnJS(onTickChanged)(index, false);
182
+ }
183
+ if (shouldBeFast && isFastMode.value) {
184
+ runOnJS(onTickChanged)(index, true);
185
+ }
186
+ }
187
+ }, [tickSpacing, minOffset, maxOffset, fastSwipeTickThreshold]);
188
+
189
+ // ── Pan gesture ─────────────────────────────────────────────────────────
190
+ const pan = Gesture.Pan().onStart(() => {
191
+ contextX.value = translateX.value;
192
+ }).onUpdate(event => {
193
+ translateX.value = contextX.value + event.translationX;
194
+ }).onEnd(event => {
195
+ translateX.value = withDecay({
196
+ velocity: event.velocityX,
197
+ deceleration: 0.99
198
+ }, finished => {
199
+ if (finished) {
200
+ const maxTranslateX = -minOffset * tickSpacing;
201
+ const minTranslateX = -maxOffset * tickSpacing;
202
+ const clampedX = Math.min(Math.max(translateX.value, minTranslateX), maxTranslateX);
203
+ const snappedIndex = Math.round(clampedX / tickSpacing);
204
+ const snappedX = snappedIndex * tickSpacing;
205
+ translateX.value = withTiming(snappedX, {
206
+ duration: 150
207
+ }, done => {
208
+ if (done) {
209
+ isFastMode.value = false;
210
+ const index = Math.round(-snappedX / tickSpacing);
211
+ runOnJS(onAnimationSettled)(index);
212
+ }
213
+ });
214
+ }
215
+ });
216
+ });
217
+ const animatedStyle = useAnimatedStyle(() => ({
218
+ transform: [{
219
+ translateX: translateX.value
220
+ }]
221
+ }), [translateX]);
222
+
223
+ // ── Header ──────────────────────────────────────────────────────────────
224
+ const displayDate = new Date(displayDateStr);
225
+ const displayFormat = displayDate.toLocaleDateString('en-US', {
226
+ weekday: 'long',
227
+ month: 'long',
228
+ day: 'numeric'
229
+ });
230
+ const displayYear = displayDate.getFullYear();
231
+ const currentYear = new Date().getFullYear();
232
+ const showYear = displayYear !== currentYear;
233
+ const isTodaySelected = displayDateStr === formatDateString(new Date(todayTime));
234
+ const handleResetToToday = useCallback(() => {
235
+ stopContinuousHaptic();
236
+ translateX.value = withTiming(0, {
237
+ duration: 300
238
+ });
239
+ const todayStr = formatDateString(new Date(todayTime));
240
+ setDisplayDateStr(todayStr);
241
+ setWindowCenter(0);
242
+ onSelectDate(new Date(todayTime), todayStr);
243
+ }, [todayTime, onSelectDate, stopContinuousHaptic, translateX]);
244
+
245
+ // ── Render ──────────────────────────────────────────────────────────────
246
+ return /*#__PURE__*/_jsxs(View, {
247
+ style: [styles.container, {
248
+ height,
249
+ backgroundColor: colors.backgroundColor
250
+ }],
251
+ children: [renderHeader ? renderHeader(displayDate, isTodaySelected, handleResetToToday) : /*#__PURE__*/_jsxs(View, {
252
+ style: styles.headerContainer,
253
+ children: [/*#__PURE__*/_jsxs(View, {
254
+ style: styles.dateHeader,
255
+ children: [/*#__PURE__*/_jsx(Text, {
256
+ style: [styles.yearText, {
257
+ color: colors.labelColor
258
+ }],
259
+ children: showYear ? displayYear : ''
260
+ }), /*#__PURE__*/_jsx(Text, {
261
+ style: [styles.dateText, {
262
+ color: colors.accentColor
263
+ }],
264
+ children: displayFormat
265
+ })]
266
+ }), showResetToToday && !isTodaySelected && /*#__PURE__*/_jsx(TouchableOpacity, {
267
+ onPress: handleResetToToday,
268
+ style: styles.resetBtn,
269
+ children: /*#__PURE__*/_jsx(Text, {
270
+ style: [styles.resetText, {
271
+ color: colors.accentColor
272
+ }],
273
+ children: "Today"
274
+ })
275
+ })]
276
+ }), /*#__PURE__*/_jsx(GestureDetector, {
277
+ gesture: pan,
278
+ children: /*#__PURE__*/_jsxs(View, {
279
+ style: [styles.rulerContainer, {
280
+ height: rulerHeight
281
+ }],
282
+ children: [/*#__PURE__*/_jsx(View, {
283
+ style: [styles.centerIndicator, {
284
+ backgroundColor: colors.indicatorColor,
285
+ width: indicatorWidth,
286
+ height: indicatorHeight
287
+ }]
288
+ }), /*#__PURE__*/_jsx(Animated.View, {
289
+ style: [styles.ticksWrapper, animatedStyle],
290
+ children: visibleTicks.map(tick => /*#__PURE__*/_jsx(Tick, {
291
+ offset: tick.offset,
292
+ isLabeled: tick.isLabeled,
293
+ isToday: tick.isToday,
294
+ isHighlighted: tick.isHighlighted,
295
+ dateStr: tick.dateStr,
296
+ tickSpacing: tickSpacing,
297
+ tickHeight: tickHeight,
298
+ tickTallHeight: tickTallHeight,
299
+ tickThickness: tickThickness,
300
+ tickColor: colors.tickColor,
301
+ tickTallColor: colors.tickTallColor,
302
+ labelColor: colors.labelColor,
303
+ accentColor: colors.accentColor,
304
+ todayDotSize: todayDotSize,
305
+ labelFontSize: labelFontSize,
306
+ rulerHeight: rulerHeight
307
+ }, tick.offset))
308
+ })]
309
+ })
310
+ })]
311
+ });
312
+ }
313
+ const styles = StyleSheet.create({
314
+ container: {
315
+ justifyContent: 'center',
316
+ alignItems: 'center',
317
+ overflow: 'hidden'
318
+ },
319
+ headerContainer: {
320
+ flexDirection: 'row',
321
+ alignItems: 'center',
322
+ justifyContent: 'center',
323
+ width: '100%',
324
+ paddingHorizontal: 20,
325
+ marginBottom: 20,
326
+ height: 40
327
+ },
328
+ dateHeader: {
329
+ alignItems: 'center'
330
+ },
331
+ yearText: {
332
+ fontSize: 12,
333
+ fontWeight: '500',
334
+ marginBottom: 2
335
+ },
336
+ dateText: {
337
+ fontSize: 18,
338
+ fontWeight: '600'
339
+ },
340
+ resetBtn: {
341
+ position: 'absolute',
342
+ right: 20,
343
+ paddingVertical: 6,
344
+ paddingHorizontal: 12,
345
+ borderRadius: 16,
346
+ backgroundColor: 'rgba(0,0,0,0.05)'
347
+ },
348
+ resetText: {
349
+ fontSize: 14,
350
+ fontWeight: '600'
351
+ },
352
+ rulerContainer: {
353
+ width: SCREEN_WIDTH,
354
+ position: 'relative',
355
+ flexDirection: 'row',
356
+ alignItems: 'flex-end',
357
+ justifyContent: 'center'
358
+ },
359
+ centerIndicator: {
360
+ position: 'absolute',
361
+ zIndex: 10,
362
+ bottom: 0
363
+ },
364
+ ticksWrapper: {
365
+ position: 'absolute',
366
+ height: '100%',
367
+ width: 0,
368
+ left: SCREEN_WIDTH / 2
369
+ }
370
+ });
371
+ //# sourceMappingURL=RulerDatePicker.js.map