@dreamstack-us/kaal 0.0.1 → 0.0.2

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 (134) hide show
  1. package/lib/module/components/CalendarGrid/CalendarGrid.js +47 -13
  2. package/lib/module/components/CalendarGrid/CalendarGrid.js.map +1 -1
  3. package/lib/module/components/CalendarGrid/CalendarGrid.styles.js +22 -17
  4. package/lib/module/components/CalendarGrid/CalendarGrid.styles.js.map +1 -1
  5. package/lib/module/components/CalendarGrid/CalendarGrid.web.js +203 -0
  6. package/lib/module/components/CalendarGrid/CalendarGrid.web.js.map +1 -0
  7. package/lib/module/components/CalendarGrid/DayCell.js +64 -52
  8. package/lib/module/components/CalendarGrid/DayCell.js.map +1 -1
  9. package/lib/module/components/CalendarGrid/DayCell.web.js +112 -0
  10. package/lib/module/components/CalendarGrid/DayCell.web.js.map +1 -0
  11. package/lib/module/components/CalendarGrid/index.js +1 -1
  12. package/lib/module/components/CalendarGrid/index.js.map +1 -1
  13. package/lib/module/components/DatePicker/DatePicker.android.js +17 -8
  14. package/lib/module/components/DatePicker/DatePicker.android.js.map +1 -1
  15. package/lib/module/components/DatePicker/DatePicker.ios.js +17 -8
  16. package/lib/module/components/DatePicker/DatePicker.ios.js.map +1 -1
  17. package/lib/module/components/DatePicker/DatePicker.js.map +1 -1
  18. package/lib/module/components/DatePicker/DatePicker.styles.js +19 -20
  19. package/lib/module/components/DatePicker/DatePicker.styles.js.map +1 -1
  20. package/lib/module/components/DatePicker/DatePicker.web.js +26 -12
  21. package/lib/module/components/DatePicker/DatePicker.web.js.map +1 -1
  22. package/lib/module/components/TimePicker/ClockFace.js +27 -7
  23. package/lib/module/components/TimePicker/ClockFace.js.map +1 -1
  24. package/lib/module/components/TimePicker/ClockFace.web.js +253 -0
  25. package/lib/module/components/TimePicker/ClockFace.web.js.map +1 -0
  26. package/lib/module/components/TimePicker/MaterialTimePicker.js +68 -16
  27. package/lib/module/components/TimePicker/MaterialTimePicker.js.map +1 -1
  28. package/lib/module/components/TimePicker/MaterialTimePicker.web.js +231 -0
  29. package/lib/module/components/TimePicker/MaterialTimePicker.web.js.map +1 -0
  30. package/lib/module/components/TimePicker/TimePicker.android.js +13 -6
  31. package/lib/module/components/TimePicker/TimePicker.android.js.map +1 -1
  32. package/lib/module/components/TimePicker/TimePicker.ios.js +14 -7
  33. package/lib/module/components/TimePicker/TimePicker.ios.js.map +1 -1
  34. package/lib/module/components/TimePicker/TimePicker.styles.js +53 -45
  35. package/lib/module/components/TimePicker/TimePicker.styles.js.map +1 -1
  36. package/lib/module/components/TimePicker/TimePicker.web.js +24 -12
  37. package/lib/module/components/TimePicker/TimePicker.web.js.map +1 -1
  38. package/lib/module/components/TimePicker/TimeWheelPicker.js +45 -10
  39. package/lib/module/components/TimePicker/TimeWheelPicker.js.map +1 -1
  40. package/lib/module/components/TimePicker/TimeWheelPicker.web.js +339 -0
  41. package/lib/module/components/TimePicker/TimeWheelPicker.web.js.map +1 -0
  42. package/lib/module/components/TimePicker/index.js +3 -3
  43. package/lib/module/components/TimePicker/index.js.map +1 -1
  44. package/lib/module/components/WheelPicker/WheelPicker.js +21 -2
  45. package/lib/module/components/WheelPicker/WheelPicker.js.map +1 -1
  46. package/lib/module/components/WheelPicker/WheelPicker.styles.js +13 -8
  47. package/lib/module/components/WheelPicker/WheelPicker.styles.js.map +1 -1
  48. package/lib/module/components/WheelPicker/WheelPicker.web.js +146 -57
  49. package/lib/module/components/WheelPicker/WheelPicker.web.js.map +1 -1
  50. package/lib/module/context/ThemeOverrideContext.js +34 -0
  51. package/lib/module/context/ThemeOverrideContext.js.map +1 -0
  52. package/lib/module/index.js +3 -0
  53. package/lib/module/index.js.map +1 -1
  54. package/lib/module/utils/validation.js +74 -34
  55. package/lib/module/utils/validation.js.map +1 -1
  56. package/lib/typescript/components/CalendarGrid/CalendarGrid.d.ts +9 -0
  57. package/lib/typescript/components/CalendarGrid/CalendarGrid.d.ts.map +1 -1
  58. package/lib/typescript/components/CalendarGrid/CalendarGrid.styles.d.ts +12 -10
  59. package/lib/typescript/components/CalendarGrid/CalendarGrid.styles.d.ts.map +1 -1
  60. package/lib/typescript/components/CalendarGrid/CalendarGrid.web.d.ts +21 -0
  61. package/lib/typescript/components/CalendarGrid/CalendarGrid.web.d.ts.map +1 -0
  62. package/lib/typescript/components/CalendarGrid/DayCell.d.ts.map +1 -1
  63. package/lib/typescript/components/CalendarGrid/DayCell.web.d.ts +12 -0
  64. package/lib/typescript/components/CalendarGrid/DayCell.web.d.ts.map +1 -0
  65. package/lib/typescript/components/DatePicker/DatePicker.android.d.ts.map +1 -1
  66. package/lib/typescript/components/DatePicker/DatePicker.d.ts +12 -1
  67. package/lib/typescript/components/DatePicker/DatePicker.d.ts.map +1 -1
  68. package/lib/typescript/components/DatePicker/DatePicker.ios.d.ts.map +1 -1
  69. package/lib/typescript/components/DatePicker/DatePicker.styles.d.ts +12 -13
  70. package/lib/typescript/components/DatePicker/DatePicker.styles.d.ts.map +1 -1
  71. package/lib/typescript/components/DatePicker/DatePicker.web.d.ts.map +1 -1
  72. package/lib/typescript/components/TimePicker/ClockFace.d.ts.map +1 -1
  73. package/lib/typescript/components/TimePicker/ClockFace.web.d.ts +12 -0
  74. package/lib/typescript/components/TimePicker/ClockFace.web.d.ts.map +1 -0
  75. package/lib/typescript/components/TimePicker/MaterialTimePicker.d.ts.map +1 -1
  76. package/lib/typescript/components/TimePicker/MaterialTimePicker.web.d.ts +12 -0
  77. package/lib/typescript/components/TimePicker/MaterialTimePicker.web.d.ts.map +1 -0
  78. package/lib/typescript/components/TimePicker/TimePicker.android.d.ts.map +1 -1
  79. package/lib/typescript/components/TimePicker/TimePicker.ios.d.ts.map +1 -1
  80. package/lib/typescript/components/TimePicker/TimePicker.styles.d.ts +29 -25
  81. package/lib/typescript/components/TimePicker/TimePicker.styles.d.ts.map +1 -1
  82. package/lib/typescript/components/TimePicker/TimePicker.web.d.ts.map +1 -1
  83. package/lib/typescript/components/TimePicker/TimeWheelPicker.d.ts.map +1 -1
  84. package/lib/typescript/components/TimePicker/TimeWheelPicker.web.d.ts +11 -0
  85. package/lib/typescript/components/TimePicker/TimeWheelPicker.web.d.ts.map +1 -0
  86. package/lib/typescript/components/WheelPicker/WheelPicker.d.ts +14 -1
  87. package/lib/typescript/components/WheelPicker/WheelPicker.d.ts.map +1 -1
  88. package/lib/typescript/components/WheelPicker/WheelPicker.styles.d.ts +9 -7
  89. package/lib/typescript/components/WheelPicker/WheelPicker.styles.d.ts.map +1 -1
  90. package/lib/typescript/components/WheelPicker/WheelPicker.web.d.ts.map +1 -1
  91. package/lib/typescript/context/ThemeOverrideContext.d.ts +23 -0
  92. package/lib/typescript/context/ThemeOverrideContext.d.ts.map +1 -0
  93. package/lib/typescript/index.d.ts +4 -2
  94. package/lib/typescript/index.d.ts.map +1 -1
  95. package/lib/typescript/types/datepicker.d.ts +41 -0
  96. package/lib/typescript/types/datepicker.d.ts.map +1 -1
  97. package/lib/typescript/types/timepicker.d.ts +62 -0
  98. package/lib/typescript/types/timepicker.d.ts.map +1 -1
  99. package/lib/typescript/utils/validation.d.ts +47 -27
  100. package/lib/typescript/utils/validation.d.ts.map +1 -1
  101. package/package.json +8 -8
  102. package/src/components/CalendarGrid/CalendarGrid.styles.ts +21 -17
  103. package/src/components/CalendarGrid/CalendarGrid.tsx +98 -12
  104. package/src/components/CalendarGrid/CalendarGrid.web.tsx +305 -0
  105. package/src/components/CalendarGrid/DayCell.tsx +78 -59
  106. package/src/components/CalendarGrid/DayCell.web.tsx +129 -0
  107. package/src/components/DatePicker/DatePicker.android.tsx +14 -8
  108. package/src/components/DatePicker/DatePicker.ios.tsx +14 -8
  109. package/src/components/DatePicker/DatePicker.styles.ts +18 -22
  110. package/src/components/DatePicker/DatePicker.tsx +12 -1
  111. package/src/components/DatePicker/DatePicker.web.tsx +21 -13
  112. package/src/components/TimePicker/ClockFace.tsx +34 -8
  113. package/src/components/TimePicker/ClockFace.web.tsx +303 -0
  114. package/src/components/TimePicker/MaterialTimePicker.tsx +144 -13
  115. package/src/components/TimePicker/MaterialTimePicker.web.tsx +271 -0
  116. package/src/components/TimePicker/TimePicker.android.tsx +9 -1
  117. package/src/components/TimePicker/TimePicker.ios.tsx +10 -6
  118. package/src/components/TimePicker/TimePicker.styles.ts +52 -45
  119. package/src/components/TimePicker/TimePicker.web.tsx +17 -7
  120. package/src/components/TimePicker/TimeWheelPicker.tsx +60 -6
  121. package/src/components/TimePicker/TimeWheelPicker.web.tsx +401 -0
  122. package/src/components/WheelPicker/WheelPicker.styles.ts +12 -8
  123. package/src/components/WheelPicker/WheelPicker.tsx +24 -2
  124. package/src/components/WheelPicker/WheelPicker.web.tsx +153 -57
  125. package/src/context/ThemeOverrideContext.tsx +38 -0
  126. package/src/index.ts +11 -0
  127. package/src/types/datepicker.ts +44 -0
  128. package/src/types/timepicker.ts +74 -0
  129. package/src/utils/validation.ts +111 -55
  130. package/lib/module/unistyles.js +0 -9
  131. package/lib/module/unistyles.js.map +0 -1
  132. package/lib/typescript/unistyles.d.ts +0 -3
  133. package/lib/typescript/unistyles.d.ts.map +0 -1
  134. package/src/unistyles.ts +0 -6
