@utilitywarehouse/hearth-react-native 0.24.0 → 0.26.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 (90) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-lint.log +13 -13
  3. package/CHANGELOG.md +72 -0
  4. package/build/components/DatePicker/DatePickerCalendar.js +4 -9
  5. package/build/components/Modal/Modal.d.ts +1 -1
  6. package/build/components/Modal/Modal.js +30 -7
  7. package/build/components/Modal/Modal.props.d.ts +4 -2
  8. package/build/components/TimePicker/TimePicker.d.ts +6 -0
  9. package/build/components/TimePicker/TimePicker.js +78 -0
  10. package/build/components/TimePicker/TimePicker.props.d.ts +45 -0
  11. package/build/components/TimePicker/TimePicker.props.js +1 -0
  12. package/build/components/TimePicker/TimePickerView.d.ts +12 -0
  13. package/build/components/TimePicker/TimePickerView.js +130 -0
  14. package/build/components/TimePicker/TimePickerWheel.d.ts +8 -0
  15. package/build/components/TimePicker/TimePickerWheel.js +78 -0
  16. package/build/components/{DatePicker/time-picker/wheel-web.d.ts → TimePicker/TimePickerWheel.web.d.ts} +4 -4
  17. package/build/components/TimePicker/TimePickerWheel.web.js +122 -0
  18. package/build/components/TimePicker/index.d.ts +6 -0
  19. package/build/components/TimePicker/index.js +3 -0
  20. package/build/components/TimePickerInput/TimePickerInput.d.ts +6 -0
  21. package/build/components/TimePickerInput/TimePickerInput.js +127 -0
  22. package/build/components/TimePickerInput/TimePickerInput.props.d.ts +52 -0
  23. package/build/components/TimePickerInput/TimePickerInput.props.js +1 -0
  24. package/build/components/TimePickerInput/TimePickerInputDoneButton.d.ts +8 -0
  25. package/build/components/TimePickerInput/TimePickerInputDoneButton.js +19 -0
  26. package/build/components/TimePickerInput/TimePickerInputDoneButton.web.d.ts +5 -0
  27. package/build/components/TimePickerInput/TimePickerInputDoneButton.web.js +5 -0
  28. package/build/components/TimePickerInput/index.d.ts +2 -0
  29. package/build/components/TimePickerInput/index.js +1 -0
  30. package/build/components/index.d.ts +2 -0
  31. package/build/components/index.js +2 -0
  32. package/docs/components/AllComponents.web.tsx +30 -0
  33. package/package.json +3 -2
  34. package/src/components/DatePicker/DatePickerCalendar.tsx +30 -13
  35. package/src/components/Modal/Modal.docs.mdx +9 -3
  36. package/src/components/Modal/Modal.props.ts +4 -2
  37. package/src/components/Modal/Modal.tsx +44 -7
  38. package/src/components/TimePicker/TimePicker.docs.mdx +84 -0
  39. package/src/components/TimePicker/TimePicker.figma.tsx +29 -0
  40. package/src/components/TimePicker/TimePicker.props.ts +45 -0
  41. package/src/components/TimePicker/TimePicker.stories.tsx +85 -0
  42. package/src/components/TimePicker/TimePicker.tsx +150 -0
  43. package/src/components/TimePicker/TimePickerView.tsx +216 -0
  44. package/src/components/TimePicker/TimePickerWheel.tsx +154 -0
  45. package/src/components/TimePicker/TimePickerWheel.web.tsx +217 -0
  46. package/src/components/TimePicker/index.ts +8 -0
  47. package/src/components/TimePickerInput/TimePickerInput.docs.mdx +135 -0
  48. package/src/components/TimePickerInput/TimePickerInput.figma.tsx +34 -0
  49. package/src/components/TimePickerInput/TimePickerInput.props.ts +55 -0
  50. package/src/components/TimePickerInput/TimePickerInput.stories.tsx +175 -0
  51. package/src/components/TimePickerInput/TimePickerInput.tsx +283 -0
  52. package/src/components/TimePickerInput/TimePickerInputDoneButton.tsx +42 -0
  53. package/src/components/TimePickerInput/TimePickerInputDoneButton.web.tsx +7 -0
  54. package/src/components/TimePickerInput/index.ts +2 -0
  55. package/src/components/index.ts +2 -0
  56. package/build/components/DatePicker/TimePicker.d.ts +0 -3
  57. package/build/components/DatePicker/TimePicker.js +0 -84
  58. package/build/components/DatePicker/time-picker/animated-math.d.ts +0 -4
  59. package/build/components/DatePicker/time-picker/animated-math.js +0 -19
  60. package/build/components/DatePicker/time-picker/period-native.d.ts +0 -6
  61. package/build/components/DatePicker/time-picker/period-native.js +0 -17
  62. package/build/components/DatePicker/time-picker/period-picker.d.ts +0 -6
  63. package/build/components/DatePicker/time-picker/period-picker.js +0 -10
  64. package/build/components/DatePicker/time-picker/period-web.d.ts +0 -6
  65. package/build/components/DatePicker/time-picker/period-web.js +0 -21
  66. package/build/components/DatePicker/time-picker/wheel-native.d.ts +0 -8
  67. package/build/components/DatePicker/time-picker/wheel-native.js +0 -19
  68. package/build/components/DatePicker/time-picker/wheel-picker/index.d.ts +0 -2
  69. package/build/components/DatePicker/time-picker/wheel-picker/index.js +0 -2
  70. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.d.ts +0 -16
  71. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.js +0 -97
  72. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.d.ts +0 -21
  73. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.js +0 -88
  74. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.d.ts +0 -23
  75. package/build/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.js +0 -21
  76. package/build/components/DatePicker/time-picker/wheel-web.js +0 -146
  77. package/build/components/DatePicker/time-picker/wheel.d.ts +0 -8
  78. package/build/components/DatePicker/time-picker/wheel.js +0 -10
  79. package/src/components/DatePicker/TimePicker.tsx +0 -141
  80. package/src/components/DatePicker/time-picker/animated-math.ts +0 -33
  81. package/src/components/DatePicker/time-picker/period-native.tsx +0 -34
  82. package/src/components/DatePicker/time-picker/period-picker.tsx +0 -16
  83. package/src/components/DatePicker/time-picker/period-web.tsx +0 -36
  84. package/src/components/DatePicker/time-picker/wheel-native.tsx +0 -37
  85. package/src/components/DatePicker/time-picker/wheel-picker/index.ts +0 -3
  86. package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker-item.tsx +0 -132
  87. package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker.style.ts +0 -22
  88. package/src/components/DatePicker/time-picker/wheel-picker/wheel-picker.tsx +0 -200
  89. package/src/components/DatePicker/time-picker/wheel-web.tsx +0 -180
  90. package/src/components/DatePicker/time-picker/wheel.tsx +0 -18