@@ -1,35 +1,31 @@
1
- import { Platform } from 'react-native';
2
- import { StyleSheet, type UnistylesVariants } from 'react-native-unistyles';
1
+ import { Platform, StyleSheet } from 'react-native';
3
2
 
4
- export const styles = StyleSheet.create((theme) => ({
3
+ /**
4
+ * Default styles for DatePicker using plain React Native StyleSheet.
5
+ * Colors use dark theme defaults - consumers override via themeOverrides prop.
6
+ */
7
+ export const styles = StyleSheet.create({
5
8
  container: {
6
- backgroundColor: theme.colors.background.default,
7
- borderRadius: theme.radii.card,
9
+ backgroundColor: '#1E1E1E',
10
+ borderRadius: 16,
8
11
  overflow: 'hidden',
9
- variants: {
10
- size: {
11
- compact: {
12
- padding: theme.spacing(2),
13
- },
14
- default: {
15
- padding: theme.spacing(4),
16
- },
17
- large: {
18
- padding: theme.spacing(6),
19
- },
20
- },
21
- },
12
+ padding: 16,
13
+ },
14
+ containerCompact: {
15
+ padding: 8,
16
+ },
17
+ containerLarge: {
18
+ padding: 24,
22
19
  },
23
20
  backdrop: Platform.select({
24
21
  web: {
22
+ // @ts-ignore - web-only properties
25
23
  backdropFilter: 'blur(20px) saturate(180%)',
26
24
  WebkitBackdropFilter: 'blur(20px) saturate(180%)',
27
25
  backgroundColor: 'rgba(255, 255, 255, 0.7)',
28
26
  },
29
27
  default: {
30
- backgroundColor: theme.colors.background.elevated,
28
+ backgroundColor: '#2C2C2E',
31
29
  },
32
30
  }),
33
- }));
34
-
35
- export type DatePickerVariants = UnistylesVariants<typeof styles>;
31
+ });
@@ -1,5 +1,5 @@
1
1
  import type React from 'react';