@@ -0,0 +1,216 @@
1
+ import dayjs from 'dayjs';
2
+ import { memo, useCallback, useMemo } from 'react';
3
+ import { View } from 'react-native';
4
+ import { StyleSheet } from 'react-native-unistyles';
5
+ import { BodyText } from '../BodyText';
6
+ import { Numerals } from '../DatePicker/DatePicker.props';
7
+ import { formatNumber, getParsedDate } from '../DatePicker/utils';
8
+ import type { DateType, PickerOption } from './TimePicker.props';
9
+ import TimePickerWheel from './TimePickerWheel';
10
+
11
+ export type Period = 'AM' | 'PM';
12
+
13
+ type TimePickerViewProps = {
14
+ currentDate: DateType;
15
+ onSelectDate: (date: DateType) => void;
16
+ timeZone?: string;
17
+ use12Hours?: boolean;
18
+ minuteInterval?: number;
19
+ containerHeight?: number;
20
+ };
21
+
22
+ const createNumberList = (
23
+ num: number,
24
+ numerals: Numerals,
25
+ startFrom: number = 0
26
+ ): PickerOption[] => {
27
+ return Array.from({ length: num }, (_, i) => ({
28
+ value: i + startFrom,
29
+ text:
30
+ i + startFrom < 10
31
+ ? `${formatNumber(0, numerals)}${formatNumber(i + startFrom, numerals)}`
32
+ : `${formatNumber(i + startFrom, numerals)}`,
33
+ }));
34
+ };
35
+
36
+ const createMinuteList = (interval: number, numerals: Numerals): PickerOption[] => {
37
+ const safeInterval = Math.min(59, Math.max(1, Math.floor(interval)));
38
+ const values = Array.from({ length: Math.ceil(60 / safeInterval) }, (_, index) =>
39
+ Math.min(index * safeInterval, 59)
40
+ ).filter((value, index, array) => array.indexOf(value) === index && value < 60);
41
+
42
+ return values.map(value => ({
43
+ value,
44
+ text:
45
+ value < 10
46
+ ? `${formatNumber(0, numerals)}${formatNumber(value, numerals)}`
47
+ : `${formatNumber(value, numerals)}`,
48
+ }));
49
+ };
50
+
51
+ const getClosestMinute = (value: number, options: PickerOption[]) => {
52
+ if (!options.length) return value;
53
+ const values = options.map(option => option.value as number);
54
+ if (values.includes(value)) return value;
55
+
56
+ let closest = values[0] ?? value;
57
+ let closestDiff = Math.abs(value - closest);
58
+
59
+ values.forEach(optionValue => {
60
+ const diff = Math.abs(value - optionValue);
61
+ if (diff < closestDiff) {
62
+ closestDiff = diff;
63
+ closest = optionValue;
64
+ }
65
+ });
66
+
67
+ return closest;
68
+ };
69
+
70
+ const TimePickerView = ({
71
+ currentDate,
72
+ onSelectDate,
73
+ timeZone,
74
+ use12Hours,
75
+ minuteInterval = 1,
76
+ }: TimePickerViewProps) => {
77
+ const hours = useMemo(
78
+ () => createNumberList(use12Hours ? 12 : 24, 'latn', use12Hours ? 1 : 0),
79
+ [use12Hours]
80
+ );
81
+
82
+ const minutes = useMemo(() => createMinuteList(minuteInterval, 'latn'), [minuteInterval]);
83
+
84
+ const periodOptions = useMemo(
85
+ () => [
86
+ { value: 'AM', text: 'AM' },
87
+ { value: 'PM', text: 'PM' },
88
+ ],
89
+ []
90
+ );
91
+
92
+ const baseDate = currentDate;
93
+ const { hour, hour12, minute, period } = getParsedDate(baseDate);
94
+ const minuteValue = useMemo(() => getClosestMinute(minute, minutes), [minute, minutes]);
95
+
96
+ const handleChangeHour = useCallback(
97
+ (value: number) => {
98
+ let hour24 = value;
99
+
100
+ if (use12Hours) {
101
+ if (period === 'AM' && value === 12) {
102
+ hour24 = 0;
103
+ } else if (period === 'PM' && value < 12) {
104
+ hour24 = value + 12;
105
+ } else {
106
+ hour24 = value;
107
+ }
108
+ }
109
+
110
+ const newDate = dayjs.tz(baseDate, timeZone).hour(hour24).minute(minuteValue);
111
+ onSelectDate(newDate);
112
+ },
113
+ [baseDate, onSelectDate, timeZone, use12Hours, period, minuteValue]
114
+ );
115
+
116
+ const handleChangeMinute = useCallback(
117
+ (value: number) => {
118
+ const newDate = dayjs.tz(baseDate, timeZone).minute(value);
119
+ onSelectDate(newDate);
120
+ },
121
+ [baseDate, onSelectDate, timeZone]
122
+ );
123
+
124
+ const handlePeriodChange = useCallback(
125
+ (newPeriod: Period) => {
126
+ let newHour = hour12;
127
+ if (newPeriod === 'PM' && hour12 < 12) {
128
+ newHour = hour12 + 12;
129
+ } else if (newPeriod === 'AM' && hour12 === 12) {
130
+ newHour = 0;
131
+ } else if (newPeriod === 'AM' && hour >= 12) {
132
+ newHour = hour12;
133
+ }
134
+
135
+ const newDate = dayjs.tz(baseDate, timeZone).hour(newHour);
136
+ onSelectDate(newDate);
137
+ },
138
+ [baseDate, onSelectDate, timeZone, hour, hour12]
139
+ );
140
+
141
+ return (
142
+ <View style={styles.container} testID="time-selector">
143
+ <View style={styles.timePickerContainer}>
144
+ <View style={styles.wheelContainer}>
145
+ <TimePickerWheel
146
+ value={use12Hours ? hour12 : hour}
147
+ items={hours}
148
+ setValue={handleChangeHour}
149
+ />
150
+ </View>
151
+ <BodyText style={styles.timeSeparator} size="lg">
152
+ :
153
+ </BodyText>
154
+ <View style={styles.wheelContainer}>
155
+ <TimePickerWheel value={minuteValue} items={minutes} setValue={handleChangeMinute} />
156
+ </View>
157
+ </View>
158
+ {use12Hours && period ? (
159
+ <View style={styles.periodContainer}>
160
+ <TimePickerWheel value={period} items={periodOptions} setValue={handlePeriodChange} />
161
+ </View>
162
+ ) : null}
163
+ </View>
164
+ );
165
+ };
166
+
167
+ const styles = StyleSheet.create({
168
+ container: {
169
+ flex: 1,
170
+ alignItems: 'center',
171
+ justifyContent: 'center',
172
+ flexDirection: 'row',
173
+ },
174
+ wheelContainer: {
175
+ flex: 1,
176
+ },
177
+ timePickerContainer: {
178
+ alignItems: 'center',
179
+ justifyContent: 'center',
180
+ width: 146,
181
+ height: 208,
182
+ flexDirection: 'row',
183
+ marginBottom: -30,
184
+ },
185
+ timeSeparator: {
186
+ marginHorizontal: 5,
187
+ },
188
+ periodContainer: {
189
+ marginLeft: 10,
190
+ },
191
+ });
192
+
193
+ const customComparator = (
194
+ prev: Readonly<TimePickerViewProps>,
195
+ next: Readonly<TimePickerViewProps>
196
+ ) => {
197
+ if (prev.onSelectDate !== next.onSelectDate) {
198
+ return false;
199
+ }
200
+
201
+ if (prev.timeZone !== next.timeZone) {
202
+ return false;
203
+ }
204
+
205
+ if (prev.use12Hours !== next.use12Hours) {
206
+ return false;
207
+ }
208
+
209
+ if (prev.minuteInterval !== next.minuteInterval) {
210
+ return false;
211
+ }
212
+
213
+ return dayjs(prev.currentDate).isSame(next.currentDate, 'minute');
214
+ };
215
+
216
+ export default memo(TimePickerView, customComparator);
@@ -0,0 +1,154 @@
1
+ import WheelPicker from '@quidone/react-native-wheel-picker';
2
+ import { useCallback, useMemo } from 'react';
3
+ import { View } from 'react-native';
4
+ import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
5
+ import { StyleSheet } from 'react-native-unistyles';
6
+ import { useTheme } from '../../hooks';
7
+ import { BodyText } from '../BodyText';
8
+ import type { PickerOption } from './TimePicker.props';
9
+
10
+ type TimePickerWheelProps = {
11
+ value: number | string;
12
+ setValue?: (value: any) => void;
13
+ items: PickerOption[];
14
+ };
15
+
16
+ const ITEM_HEIGHT = 40;
17
+ const VISIBLE_REST = 3;
18
+
19
+ const TimePickerWheel = ({ value, setValue = () => {}, items }: TimePickerWheelProps) => {
20
+ const theme = useTheme();
21
+ const fadeHeight = ITEM_HEIGHT * 1.5;
22
+ const gradientId = useMemo(() => `wheel-fade-${Math.random().toString(36).slice(2)}`, []);
23
+ const displayCount = VISIBLE_REST * 2 + 1;
24
+ const pickerHeight = ITEM_HEIGHT * displayCount;
25
+
26
+ const data = useMemo(
27
+ () =>
28
+ items.map(item => ({
29
+ value: item.value,
30
+ label: item.text,
31
+ })),
32
+ [items]
33
+ );
34
+
35
+ const handleValueChanged = useCallback(
36
+ ({ item }: { item: { value: number | string } }) => {
37
+ if (item?.value === value) {
38
+ return;
39
+ }
40
+ if (item && item.value !== undefined) {
41
+ setValue(item.value);
42
+ }
43
+ },
44
+ [setValue, value]
45
+ );
46
+
47
+ const renderOverlay = useCallback(
48
+ () => (
49
+ <View style={[styles.overlayContainer]} pointerEvents="none">
50
+ <View pointerEvents="none" style={[styles.fadeOverlay, { height: fadeHeight }]}>
51
+ <Svg width="100%" height="100%" preserveAspectRatio="none">
52
+ <Defs>
53
+ <LinearGradient id={`${gradientId}-top`} x1="0" y1="0" x2="0" y2="1">
54
+ <Stop offset="0" stopColor={theme.color.background.secondary} stopOpacity={1} />
55
+ <Stop offset="1" stopColor={theme.color.background.secondary} stopOpacity={0} />
56
+ </LinearGradient>
57
+ </Defs>
58
+ <Rect width="100%" height="100%" fill={`url(#${gradientId}-top)`} />
59
+ </Svg>
60
+ </View>
61
+ <View
62
+ pointerEvents="none"
63
+ style={[styles.fadeOverlay, styles.fadeOverlayBottom, { height: fadeHeight }]}
64
+ >
65
+ <Svg width="100%" height="100%" preserveAspectRatio="none">
66
+ <Defs>
67
+ <LinearGradient id={`${gradientId}-bottom`} x1="0" y1="0" x2="0" y2="1">
68
+ <Stop offset="0" stopColor={theme.color.background.secondary} stopOpacity={0} />
69
+ <Stop offset="1" stopColor={theme.color.background.secondary} stopOpacity={1} />
70
+ </LinearGradient>
71
+ </Defs>
72
+ <Rect width="100%" height="100%" fill={`url(#${gradientId}-bottom)`} />
73
+ </Svg>
74
+ </View>
75
+ </View>
76
+ ),
77
+ [fadeHeight, gradientId, theme.color.background.secondary]
78
+ );
79
+
80
+ const renderItem = useCallback(
81
+ ({ item }: { item: { label: string } }) => (
82
+ <View style={styles.indicator}>
83
+ <BodyText size="lg">{item.label}</BodyText>
84
+ </View>
85
+ ),
86
+ []
87
+ );
88
+
89
+ return (
90
+ <View style={[styles.container, { height: pickerHeight }]}>
91
+ <View style={styles.overlayContainer}>
92
+ <View style={[styles.selection]} />
93
+ </View>
94
+ <WheelPicker
95
+ data={data}
96
+ value={value}
97
+ onValueChanged={handleValueChanged}
98
+ itemHeight={ITEM_HEIGHT}
99
+ visibleItemCount={displayCount}
100
+ width={theme.components.timePicker.time.item.width}
101
+ renderItem={renderItem}
102
+ renderOverlay={renderOverlay}
103
+ />
104
+ </View>
105
+ );
106
+ };
107
+
108
+ const styles = StyleSheet.create(theme => ({
109
+ container: {
110
+ minWidth: theme.components.timePicker.time.item.width,
111
+ overflow: 'hidden',
112
+ alignItems: 'center',
113
+ justifyContent: 'center',
114
+ position: 'relative',
115
+ },
116
+ overlay: {
117
+ position: 'absolute',
118
+ top: 0,
119
+ left: 0,
120
+ right: 0,
121
+ bottom: 0,
122
+ alignItems: 'center',
123
+ },
124
+ indicator: {
125
+ width: theme.components.timePicker.time.item.width,
126
+ height: theme.components.timePicker.time.item.height,
127
+ alignItems: 'center',
128
+ justifyContent: 'center',
129
+ },
130
+ overlayContainer: {
131
+ ...StyleSheet.absoluteFillObject,
132
+ justifyContent: 'center',
133
+ alignItems: 'center',
134
+ },
135
+ selection: {
136
+ alignSelf: 'stretch',
137
+ backgroundColor: theme.color.interactive.neutral.surface.subtle.active,
138
+ borderRadius: theme.borderRadius.md,
139
+ width: theme.components.timePicker.time.item.width,
140
+ height: theme.components.timePicker.time.item.height,
141
+ },
142
+ fadeOverlay: {
143
+ position: 'absolute',
144
+ top: 0,
145
+ left: 0,
146
+ right: 0,
147
+ },
148
+ fadeOverlayBottom: {
149
+ top: undefined,
150
+ bottom: 0,
151
+ },
152
+ }));
153
+
154
+ export default TimePickerWheel;
@@ -0,0 +1,217 @@
1
+ import { memo, useCallback, useMemo } from 'react';
2
+ import { Platform, View } from 'react-native';
3
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
4
+ import Animated, {
5
+ Extrapolate,
6
+ interpolate,
7
+ runOnJS,
8
+ type SharedValue,
9
+ useAnimatedStyle,
10
+ useSharedValue,
11
+ } from 'react-native-reanimated';
12
+ import { StyleSheet } from 'react-native-unistyles';
13
+ import { isEqual } from '../../utils';
14
+ import { BodyText } from '../BodyText';
15
+ import type { PickerOption } from './TimePicker.props';
16
+
17
+ type TimePickerWheelProps = {
18
+ value: number | string;
19
+ setValue?: (value: any) => void;
20
+ items: PickerOption[];
21
+ };
22
+
23
+ const ITEM_HEIGHT = 44;
24
+
25
+ type WheelWebItemProps = {
26
+ displayValue: PickerOption | undefined;
27
+ index: number;
28
+ currentIndex: number;
29
+ translateY: SharedValue<number>;
30
+ radius: number;
31
+ displayCount: number;
32
+ value: number | string;
33
+ };
34
+
35
+ const WheelWebItem = ({
36
+ displayValue,
37
+ index,
38
+ currentIndex,
39
+ translateY,
40
+ radius,
41
+ displayCount,
42
+ value,
43
+ }: WheelWebItemProps) => {
44
+ const baseOpacity = displayValue?.value !== value ? 0.3 : 1;
45
+
46
+ const animatedStyle = useAnimatedStyle(() => {
47
+ const offset = (radius * 2) / displayCount;
48
+ const shifted = interpolate(
49
+ translateY.value,
50
+ [-radius, radius],
51
+ [-radius + offset * (index - currentIndex), radius + offset * (index - currentIndex)],
52
+ Extrapolate.EXTEND
53
+ );
54
+ const angle = interpolate(
55
+ shifted,
56
+ [-radius, radius],
57
+ [-Math.PI / 2, Math.PI / 2],
58
+ Extrapolate.CLAMP
59
+ );
60
+ const translate = radius * Math.sin(angle);
61
+ const rotateX = (angle * 180) / Math.PI;
62
+
63
+ return {
64
+ position: 'absolute',
65
+ height: ITEM_HEIGHT - 10,
66
+ opacity: baseOpacity,
67
+ alignItems: 'center',
68
+ justifyContent: 'center',
69
+ transform: [{ translateY: translate }, { rotateX: `${rotateX}deg` }],
70
+ };
71
+ }, [baseOpacity, currentIndex, displayCount, index, radius, translateY]);
72
+
73
+ return (
74
+ <Animated.View style={animatedStyle}>
75
+ <BodyText size="lg">{displayValue?.text}</BodyText>
76
+ </Animated.View>
77
+ );
78
+ };
79
+
80
+ const TimePickerWheel = ({ value, setValue = () => {}, items }: TimePickerWheelProps) => {
81
+ const displayCount = 5;
82
+ const translateY = useSharedValue(0);
83
+ const renderCount = displayCount * 2 < items.length ? displayCount * 8 : displayCount * 2 - 1;
84
+ const circular = items.length >= displayCount;
85
+ const height = 140;
86
+ const radius = height / 2;
87
+
88
+ const valueIndex = useMemo(() => {
89
+ return Math.max(
90
+ 0,
91
+ items.findIndex(item => item.value === value)
92
+ );
93
+ }, [items, value]);
94
+
95
+ const handlePanEnd = useCallback(
96
+ (deltaY: number) => {
97
+ let newValueIndex = valueIndex - Math.round(deltaY / ((radius * 2) / displayCount));
98
+ if (circular) {
99
+ newValueIndex = (newValueIndex + items.length) % items.length;
100
+ } else {
101
+ if (newValueIndex < 0) {
102
+ newValueIndex = 0;
103
+ } else if (newValueIndex >= items.length) {
104
+ newValueIndex = items.length - 1;
105
+ }
106
+ }
107
+ const newValue = items[newValueIndex];
108
+ if (newValue?.value === value) {
109
+ return;
110
+ }
111
+ if (newValue?.value) {
112
+ setValue(newValue.value);
113
+ } else if (items[0]?.value) {
114
+ setValue(items[0].value);
115
+ }
116
+ },
117
+ [circular, displayCount, items, radius, setValue, value, valueIndex]
118
+ );
119
+
120
+ const panGesture = useMemo(
121
+ () =>
122
+ Gesture.Pan()
123
+ .onUpdate(event => {
124
+ translateY.value = event.translationY;
125
+ })
126
+ .onEnd(event => {
127
+ runOnJS(handlePanEnd)(event.translationY);
128
+ translateY.value = 0;
129
+ }),
130
+ [handlePanEnd, translateY]
131
+ );
132
+
133
+ const displayValues = useMemo(() => {
134
+ const centerIndex = Math.floor(renderCount / 2);
135
+
136
+ return Array.from({ length: renderCount }, (_, index) => {
137
+ let targetIndex = valueIndex + index - centerIndex;
138
+ if (circular) {
139
+ targetIndex = ((targetIndex % items.length) + items.length) % items.length;
140
+ } else {
141
+ targetIndex = Math.max(0, Math.min(targetIndex, items.length - 1));
142
+ }
143
+ return items[targetIndex] || items[0];
144
+ });
145
+ }, [renderCount, valueIndex, items, circular]);
146
+
147
+ const currentIndex = Math.max(
148
+ 0,
149
+ displayValues.findIndex(item => item?.value === value)
150
+ );
151
+
152
+ return (
153
+ <GestureDetector gesture={panGesture}>
154
+ <View style={styles.container}>
155
+ <View
156
+ style={[
157
+ styles.selectedIndicator,
158
+ {
159
+ transform: [{ translateY: -ITEM_HEIGHT / 2 }],
160
+ height: ITEM_HEIGHT,
161
+ },
162
+ ]}
163
+ />
164
+ {displayValues?.map((displayValue, index) => (
165
+ <WheelWebItem
166
+ key={`${displayValue?.text}-${index}`}
167
+ displayValue={displayValue}
168
+ index={index}
169
+ currentIndex={currentIndex}
170
+ translateY={translateY}
171
+ radius={radius}
172
+ displayCount={displayCount}
173
+ value={value}
174
+ />
175
+ ))}
176
+ </View>
177
+ </GestureDetector>
178
+ );
179
+ };
180
+
181
+ const styles = StyleSheet.create(theme => ({
182
+ container: {
183
+ minWidth: 30,
184
+ overflow: 'hidden',
185
+ alignItems: 'center',
186
+ justifyContent: 'center',
187
+ height: 208,
188
+ ...Platform.select({
189
+ web: {
190
+ cursor: 'pointer',
191
+ userSelect: 'none',
192
+ },
193
+ }),
194
+ },
195
+ selectedIndicator: {
196
+ position: 'absolute',
197
+ width: theme.components.timePicker.time.item.width,
198
+ height: theme.components.timePicker.time.item.height,
199
+ top: '50%',
200
+ backgroundColor: theme.color.interactive.neutral.surface.subtle.active,
201
+ borderRadius: theme.borderRadius.md,
202
+ alignItems: 'center',
203
+ justifyContent: 'center',
204
+ },
205
+ }));
206
+
207
+ const customComparator = (
208
+ prev: Readonly<TimePickerWheelProps>,
209
+ next: Readonly<TimePickerWheelProps>
210
+ ) => {
211
+ const areEqual =
212
+ prev.value === next.value && prev.setValue === next.setValue && isEqual(prev.items, next.items);
213
+
214
+ return areEqual;
215
+ };
216
+
217
+ export default memo(TimePickerWheel, customComparator);
@@ -0,0 +1,8 @@
1
+ import 'dayjs/locale/en';
2
+ import '../DatePicker/polyfill';
3
+
4
+ export type { BottomSheetMethods as TimePickerMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
5
+ export type { DateType } from '../DatePicker/DatePicker.props';
6
+
7
+ export { default as TimePicker } from './TimePicker';
8
+ export type { TimePickerProps } from './TimePicker.props';