2
- import type { DatePickerMode } from '../../types';
2
+ import type { DatePickerMode, DatePickerThemeOverrides } from '../../types';
3
3
 
4
4
  export interface KaalDatePickerProps {
5
5
  value: Date;
@@ -11,6 +11,17 @@ export interface KaalDatePickerProps {
11
11
  maxDate?: Date;
12
12
  disabledDates?: Date[];
13
13
  locale?: string;
14
+ /**
15
+ * First day of the week: 0 = Sunday, 1 = Monday
16
+ * @default 0 (Sunday)
17
+ *
18
+ * TODO: This is a temporary solution. In the future, we need to add full
19
+ * locale support to handle different calendar formats, layouts, and
20
+ * localized day/month names across different regions.
21
+ */
22
+ weekStartsOn?: 0 | 1;
23
+ /** Custom theme overrides for styling without matching Kaal's theme structure */
24
+ themeOverrides?: DatePickerThemeOverrides;
14
25
  }
15
26
 
16
27
  // Platform-specific implementations are handled by Metro's file resolution
@@ -1,4 +1,5 @@
1
1
  import type React from 'react';
2
+ import { ThemeOverrideProvider } from '../../context/ThemeOverrideContext';
2
3
  import { CalendarGrid } from '../CalendarGrid';
3
4
  import { WheelPicker } from '../WheelPicker';
4
5
  import type { KaalDatePickerProps } from './DatePicker';
@@ -11,26 +12,33 @@ export const DatePicker: React.FC<KaalDatePickerProps> = ({
11
12
  minDate,
12
13
  maxDate,
13
14
  disabledDates,
15
+ themeOverrides,
16
+ weekStartsOn = 0,
14
17
  }) => {
15
18
  if (theme === 'ios' && variant === 'wheel') {
16
19
  return (
17
- <WheelPicker
20
+ <ThemeOverrideProvider value={{ datePicker: themeOverrides }}>
21
+ <WheelPicker
22
+ value={value}
23
+ onChange={onChange}
24
+ minDate={minDate}
25
+ maxDate={maxDate}
26
+ />
27
+ </ThemeOverrideProvider>
28
+ );
29
+ }
30
+
31
+ return (
32
+ <ThemeOverrideProvider value={{ datePicker: themeOverrides }}>
33
+ <CalendarGrid
18
34
  value={value}
19
35
  onChange={onChange}
20
36
  minDate={minDate}
21
37
  maxDate={maxDate}
38
+ disabledDates={disabledDates}
39
+ themeMode={theme === 'native' ? 'ios' : theme}
40
+ weekStartsOn={weekStartsOn}
22
41
  />
23
- );
24
- }
25
-
26
- return (
27
- <CalendarGrid
28
- value={value}
29
- onChange={onChange}
30
- minDate={minDate}
31
- maxDate={maxDate}
32
- disabledDates={disabledDates}
33
- themeMode={theme === 'native' ? 'ios' : theme}
34
- />
42
+ </ThemeOverrideProvider>
35
43
  );
36
44
  };
@@ -4,8 +4,9 @@ import { View } from 'react-native';
4
4
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
5
5
  import { runOnJS } from 'react-native-reanimated';
6
6
  import Svg, { Circle, G, Line, Text as SvgText } from 'react-native-svg';
7
+ import { useTimePickerOverrides } from '../../context/ThemeOverrideContext';
7
8
  import { to12Hour, to24Hour } from '../../hooks/useTimePicker';
8
- import type { ClockMode, TimePeriod, TimeValue } from '../../types/timepicker';
9
+ import type { ClockMode, TimeValue } from '../../types/timepicker';
9
10
  import { styles } from './TimePicker.styles';
10
11
 
11
12
  // Clock dimensions
@@ -19,6 +20,15 @@ const CENTER_DOT_RADIUS = 4;
19
20
  const HOURS_12 = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
20
21
  const MINUTE_LABELS = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
21
22
 
23
+ // Default colors (dark theme)
24
+ const DEFAULT_COLORS = {
25
+ clockBackground: '#3F384C',
26
+ handColor: '#4DA6FF',
27
+ selectionDotColor: '#4DA6FF',
28
+ textColor: '#E6E1E5',
29
+ textSelectedColor: '#FFFFFF',
30
+ };
31
+
22
32
  /**
23
33
  * Converts clock position (0-11 for hours, 0-59 for minutes) to angle in degrees
24
34
  * 12 o'clock is at -90 degrees (top)
@@ -54,8 +64,24 @@ interface ClockFaceProps {
54
64
 
55
65
  export const ClockFace: React.FC<ClockFaceProps> = memo(
56
66
  ({ value, onChange, mode, onModeChange, is24Hour = false }) => {
67
+ const overrides = useTimePickerOverrides();
57
68
  const { hour: hour12, period } = to12Hour(value.hours);
58
69
 
70
+ // Build colors from overrides
71
+ const colors = useMemo(
72
+ () => ({
73
+ clockBackground:
74
+ overrides?.clockBackground ?? DEFAULT_COLORS.clockBackground,
75
+ handColor: overrides?.clockHandColor ?? DEFAULT_COLORS.handColor,
76
+ selectionDotColor:
77
+ overrides?.clockSelectionColor ?? DEFAULT_COLORS.selectionDotColor,
78
+ textColor: overrides?.clockTextColor ?? DEFAULT_COLORS.textColor,
79
+ textSelectedColor:
80
+ overrides?.clockTextSelectedColor ?? DEFAULT_COLORS.textSelectedColor,
81
+ }),
82
+ [overrides],
83
+ );
84
+
59
85
  // Calculate hand end position based on current value
60
86
  const handAngle = useMemo(() => {
61
87
  if (mode === 'hours') {
@@ -149,7 +175,7 @@ export const ClockFace: React.FC<ClockFaceProps> = memo(
149
175
  alignmentBaseline="central"
150
176
  fontSize={14}
151
177
  fontWeight={isSelected ? '500' : '400'}
152
- fill={isSelected ? '#FFFFFF' : '#E6E1E5'}
178
+ fill={isSelected ? colors.textSelectedColor : colors.textColor}
153
179
  >
154
180
  {hour}
155
181
  </SvgText>
@@ -171,13 +197,13 @@ export const ClockFace: React.FC<ClockFaceProps> = memo(
171
197
  alignmentBaseline="central"
172
198
  fontSize={14}
173
199
  fontWeight={isSelected ? '500' : '400'}
174
- fill={isSelected ? '#FFFFFF' : '#E6E1E5'}
200
+ fill={isSelected ? colors.textSelectedColor : colors.textColor}
175
201
  >
176
202
  {minute.toString().padStart(2, '0')}
177
203
  </SvgText>
178
204
  );
179
205
  });
180
- }, [mode, hour12, value.minutes]);
206
+ }, [mode, hour12, value.minutes, colors]);
181
207
 
182
208
  return (
183
209
  <View style={styles.clockContainer}>
@@ -192,7 +218,7 @@ export const ClockFace: React.FC<ClockFaceProps> = memo(
192
218
  cx={CLOCK_CENTER}
193
219
  cy={CLOCK_CENTER}
194
220
  r={CLOCK_SIZE / 2 - 4}
195
- fill="#3F384C"
221
+ fill={colors.clockBackground}
196
222
  />
197
223
 
198
224
  {/* Selection dot (behind numbers) */}
@@ -200,7 +226,7 @@ export const ClockFace: React.FC<ClockFaceProps> = memo(
200
226
  cx={handEndPos.x}
201
227
  cy={handEndPos.y}
202
228
  r={SELECTION_DOT_RADIUS}
203
- fill="#4DA6FF"
229
+ fill={colors.selectionDotColor}
204
230
  />
205
231
 
206
232
  {/* Clock hand */}
@@ -209,7 +235,7 @@ export const ClockFace: React.FC<ClockFaceProps> = memo(
209
235
  y1={CLOCK_CENTER}
210
236
  x2={handEndPos.x}
211
237
  y2={handEndPos.y}
212
- stroke="#4DA6FF"
238
+ stroke={colors.handColor}
213
239
  strokeWidth={2}
214
240
  />
215
241
 
@@ -218,7 +244,7 @@ export const ClockFace: React.FC<ClockFaceProps> = memo(
218
244
  cx={CLOCK_CENTER}
219
245
  cy={CLOCK_CENTER}
220
246
  r={CENTER_DOT_RADIUS}
221
- fill="#4DA6FF"
247
+ fill={colors.handColor}
222
248
  />
223
249
 
224
250
  {/* Numbers */}
@@ -0,0 +1,303 @@
1
+ /// <reference lib="dom" />
2
+ import type React from 'react';
3
+ import { memo, useCallback, useMemo, useRef } from 'react';
4
+ import { View } from 'react-native';
5
+ import { useTimePickerOverrides } from '../../context/ThemeOverrideContext';
6
+ import { to12Hour, to24Hour } from '../../hooks/useTimePicker';
7
+ import type { ClockMode, TimeValue } from '../../types/timepicker';
8
+ import { styles } from './TimePicker.styles';
9
+
10
+ // Clock dimensions
11
+ const CLOCK_SIZE = 256;
12
+ const CLOCK_CENTER = CLOCK_SIZE / 2;
13
+ const OUTER_RADIUS = 96;
14
+ const SELECTION_DOT_RADIUS = 20;
15
+ const CENTER_DOT_RADIUS = 4;
16
+
17
+ // Hour positions (12 at top, clockwise)
18
+ const HOURS_12 = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
19
+ const MINUTE_LABELS = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
20
+
21
+ // Default colors (dark theme)
22
+ const DEFAULT_COLORS = {
23
+ clockBackground: '#3F384C',
24
+ handColor: '#4DA6FF',
25
+ selectionDotColor: '#4DA6FF',
26
+ textColor: '#E6E1E5',
27
+ textSelectedColor: '#FFFFFF',
28
+ };
29
+
30
+ /**
31
+ * Converts clock position (0-11 for hours, 0-59 for minutes) to angle in degrees
32
+ * 12 o'clock is at -90 degrees (top)
33
+ */
34
+ const positionToAngle = (position: number, isMinutes = false): number => {
35
+ if (isMinutes) {
36
+ return position * 6 - 90; // 360/60 = 6 degrees per minute
37
+ }
38
+ return position * 30 - 90; // 360/12 = 30 degrees per position
39
+ };
40
+
41
+ /**
42
+ * Gets x,y coordinates on circle from angle and radius
43
+ */
44
+ const getPointOnCircle = (
45
+ angleDegrees: number,
46
+ radius: number,
47
+ ): { x: number; y: number } => {
48
+ const rad = (angleDegrees * Math.PI) / 180;
49
+ return {
50
+ x: CLOCK_CENTER + radius * Math.cos(rad),
51
+ y: CLOCK_CENTER + radius * Math.sin(rad),
52
+ };
53
+ };
54
+
55
+ interface ClockFaceProps {
56
+ value: TimeValue;
57
+ onChange: (time: TimeValue) => void;
58
+ mode: ClockMode;
59
+ onModeChange?: (mode: ClockMode) => void;
60
+ is24Hour?: boolean;
61
+ }
62
+
63
+ export const ClockFace: React.FC<ClockFaceProps> = memo(
64
+ ({ value, onChange, mode, onModeChange, is24Hour = false }) => {
65
+ const overrides = useTimePickerOverrides();
66
+ const svgRef = useRef<SVGSVGElement>(null);
67
+ const isDragging = useRef(false);
68
+ const { hour: hour12, period } = to12Hour(value.hours);
69
+
70
+ // Build colors from overrides
71
+ const colors = useMemo(
72
+ () => ({
73
+ clockBackground:
74
+ overrides?.clockBackground ?? DEFAULT_COLORS.clockBackground,
75
+ handColor: overrides?.clockHandColor ?? DEFAULT_COLORS.handColor,
76
+ selectionDotColor:
77
+ overrides?.clockSelectionColor ?? DEFAULT_COLORS.selectionDotColor,
78
+ textColor: overrides?.clockTextColor ?? DEFAULT_COLORS.textColor,
79
+ textSelectedColor:
80
+ overrides?.clockTextSelectedColor ?? DEFAULT_COLORS.textSelectedColor,
81
+ }),
82
+ [overrides],
83
+ );
84
+
85
+ // Calculate hand end position based on current value
86
+ const handAngle = useMemo(() => {
87
+ if (mode === 'hours') {
88
+ const hourIndex = HOURS_12.indexOf(hour12);
89
+ return positionToAngle(hourIndex >= 0 ? hourIndex : 0);
90
+ }
91
+ return positionToAngle(value.minutes, true);
92
+ }, [mode, hour12, value.minutes]);
93
+
94
+ const handEndPos = getPointOnCircle(handAngle, OUTER_RADIUS);
95
+
96
+ const handleInteraction = useCallback(
97
+ (clientX: number, clientY: number) => {
98
+ if (!svgRef.current) return;
99
+
100
+ const rect = svgRef.current.getBoundingClientRect();
101
+ const x = clientX - rect.left;
102
+ const y = clientY - rect.top;
103
+
104
+ // Convert touch coordinates to angle
105
+ const dx = x - CLOCK_CENTER;
106
+ const dy = y - CLOCK_CENTER;
107
+ const angle = Math.atan2(dy, dx) * (180 / Math.PI);
108
+
109
+ // Normalize angle to 0-360, with 0 at top (12 o'clock)
110
+ const normalized = (((angle + 90) % 360) + 360) % 360;
111
+
112
+ if (mode === 'hours') {
113
+ // Round to nearest hour position
114
+ const position = Math.round(normalized / 30) % 12;
115
+ const hour = HOURS_12[position];
116
+ if (hour !== undefined) {
117
+ const newHours = to24Hour(hour, period);
118
+ onChange({ hours: newHours, minutes: value.minutes });
119
+ }
120
+ } else {
121
+ // Round to nearest minute
122
+ const minutes = Math.round(normalized / 6) % 60;
123
+ onChange({ hours: value.hours, minutes });
124
+ }
125
+ },
126
+ [mode, period, value, onChange],
127
+ );
128
+
129
+ const handleInteractionEnd = useCallback(() => {
130
+ // Switch to minutes mode after selecting hours
131
+ if (mode === 'hours' && onModeChange) {
132
+ // Small delay for visual feedback
133
+ setTimeout(() => onModeChange('minutes'), 200);
134
+ }
135
+ }, [mode, onModeChange]);
136
+
137
+ const handleMouseDown = useCallback(
138
+ (e: React.MouseEvent<SVGSVGElement>) => {
139
+ isDragging.current = true;
140
+ handleInteraction(e.clientX, e.clientY);
141
+ },
142
+ [handleInteraction],
143
+ );
144
+
145
+ const handleMouseMove = useCallback(
146
+ (e: React.MouseEvent<SVGSVGElement>) => {
147
+ if (!isDragging.current) return;
148
+ handleInteraction(e.clientX, e.clientY);
149
+ },
150
+ [handleInteraction],
151
+ );
152
+
153
+ const handleMouseUp = useCallback(() => {
154
+ if (isDragging.current) {
155
+ isDragging.current = false;
156
+ handleInteractionEnd();
157
+ }
158
+ }, [handleInteractionEnd]);
159
+
160
+ const handleTouchStart = useCallback(
161
+ (e: React.TouchEvent<SVGSVGElement>) => {
162
+ isDragging.current = true;
163
+ const touch = e.touches[0];
164
+ if (touch) {
165
+ handleInteraction(touch.clientX, touch.clientY);
166
+ }
167
+ },
168
+ [handleInteraction],
169
+ );
170
+
171
+ const handleTouchMove = useCallback(
172
+ (e: React.TouchEvent<SVGSVGElement>) => {
173
+ if (!isDragging.current) return;
174
+ const touch = e.touches[0];
175
+ if (touch) {
176
+ handleInteraction(touch.clientX, touch.clientY);
177
+ }
178
+ },
179
+ [handleInteraction],
180
+ );
181
+
182
+ const handleTouchEnd = useCallback(() => {
183
+ if (isDragging.current) {
184
+ isDragging.current = false;
185
+ handleInteractionEnd();
186
+ }
187
+ }, [handleInteractionEnd]);
188
+
189
+ // Render clock numbers
190
+ const numbers = useMemo(() => {
191
+ if (mode === 'hours') {
192
+ return HOURS_12.map((hour, index) => {
193
+ const angle = positionToAngle(index);
194
+ const pos = getPointOnCircle(angle, OUTER_RADIUS);
195
+ const isSelected = hour12 === hour;
196
+
197
+ return (
198
+ <text
199
+ key={hour}
200
+ x={pos.x}
201
+ y={pos.y}
202
+ textAnchor="middle"
203
+ dominantBaseline="central"
204
+ fontSize={14}
205
+ fontWeight={isSelected ? '500' : '400'}
206
+ fill={isSelected ? colors.textSelectedColor : colors.textColor}
207
+ style={{ userSelect: 'none' }}
208
+ >
209
+ {hour}
210
+ </text>
211
+ );
212
+ });
213
+ }
214
+
215
+ return MINUTE_LABELS.map((minute, index) => {
216
+ const angle = positionToAngle(index);
217
+ const pos = getPointOnCircle(angle, OUTER_RADIUS);
218
+ const isSelected = value.minutes === minute;
219
+
220
+ return (
221
+ <text
222
+ key={minute}
223
+ x={pos.x}
224
+ y={pos.y}
225
+ textAnchor="middle"
226
+ dominantBaseline="central"
227
+ fontSize={14}
228
+ fontWeight={isSelected ? '500' : '400'}
229
+ fill={isSelected ? colors.textSelectedColor : colors.textColor}
230
+ style={{ userSelect: 'none' }}
231
+ >
232
+ {minute.toString().padStart(2, '0')}
233
+ </text>
234
+ );
235
+ });
236
+ }, [mode, hour12, value.minutes, colors]);
237
+
238
+ return (
239
+ <View style={styles.clockContainer}>
240
+ <svg
241
+ ref={svgRef}
242
+ width={CLOCK_SIZE}
243
+ height={CLOCK_SIZE}
244
+ viewBox={`0 0 ${CLOCK_SIZE} ${CLOCK_SIZE}`}
245
+ onMouseDown={handleMouseDown}
246
+ onMouseMove={handleMouseMove}
247
+ onMouseUp={handleMouseUp}
248
+ onMouseLeave={handleMouseUp}
249
+ onTouchStart={handleTouchStart}
250
+ onTouchMove={handleTouchMove}
251
+ onTouchEnd={handleTouchEnd}
252
+ style={{ cursor: 'pointer', touchAction: 'none' }}
253
+ role="img"
254
+ aria-label={`Clock face for selecting ${mode === 'hours' ? 'hours' : 'minutes'}`}
255
+ >
256
+ <title>
257
+ {mode === 'hours'
258
+ ? 'Hour selection clock'
259
+ : 'Minute selection clock'}
260
+ </title>
261
+ {/* Background circle */}
262
+ <circle
263
+ cx={CLOCK_CENTER}
264
+ cy={CLOCK_CENTER}
265
+ r={CLOCK_SIZE / 2 - 4}
266
+ fill={colors.clockBackground}
267
+ />
268
+
269
+ {/* Selection dot (behind numbers) */}
270
+ <circle
271
+ cx={handEndPos.x}
272
+ cy={handEndPos.y}
273
+ r={SELECTION_DOT_RADIUS}
274
+ fill={colors.selectionDotColor}
275
+ />
276
+
277
+ {/* Clock hand */}
278
+ <line
279
+ x1={CLOCK_CENTER}
280
+ y1={CLOCK_CENTER}
281
+ x2={handEndPos.x}
282
+ y2={handEndPos.y}
283
+ stroke={colors.handColor}
284
+ strokeWidth={2}
285
+ />
286
+
287
+ {/* Center dot */}
288
+ <circle
289
+ cx={CLOCK_CENTER}
290
+ cy={CLOCK_CENTER}
291
+ r={CENTER_DOT_RADIUS}
292
+ fill={colors.handColor}
293
+ />
294
+
295
+ {/* Numbers */}
296
+ <g>{numbers}</g>
297
+ </svg>
298
+ </View>
299
+ );
300
+ },
301
+ );
302
+
303
+ ClockFace.displayName = 'ClockFace';