@expokit/cli 0.0.0-stage → 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 (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -2
  3. package/bin/expo-kit.js +1204 -0
  4. package/bin/lib.mjs +343 -0
  5. package/bin/mcp.mjs +338 -0
  6. package/package.json +44 -4
  7. package/registry/_docs/accordion.json +99 -0
  8. package/registry/_docs/action-sheet.json +110 -0
  9. package/registry/_docs/alert.json +127 -0
  10. package/registry/_docs/avatar.json +203 -0
  11. package/registry/_docs/badge.json +117 -0
  12. package/registry/_docs/bottom-nav.json +119 -0
  13. package/registry/_docs/bottom-sheet.json +158 -0
  14. package/registry/_docs/bullet.json +24 -0
  15. package/registry/_docs/button-3d.json +122 -0
  16. package/registry/_docs/button.json +115 -0
  17. package/registry/_docs/calendar.json +182 -0
  18. package/registry/_docs/card-stack.json +108 -0
  19. package/registry/_docs/card.json +111 -0
  20. package/registry/_docs/carousel.json +102 -0
  21. package/registry/_docs/checkbox.json +93 -0
  22. package/registry/_docs/checkout.json +102 -0
  23. package/registry/_docs/chip-group.json +116 -0
  24. package/registry/_docs/color-swatch.json +66 -0
  25. package/registry/_docs/confetti.json +82 -0
  26. package/registry/_docs/coupon.json +176 -0
  27. package/registry/_docs/cube-pager.json +84 -0
  28. package/registry/_docs/dashboard.json +120 -0
  29. package/registry/_docs/date-picker.json +249 -0
  30. package/registry/_docs/dialog.json +162 -0
  31. package/registry/_docs/drawer.json +171 -0
  32. package/registry/_docs/fab.json +120 -0
  33. package/registry/_docs/flip-card.json +111 -0
  34. package/registry/_docs/floating-input.json +191 -0
  35. package/registry/_docs/gift-card.json +108 -0
  36. package/registry/_docs/glass-button.json +118 -0
  37. package/registry/_docs/glass-nav-bar.json +78 -0
  38. package/registry/_docs/glass-search-bar.json +90 -0
  39. package/registry/_docs/glass-segmented.json +66 -0
  40. package/registry/_docs/glass-sheet.json +108 -0
  41. package/registry/_docs/glass-tab-bar.json +76 -0
  42. package/registry/_docs/glass.json +78 -0
  43. package/registry/_docs/inbox.json +72 -0
  44. package/registry/_docs/isometric-bar.json +72 -0
  45. package/registry/_docs/kit.json +11 -0
  46. package/registry/_docs/kpi-card.json +11 -0
  47. package/registry/_docs/like-button.json +72 -0
  48. package/registry/_docs/liquidglass-tab.json +27 -0
  49. package/registry/_docs/list-item.json +200 -0
  50. package/registry/_docs/loyalty-card.json +108 -0
  51. package/registry/_docs/morph-button.json +108 -0
  52. package/registry/_docs/number-ticker.json +96 -0
  53. package/registry/_docs/offers.json +78 -0
  54. package/registry/_docs/onboarding.json +66 -0
  55. package/registry/_docs/otp-input.json +134 -0
  56. package/registry/_docs/pagination-dots.json +88 -0
  57. package/registry/_docs/parallax-layers.json +84 -0
  58. package/registry/_docs/parallax-scroll.json +186 -0
  59. package/registry/_docs/password-input.json +92 -0
  60. package/registry/_docs/payment-card.json +153 -0
  61. package/registry/_docs/pixel-bar.json +66 -0
  62. package/registry/_docs/polar-area.json +78 -0
  63. package/registry/_docs/profile.json +84 -0
  64. package/registry/_docs/progress-steps.json +92 -0
  65. package/registry/_docs/progress.json +132 -0
  66. package/registry/_docs/radio-group.json +92 -0
  67. package/registry/_docs/rating.json +90 -0
  68. package/registry/_docs/ring-carousel.json +102 -0
  69. package/registry/_docs/scratch-card.json +90 -0
  70. package/registry/_docs/search-bar.json +140 -0
  71. package/registry/_docs/search-results.json +120 -0
  72. package/registry/_docs/segmented-control.json +103 -0
  73. package/registry/_docs/select.json +252 -0
  74. package/registry/_docs/settings.json +72 -0
  75. package/registry/_docs/shimmer-button.json +78 -0
  76. package/registry/_docs/sign-in.json +198 -0
  77. package/registry/_docs/skeleton.json +142 -0
  78. package/registry/_docs/slide-to-confirm.json +84 -0
  79. package/registry/_docs/slider.json +157 -0
  80. package/registry/_docs/spin-wheel.json +114 -0
  81. package/registry/_docs/spinner.json +91 -0
  82. package/registry/_docs/stepper.json +168 -0
  83. package/registry/_docs/swipe-deck.json +102 -0
  84. package/registry/_docs/swipeable-row.json +72 -0
  85. package/registry/_docs/switch.json +104 -0
  86. package/registry/_docs/tabs.json +105 -0
  87. package/registry/_docs/tag-input.json +128 -0
  88. package/registry/_docs/textarea.json +175 -0
  89. package/registry/_docs/ticket-card.json +174 -0
  90. package/registry/_docs/ticket-shape.json +11 -0
  91. package/registry/_docs/tilt-card.json +141 -0
  92. package/registry/_docs/time-picker.json +253 -0
  93. package/registry/_docs/timeline-chart.json +30 -0
  94. package/registry/_docs/timeline.json +156 -0
  95. package/registry/_docs/toast.json +161 -0
  96. package/registry/_docs/treemap.json +30 -0
  97. package/registry/_docs/waterfall.json +36 -0
  98. package/registry/_docs/wheel-list.json +102 -0
  99. package/registry/accordion/files/src/components/ui/accordion.tsx +233 -0
  100. package/registry/accordion/files/src/lib/kit.tsx +219 -0
  101. package/registry/accordion.json +17 -0
  102. package/registry/action-sheet/files/src/components/ui/action-sheet.tsx +580 -0
  103. package/registry/action-sheet/files/src/lib/kit.tsx +219 -0
  104. package/registry/action-sheet.json +18 -0
  105. package/registry/alert/files/src/components/ui/alert.tsx +654 -0
  106. package/registry/alert/files/src/components/ui/button.tsx +274 -0
  107. package/registry/alert/files/src/lib/kit.tsx +219 -0
  108. package/registry/alert.json +20 -0
  109. package/registry/avatar/files/src/components/ui/avatar.tsx +790 -0
  110. package/registry/avatar/files/src/lib/kit.tsx +219 -0
  111. package/registry/avatar.json +18 -0
  112. package/registry/badge/files/src/components/ui/badge.tsx +316 -0
  113. package/registry/badge/files/src/lib/kit.tsx +219 -0
  114. package/registry/badge.json +17 -0
  115. package/registry/bottom-nav/files/src/components/ui/bottom-nav.tsx +1050 -0
  116. package/registry/bottom-nav/files/src/lib/kit.tsx +219 -0
  117. package/registry/bottom-nav.json +18 -0
  118. package/registry/bottom-sheet/files/src/components/ui/bottom-sheet.tsx +585 -0
  119. package/registry/bottom-sheet/files/src/lib/kit.tsx +219 -0
  120. package/registry/bottom-sheet.json +17 -0
  121. package/registry/bullet.json +14 -0
  122. package/registry/button/files/src/components/ui/button.tsx +274 -0
  123. package/registry/button/files/src/lib/kit.tsx +219 -0
  124. package/registry/button-3d/files/src/components/ui/button-3d.tsx +212 -0
  125. package/registry/button-3d/files/src/lib/kit.tsx +219 -0
  126. package/registry/button-3d.json +18 -0
  127. package/registry/button.json +18 -0
  128. package/registry/calendar/files/src/components/ui/calendar.tsx +822 -0
  129. package/registry/calendar/files/src/lib/kit.tsx +219 -0
  130. package/registry/calendar.json +17 -0
  131. package/registry/card/files/src/components/ui/card.tsx +300 -0
  132. package/registry/card/files/src/lib/kit.tsx +219 -0
  133. package/registry/card-stack.json +15 -0
  134. package/registry/card.json +17 -0
  135. package/registry/carousel.json +15 -0
  136. package/registry/checkbox/files/src/components/ui/checkbox.tsx +150 -0
  137. package/registry/checkbox/files/src/lib/kit.tsx +219 -0
  138. package/registry/checkbox.json +17 -0
  139. package/registry/checkout.json +26 -0
  140. package/registry/chip-group/files/src/components/ui/chip-group.tsx +269 -0
  141. package/registry/chip-group/files/src/lib/kit.tsx +219 -0
  142. package/registry/chip-group.json +17 -0
  143. package/registry/color-swatch.json +17 -0
  144. package/registry/confetti/files/src/components/ui/confetti.tsx +222 -0
  145. package/registry/confetti/files/src/lib/kit.tsx +219 -0
  146. package/registry/confetti.json +15 -0
  147. package/registry/coupon/files/src/components/ui/coupon.tsx +698 -0
  148. package/registry/coupon/files/src/components/ui/flip-card.tsx +205 -0
  149. package/registry/coupon/files/src/components/ui/tilt-card.tsx +635 -0
  150. package/registry/coupon/files/src/lib/kit.tsx +219 -0
  151. package/registry/coupon/files/src/lib/ticket-shape.tsx +543 -0
  152. package/registry/coupon.json +23 -0
  153. package/registry/cube-pager.json +15 -0
  154. package/registry/dashboard.json +32 -0
  155. package/registry/date-picker/files/src/components/ui/calendar.tsx +822 -0
  156. package/registry/date-picker/files/src/components/ui/date-picker.tsx +796 -0
  157. package/registry/date-picker/files/src/lib/kit.tsx +219 -0
  158. package/registry/date-picker.json +19 -0
  159. package/registry/dialog/files/src/components/ui/dialog.tsx +759 -0
  160. package/registry/dialog/files/src/lib/kit.tsx +219 -0
  161. package/registry/dialog.json +18 -0
  162. package/registry/drawer/files/src/components/ui/drawer.tsx +1135 -0
  163. package/registry/drawer/files/src/lib/kit.tsx +219 -0
  164. package/registry/drawer.json +18 -0
  165. package/registry/fab.json +18 -0
  166. package/registry/flip-card/files/src/components/ui/flip-card.tsx +205 -0
  167. package/registry/flip-card/files/src/lib/kit.tsx +219 -0
  168. package/registry/flip-card.json +15 -0
  169. package/registry/floating-input/files/src/components/ui/floating-input.tsx +528 -0
  170. package/registry/floating-input/files/src/lib/kit.tsx +219 -0
  171. package/registry/floating-input.json +17 -0
  172. package/registry/gift-card.json +21 -0
  173. package/registry/glass-button.json +22 -0
  174. package/registry/glass-nav-bar.json +21 -0
  175. package/registry/glass-search-bar.json +21 -0
  176. package/registry/glass-segmented.json +22 -0
  177. package/registry/glass-sheet.json +22 -0
  178. package/registry/glass-tab-bar.json +22 -0
  179. package/registry/glass.json +19 -0
  180. package/registry/inbox.json +30 -0
  181. package/registry/isometric-bar.json +14 -0
  182. package/registry/kit/files/src/lib/kit.tsx +219 -0
  183. package/registry/kit.json +11 -0
  184. package/registry/kpi-card/files/src/components/charts/kpi-card.tsx +45 -0
  185. package/registry/kpi-card.json +14 -0
  186. package/registry/like-button.json +18 -0
  187. package/registry/liquidglass-tab/files/src/app/(tabs)/_layout.tsx +97 -0
  188. package/registry/liquidglass-tab/files/src/app/(tabs)/alerts.tsx +5 -0
  189. package/registry/liquidglass-tab/files/src/app/(tabs)/explore.tsx +5 -0
  190. package/registry/liquidglass-tab/files/src/app/(tabs)/index.tsx +5 -0
  191. package/registry/liquidglass-tab/files/src/app/(tabs)/profile.tsx +5 -0
  192. package/registry/liquidglass-tab/files/src/app/_layout.tsx +33 -0
  193. package/registry/liquidglass-tab/files/src/components/GlassTabBar.tsx +259 -0
  194. package/registry/liquidglass-tab/files/src/components/TabScreen.tsx +122 -0
  195. package/registry/liquidglass-tab/files/src/lib/tab-bar.ts +61 -0
  196. package/registry/liquidglass-tab/files/src/lib/tab-icons.ts +130 -0
  197. package/registry/liquidglass-tab/files/src/lib/tabs.ts +108 -0
  198. package/registry/liquidglass-tab.json +23 -0
  199. package/registry/list-item/files/src/components/ui/list-item.tsx +508 -0
  200. package/registry/list-item/files/src/lib/kit.tsx +219 -0
  201. package/registry/list-item.json +17 -0
  202. package/registry/loyalty-card.json +23 -0
  203. package/registry/morph-button.json +18 -0
  204. package/registry/number-ticker.json +15 -0
  205. package/registry/offers.json +24 -0
  206. package/registry/onboarding.json +17 -0
  207. package/registry/otp-input/files/src/components/ui/otp-input.tsx +380 -0
  208. package/registry/otp-input/files/src/lib/kit.tsx +219 -0
  209. package/registry/otp-input.json +17 -0
  210. package/registry/pagination-dots/files/src/components/ui/pagination-dots.tsx +262 -0
  211. package/registry/pagination-dots/files/src/lib/kit.tsx +219 -0
  212. package/registry/pagination-dots.json +15 -0
  213. package/registry/parallax-layers.json +15 -0
  214. package/registry/parallax-scroll.json +17 -0
  215. package/registry/password-input/files/src/components/ui/floating-input.tsx +528 -0
  216. package/registry/password-input/files/src/components/ui/password-input.tsx +271 -0
  217. package/registry/password-input/files/src/lib/kit.tsx +219 -0
  218. package/registry/password-input.json +19 -0
  219. package/registry/payment-card/files/src/components/ui/flip-card.tsx +205 -0
  220. package/registry/payment-card/files/src/components/ui/payment-card.tsx +385 -0
  221. package/registry/payment-card/files/src/components/ui/tilt-card.tsx +635 -0
  222. package/registry/payment-card/files/src/lib/kit.tsx +219 -0
  223. package/registry/payment-card.json +21 -0
  224. package/registry/pixel-bar.json +14 -0
  225. package/registry/polar-area.json +14 -0
  226. package/registry/profile.json +17 -0
  227. package/registry/progress/files/src/components/ui/progress.tsx +883 -0
  228. package/registry/progress/files/src/lib/kit.tsx +219 -0
  229. package/registry/progress-steps/files/src/components/ui/progress-steps.tsx +559 -0
  230. package/registry/progress-steps/files/src/lib/kit.tsx +219 -0
  231. package/registry/progress-steps.json +18 -0
  232. package/registry/progress.json +17 -0
  233. package/registry/radio-group/files/src/components/ui/radio-group.tsx +327 -0
  234. package/registry/radio-group/files/src/lib/kit.tsx +219 -0
  235. package/registry/radio-group.json +18 -0
  236. package/registry/rating.json +18 -0
  237. package/registry/ring-carousel.json +15 -0
  238. package/registry/scratch-card.json +18 -0
  239. package/registry/search-bar/files/src/components/ui/search-bar.tsx +530 -0
  240. package/registry/search-bar/files/src/lib/kit.tsx +219 -0
  241. package/registry/search-bar.json +17 -0
  242. package/registry/search-results.json +30 -0
  243. package/registry/segmented-control/files/src/components/ui/segmented-control.tsx +341 -0
  244. package/registry/segmented-control/files/src/lib/kit.tsx +219 -0
  245. package/registry/segmented-control.json +18 -0
  246. package/registry/select/files/src/components/ui/select.tsx +1398 -0
  247. package/registry/select/files/src/lib/kit.tsx +219 -0
  248. package/registry/select.json +17 -0
  249. package/registry/settings.json +26 -0
  250. package/registry/shimmer-button.json +17 -0
  251. package/registry/sign-in.json +26 -0
  252. package/registry/skeleton/files/src/components/ui/skeleton.tsx +799 -0
  253. package/registry/skeleton/files/src/lib/kit.tsx +219 -0
  254. package/registry/skeleton.json +17 -0
  255. package/registry/slide-to-confirm.json +18 -0
  256. package/registry/slider/files/src/components/ui/slider.tsx +716 -0
  257. package/registry/slider/files/src/lib/kit.tsx +219 -0
  258. package/registry/slider.json +17 -0
  259. package/registry/spin-wheel.json +18 -0
  260. package/registry/spinner/files/src/components/ui/spinner.tsx +335 -0
  261. package/registry/spinner/files/src/lib/kit.tsx +219 -0
  262. package/registry/spinner.json +17 -0
  263. package/registry/stepper/files/src/components/ui/stepper.tsx +646 -0
  264. package/registry/stepper/files/src/lib/kit.tsx +219 -0
  265. package/registry/stepper.json +17 -0
  266. package/registry/swipe-deck.json +15 -0
  267. package/registry/swipeable-row.json +15 -0
  268. package/registry/switch/files/src/components/ui/switch.tsx +344 -0
  269. package/registry/switch/files/src/lib/kit.tsx +219 -0
  270. package/registry/switch.json +18 -0
  271. package/registry/tabs/files/src/components/ui/tabs.tsx +539 -0
  272. package/registry/tabs/files/src/lib/kit.tsx +219 -0
  273. package/registry/tabs.json +17 -0
  274. package/registry/tag-input/files/src/components/ui/tag-input.tsx +720 -0
  275. package/registry/tag-input/files/src/lib/kit.tsx +219 -0
  276. package/registry/tag-input.json +17 -0
  277. package/registry/textarea/files/src/components/ui/textarea.tsx +553 -0
  278. package/registry/textarea/files/src/lib/kit.tsx +219 -0
  279. package/registry/textarea.json +17 -0
  280. package/registry/ticket-card.json +24 -0
  281. package/registry/ticket-shape/files/src/lib/kit.tsx +219 -0
  282. package/registry/ticket-shape/files/src/lib/ticket-shape.tsx +543 -0
  283. package/registry/ticket-shape.json +17 -0
  284. package/registry/tilt-card/files/src/components/ui/tilt-card.tsx +635 -0
  285. package/registry/tilt-card/files/src/lib/kit.tsx +219 -0
  286. package/registry/tilt-card.json +17 -0
  287. package/registry/time-picker/files/src/components/ui/time-picker.tsx +1159 -0
  288. package/registry/time-picker/files/src/lib/kit.tsx +219 -0
  289. package/registry/time-picker.json +17 -0
  290. package/registry/timeline/files/src/components/ui/avatar.tsx +790 -0
  291. package/registry/timeline/files/src/components/ui/timeline.tsx +1059 -0
  292. package/registry/timeline/files/src/lib/kit.tsx +219 -0
  293. package/registry/timeline-chart.json +14 -0
  294. package/registry/timeline.json +20 -0
  295. package/registry/toast/files/src/components/ui/button.tsx +274 -0
  296. package/registry/toast/files/src/components/ui/toast.tsx +1284 -0
  297. package/registry/toast/files/src/lib/kit.tsx +219 -0
  298. package/registry/toast.json +20 -0
  299. package/registry/treemap.json +14 -0
  300. package/registry/waterfall.json +14 -0
  301. package/registry/wheel-list.json +17 -0
  302. package/schema/components.schema.json +13 -0
@@ -0,0 +1,822 @@
1
+ import React, { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ Animated,
4
+ PanResponder,
5
+ Pressable,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ type LayoutChangeEvent,
10
+ type StyleProp,
11
+ type ViewStyle,
12
+ } from 'react-native';
13
+ import Svg, { Path } from 'react-native-svg';
14
+
15
+ import { alpha, motion, readableOn, useKit } from '@/lib/kit';
16
+
17
+ export type CalendarMode = 'single' | 'range' | 'multiple';
18
+
19
+ export interface DateRange {
20
+ start: Date | null;
21
+ end: Date | null;
22
+ }
23
+
24
+ export type CalendarValue = Date | DateRange | Date[] | null;
25
+
26
+ /** `{ 'YYYY-MM-DD': colour | colour[] }` — up to three dots under a date. Build keys with `toDateKey`. */
27
+ export type CalendarMarks = Record<string, string | string[]>;
28
+
29
+ export interface CalendarBaseProps {
30
+ /** Controlled visible month (any date inside it). */
31
+ month?: Date;
32
+ /** Month shown first (default: the selected date, else today). */
33
+ defaultMonth?: Date;
34
+ onMonthChange?: (month: Date) => void;
35
+ /** 0 = Sunday, 1 = Monday. */
36
+ weekStartsOn?: 0 | 1;
37
+ /** BCP 47 locale for month and weekday names (default: device locale). */
38
+ locale?: string;
39
+ /** Earliest selectable day. */
40
+ minDate?: Date;
41
+ /** Latest selectable day. */
42
+ maxDate?: Date;
43
+ /** Return true to disable a day (booked, holidays…). */
44
+ isDateDisabled?: (date: Date) => boolean;
45
+ /** Coloured dots under dates: `{ [toDateKey(date)]: '#22c55e' | ['#22c55e', '#f59e0b'] }`. */
46
+ markedDates?: CalendarMarks;
47
+ /** Fill whole days with a colour (birthdays, shifts, people…): `{ [toDateKey(date)]: '#f472b6' }`. */
48
+ highlightedDates?: Record<string, string>;
49
+ /** `tiles` puts every day on a soft tile; `minimal` shows bare numbers. */
50
+ variant?: 'tiles' | 'minimal';
51
+ /** Weekday header: `narrow` (M T W) or `short` (Mon Tue Wed). */
52
+ weekdayFormat?: 'narrow' | 'short';
53
+ /** Show a "Today" pill in the header while another month is on screen. */
54
+ showTodayButton?: boolean;
55
+ /** Show the faded trailing / leading days of the neighbouring months. */
56
+ showOutsideDays?: boolean;
57
+ /** Ring and tint today's date. */
58
+ showToday?: boolean;
59
+ /** Always render six week rows so the height never changes between months. */
60
+ fixedWeeks?: boolean;
61
+ /** Tap the month title to jump through a month / year grid. */
62
+ monthPicker?: boolean;
63
+ /** Most dates selectable in `multiple` mode. */
64
+ max?: number;
65
+ /** Selection and today colour (default: theme accent). */
66
+ accent?: string;
67
+ style?: StyleProp<ViewStyle>;
68
+ }
69
+
70
+ export interface CalendarSingleProps {
71
+ mode?: 'single';
72
+ value?: Date | null;
73
+ defaultValue?: Date | null;
74
+ onChange?: (date: Date) => void;
75
+ }
76
+
77
+ export interface CalendarRangeProps {
78
+ mode: 'range';
79
+ value?: DateRange | null;
80
+ defaultValue?: DateRange | null;
81
+ /** Fires on every tap: first with `{ start, end: null }`, then with both ends. */
82
+ onChange?: (range: DateRange) => void;
83
+ }
84
+
85
+ export interface CalendarMultipleProps {
86
+ mode: 'multiple';
87
+ value?: Date[];
88
+ defaultValue?: Date[];
89
+ onChange?: (dates: Date[]) => void;
90
+ }
91
+
92
+ export type CalendarProps = CalendarBaseProps & (CalendarSingleProps | CalendarRangeProps | CalendarMultipleProps);
93
+
94
+ type FlatProps = CalendarBaseProps & {
95
+ mode?: CalendarMode;
96
+ value?: CalendarValue;
97
+ defaultValue?: CalendarValue;
98
+ onChange?: (value: CalendarValue) => void;
99
+ };
100
+
101
+ const CELL_H = 48;
102
+ const CIRCLE = 38;
103
+ const CIRCLE_TOP = 2;
104
+ /** Day tile corner from the kit radius: square at 0, a full circle from radius 16 up. */
105
+ const tileRadius = (md: number) => Math.min(CIRCLE / 2, Math.round(md * 1.25));
106
+ const EN_MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
107
+ const EN_DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
108
+
109
+ /* ───────────────────────── date helpers (plain Date, local time) ───────────────────────── */
110
+
111
+ /** Local `YYYY-MM-DD` key for `markedDates`. */
112
+ export function toDateKey(date: Date) {
113
+ const m = date.getMonth() + 1;
114
+ const d = date.getDate();
115
+ return `${date.getFullYear()}-${m < 10 ? '0' : ''}${m}-${d < 10 ? '0' : ''}${d}`;
116
+ }
117
+ /** Midnight of the same local day. */
118
+ export function startOfDay(date: Date) {
119
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
120
+ }
121
+ /** Same local calendar day. */
122
+ export function isSameDay(a?: Date | null, b?: Date | null) {
123
+ return !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
124
+ }
125
+ /** `date` moved by `n` days (DST-safe). */
126
+ export function addDays(date: Date, n: number) {
127
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + n);
128
+ }
129
+ const startOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1);
130
+ const addMonths = (d: Date, n: number) => new Date(d.getFullYear(), d.getMonth() + n, 1);
131
+ const monthIndex = (d: Date) => d.getFullYear() * 12 + d.getMonth();
132
+
133
+ function buildWeeks(month: Date, weekStartsOn: number, fixed: boolean) {
134
+ const y = month.getFullYear();
135
+ const m = month.getMonth();
136
+ const offset = (new Date(y, m, 1).getDay() - weekStartsOn + 7) % 7;
137
+ const days = new Date(y, m + 1, 0).getDate();
138
+ const rows = fixed ? 6 : Math.ceil((offset + days) / 7);
139
+ const weeks: Date[][] = [];
140
+ for (let r = 0; r < rows; r++) {
141
+ const row: Date[] = [];
142
+ for (let c = 0; c < 7; c++) row.push(new Date(y, m, 1 - offset + r * 7 + c));
143
+ weeks.push(row);
144
+ }
145
+ return weeks;
146
+ }
147
+
148
+ function useFormatters(locale?: string) {
149
+ return useMemo(() => {
150
+ const make = (o: Intl.DateTimeFormatOptions, fallback: (d: Date) => string) => {
151
+ try {
152
+ const f = new Intl.DateTimeFormat(locale, o);
153
+ return (d: Date) => f.format(d);
154
+ } catch {
155
+ return fallback;
156
+ }
157
+ };
158
+ return {
159
+ title: make({ month: 'long', year: 'numeric' }, (d) => `${EN_MONTHS[d.getMonth()]} ${d.getFullYear()}`),
160
+ month: make({ month: 'short' }, (d) => EN_MONTHS[d.getMonth()].slice(0, 3)),
161
+ weekday: make({ weekday: 'short' }, (d) => EN_DAYS[d.getDay()]),
162
+ weekdayNarrow: make({ weekday: 'narrow' }, (d) => EN_DAYS[d.getDay()][0]),
163
+ weekdayLong: make({ weekday: 'long' }, (d) => EN_DAYS[d.getDay()]),
164
+ full: make({ weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }, (d) => d.toDateString()),
165
+ };
166
+ }, [locale]);
167
+ }
168
+
169
+ /* ───────────────────────────────────── Calendar ───────────────────────────────────── */
170
+
171
+ /**
172
+ * Month calendar with single, range and multi-date selection. Months slide in from the
173
+ * direction you travel (tap the chevrons or swipe the grid), the title opens a month / year
174
+ * grid, selections pop with a spring and ranges draw one continuous band with rounded ends.
175
+ * Supports min / max / custom disabled days, a today ring and coloured event dots.
176
+ */
177
+ export function Calendar(props: CalendarProps) {
178
+ const {
179
+ mode = 'single',
180
+ value,
181
+ defaultValue,
182
+ onChange,
183
+ month,
184
+ defaultMonth,
185
+ onMonthChange,
186
+ weekStartsOn = 0,
187
+ locale,
188
+ minDate,
189
+ maxDate,
190
+ isDateDisabled,
191
+ markedDates,
192
+ highlightedDates,
193
+ variant = 'tiles',
194
+ weekdayFormat = 'narrow',
195
+ showTodayButton = true,
196
+ showOutsideDays = false,
197
+ showToday = true,
198
+ fixedWeeks = false,
199
+ monthPicker = true,
200
+ max,
201
+ accent,
202
+ style,
203
+ } = props as unknown as FlatProps;
204
+ const { colors, radius } = useKit();
205
+ const tint = accent ?? colors.accent;
206
+ const fmt = useFormatters(locale);
207
+
208
+ const [inner, setInner] = useState<CalendarValue>(defaultValue ?? null);
209
+ const current = value !== undefined ? value : inner;
210
+ const single = mode === 'single' && current instanceof Date ? current : null;
211
+ const range = mode === 'range' && current && !(current instanceof Date) && !Array.isArray(current) ? current : null;
212
+ const many = mode === 'multiple' && Array.isArray(current) ? current : null;
213
+
214
+ const [innerMonth, setInnerMonth] = useState(() => {
215
+ const today = new Date();
216
+ const fallback = minDate && minDate > today ? minDate : maxDate && maxDate < today ? maxDate : today;
217
+ return startOfMonth(defaultMonth ?? single ?? range?.start ?? many?.[0] ?? fallback);
218
+ });
219
+ const controlledKey = month ? monthIndex(month) : null;
220
+ const visible = useMemo(
221
+ () => (controlledKey !== null ? new Date(Math.floor(controlledKey / 12), controlledKey % 12, 1) : innerMonth),
222
+ [controlledKey, innerMonth]
223
+ );
224
+
225
+ const minM = minDate ? monthIndex(minDate) : -Infinity;
226
+ const maxM = maxDate ? monthIndex(maxDate) : Infinity;
227
+ const goTo = useCallback(
228
+ (m: Date) => {
229
+ const next = startOfMonth(m);
230
+ const idx = monthIndex(next);
231
+ if (idx < minM || idx > maxM) return;
232
+ if (month === undefined) setInnerMonth(next);
233
+ onMonthChange?.(next);
234
+ },
235
+ [month, onMonthChange, minM, maxM]
236
+ );
237
+
238
+ /* month slide transition */
239
+ const [width, setWidth] = useState(0);
240
+ const slide = useRef(new Animated.Value(1)).current; // native
241
+ const [leaving, setLeaving] = useState<{ month: Date; dir: number } | null>(null);
242
+ const dirRef = useRef(1);
243
+ const shownRef = useRef(visible);
244
+ const visibleKey = monthIndex(visible);
245
+ useLayoutEffect(() => {
246
+ const prev = shownRef.current;
247
+ if (monthIndex(prev) === visibleKey) return;
248
+ shownRef.current = visible;
249
+ const dir = visibleKey > monthIndex(prev) ? 1 : -1;
250
+ dirRef.current = dir;
251
+ setLeaving({ month: prev, dir });
252
+ slide.stopAnimation();
253
+ slide.setValue(0);
254
+ Animated.spring(slide, { toValue: 1, useNativeDriver: true, ...motion.gentle }).start(({ finished }) => {
255
+ if (finished) setLeaving(null);
256
+ });
257
+ // eslint-disable-next-line react-hooks/exhaustive-deps
258
+ }, [visibleKey]);
259
+
260
+ /* days / months / years view */
261
+ const [view, setView] = useState<'days' | 'months' | 'years'>('days');
262
+ const [pickYear, setPickYear] = useState(visible.getFullYear());
263
+ const viewAnim = useRef(new Animated.Value(1)).current; // native
264
+ const [gridH, setGridH] = useState(0);
265
+ const switchView = (next: 'days' | 'months' | 'years') => {
266
+ if (next !== 'days' && view === 'days') setPickYear(visible.getFullYear());
267
+ setView(next);
268
+ viewAnim.setValue(0);
269
+ Animated.spring(viewAnim, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
270
+ };
271
+
272
+ /* selection */
273
+ const stateRef = useRef({ mode, current, value, onChange, max, visible, goTo });
274
+ stateRef.current = { mode, current, value, onChange, max, visible, goTo };
275
+ const onDayPress = useCallback((d: Date) => {
276
+ const s = stateRef.current;
277
+ let next: CalendarValue;
278
+ if (s.mode === 'range') {
279
+ const r = s.current && !(s.current instanceof Date) && !Array.isArray(s.current) ? s.current : null;
280
+ if (!r?.start || r.end || d < r.start) next = { start: d, end: null };
281
+ else next = { start: r.start, end: d };
282
+ } else if (s.mode === 'multiple') {
283
+ const list = Array.isArray(s.current) ? s.current : [];
284
+ const has = list.some((x) => isSameDay(x, d));
285
+ if (has) next = list.filter((x) => !isSameDay(x, d));
286
+ else if (s.max !== undefined && list.length >= s.max) return;
287
+ else next = [...list, d].sort((a, b) => a.getTime() - b.getTime());
288
+ } else {
289
+ next = d;
290
+ }
291
+ if (s.value === undefined) setInner(next);
292
+ s.onChange?.(next);
293
+ if (monthIndex(d) !== monthIndex(s.visible)) s.goTo(d);
294
+ }, []);
295
+
296
+ const pan = useMemo(
297
+ () =>
298
+ PanResponder.create({
299
+ onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 14 && Math.abs(g.dx) > Math.abs(g.dy) * 1.4,
300
+ onPanResponderRelease: (_, g) => {
301
+ const s = stateRef.current;
302
+ if (g.dx < -40 || g.vx < -0.5) s.goTo(addMonths(s.visible, 1));
303
+ else if (g.dx > 40 || g.vx > 0.5) s.goTo(addMonths(s.visible, -1));
304
+ },
305
+ }),
306
+ []
307
+ );
308
+
309
+ const weekdays = useMemo(() => {
310
+ const base = new Date(2023, 0, 1); // a Sunday
311
+ return Array.from({ length: 7 }, (_, i) => {
312
+ const d = addDays(base, (i + weekStartsOn) % 7);
313
+ return { short: weekdayFormat === 'narrow' ? fmt.weekdayNarrow(d) : fmt.weekday(d), long: fmt.weekdayLong(d) };
314
+ });
315
+ }, [fmt, weekStartsOn, weekdayFormat]);
316
+
317
+ const gridProps: Omit<MonthGridProps, 'month'> = {
318
+ weekStartsOn,
319
+ fixedWeeks,
320
+ showOutsideDays,
321
+ showToday,
322
+ minDate,
323
+ maxDate,
324
+ isDateDisabled,
325
+ markedDates,
326
+ highlightedDates,
327
+ tiles: variant === 'tiles',
328
+ singleKey: single ? toDateKey(single) : undefined,
329
+ rangeStart: range?.start ? startOfDay(range.start).getTime() : undefined,
330
+ rangeEnd: range?.end ? startOfDay(range.end).getTime() : undefined,
331
+ manyKeys: many ? many.map(toDateKey).join('|') : undefined,
332
+ tint,
333
+ fullLabel: fmt.full,
334
+ onDayPress,
335
+ };
336
+
337
+ const W = width || 320;
338
+ const dir = dirRef.current;
339
+ const prevDisabled = view === 'days' ? monthIndex(visible) - 1 < minM : view === 'months' ? pickYear * 12 - 1 < minM : false;
340
+ const nextDisabled = view === 'days' ? monthIndex(visible) + 1 > maxM : view === 'months' ? (pickYear + 1) * 12 > maxM : false;
341
+ const decadeStart = Math.floor(pickYear / 12) * 12;
342
+
343
+ const step = (n: number) => {
344
+ if (view === 'days') goTo(addMonths(visible, n));
345
+ else if (view === 'months') setPickYear((y) => y + n);
346
+ else setPickYear((y) => y + n * 12);
347
+ };
348
+
349
+ const title =
350
+ view === 'days' ? fmt.title(visible) : view === 'months' ? String(pickYear) : `${decadeStart} – ${decadeStart + 11}`;
351
+ const today = new Date();
352
+ const todayM = monthIndex(today);
353
+ const showJump = showTodayButton && view === 'days' && monthIndex(visible) !== todayM && todayM >= minM && todayM <= maxM;
354
+ const tileR = tileRadius(radius.md);
355
+
356
+ return (
357
+ <View style={[styles.root, style]} onLayout={(e: LayoutChangeEvent) => setWidth(e.nativeEvent.layout.width)}>
358
+ <View style={styles.header}>
359
+ <Pressable
360
+ onPress={monthPicker ? () => switchView(view === 'days' ? 'months' : view === 'months' ? 'years' : 'days') : undefined}
361
+ disabled={!monthPicker}
362
+ accessibilityRole={monthPicker ? 'button' : 'header'}
363
+ accessibilityLabel={monthPicker ? `${title}, ${view === 'days' ? 'choose month' : view === 'months' ? 'choose year' : 'back to days'}` : title}
364
+ hitSlop={6}
365
+ style={({ pressed }) => [styles.titleBtn, { borderRadius: radius.sm, backgroundColor: pressed ? colors.muted : 'transparent' }]}
366
+ >
367
+ <Animated.Text
368
+ numberOfLines={1}
369
+ style={[
370
+ styles.title,
371
+ { color: view === 'days' ? colors.foreground : tint },
372
+ view === 'days'
373
+ ? {
374
+ opacity: slide.interpolate({ inputRange: [0, 1], outputRange: [0.2, 1], extrapolate: 'clamp' }),
375
+ transform: [{ translateX: slide.interpolate({ inputRange: [0, 1], outputRange: [dir * 14, 0] }) }],
376
+ }
377
+ : null,
378
+ ]}
379
+ >
380
+ {title}
381
+ </Animated.Text>
382
+ {monthPicker ? <Chevron turned={view !== 'days'} color={view === 'days' ? colors.mutedForeground : tint} /> : null}
383
+ </Pressable>
384
+ <View style={styles.navs}>
385
+ {showJump ? (
386
+ <Pressable
387
+ onPress={() => goTo(today)}
388
+ accessibilityRole="button"
389
+ accessibilityLabel="Go to today"
390
+ hitSlop={4}
391
+ style={({ pressed }) => [styles.todayBtn, { borderRadius: radius.capsule(32), backgroundColor: pressed ? colors.secondary : colors.muted }]}
392
+ >
393
+ <Text style={[styles.todayText, { color: tint }]}>Today</Text>
394
+ </Pressable>
395
+ ) : null}
396
+ <NavButton dir={-1} disabled={prevDisabled} onPress={() => step(-1)} />
397
+ <NavButton dir={1} disabled={nextDisabled} onPress={() => step(1)} />
398
+ </View>
399
+ </View>
400
+
401
+ {view === 'days' ? (
402
+ <Animated.View
403
+ style={{
404
+ opacity: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
405
+ transform: [{ scale: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [1.04, 1] }) }],
406
+ }}
407
+ >
408
+ <View style={styles.weekRow} accessibilityElementsHidden importantForAccessibility="no-hide-descendants">
409
+ {weekdays.map((w) => (
410
+ <Text key={w.long} numberOfLines={1} style={[styles.weekday, { color: colors.mutedForeground }]}>
411
+ {w.short}
412
+ </Text>
413
+ ))}
414
+ </View>
415
+ <View {...pan.panHandlers} style={styles.gridClip} onLayout={(e) => setGridH(e.nativeEvent.layout.height)}>
416
+ {leaving ? (
417
+ <Animated.View
418
+ pointerEvents="none"
419
+ style={[
420
+ styles.leaving,
421
+ {
422
+ opacity: slide.interpolate({ inputRange: [0, 0.7], outputRange: [1, 0], extrapolate: 'clamp' }),
423
+ transform: [{ translateX: slide.interpolate({ inputRange: [0, 1], outputRange: [0, -leaving.dir * W * 0.6] }) }],
424
+ },
425
+ ]}
426
+ >
427
+ <MonthGrid month={leaving.month} {...gridProps} />
428
+ </Animated.View>
429
+ ) : null}
430
+ <Animated.View
431
+ style={{
432
+ opacity: slide.interpolate({ inputRange: [0.15, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
433
+ transform: [{ translateX: slide.interpolate({ inputRange: [0, 1], outputRange: [dir * W * 0.6, 0] }) }],
434
+ }}
435
+ >
436
+ <MonthGrid month={visible} {...gridProps} />
437
+ </Animated.View>
438
+ </View>
439
+ </Animated.View>
440
+ ) : (
441
+ <Animated.View
442
+ style={[
443
+ styles.pickGrid,
444
+ { minHeight: gridH ? gridH + 28 : undefined },
445
+ {
446
+ opacity: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0, 1], extrapolate: 'clamp' }),
447
+ transform: [{ scale: viewAnim.interpolate({ inputRange: [0, 1], outputRange: [0.94, 1] }) }],
448
+ },
449
+ ]}
450
+ >
451
+ {Array.from({ length: 12 }, (_, i) => {
452
+ const isMonths = view === 'months';
453
+ const idx = isMonths ? pickYear * 12 + i : 0;
454
+ const year = decadeStart + i;
455
+ const label = isMonths ? fmt.month(new Date(pickYear, i, 1)) : String(year);
456
+ const selected = isMonths ? idx === monthIndex(visible) : year === visible.getFullYear();
457
+ const isNow = isMonths ? idx === monthIndex(today) : year === today.getFullYear();
458
+ const disabled = isMonths ? idx < minM || idx > maxM : year * 12 + 11 < minM || year * 12 > maxM;
459
+ return (
460
+ <Pressable
461
+ key={label + i}
462
+ disabled={disabled}
463
+ onPress={() => {
464
+ if (isMonths) {
465
+ goTo(new Date(pickYear, i, 1));
466
+ switchView('days');
467
+ } else {
468
+ setPickYear(year);
469
+ switchView('months');
470
+ }
471
+ }}
472
+ accessibilityRole="button"
473
+ accessibilityLabel={isMonths ? fmt.title(new Date(pickYear, i, 1)) : label}
474
+ accessibilityState={{ selected, disabled }}
475
+ style={styles.pickCell}
476
+ >
477
+ {({ pressed }) => (
478
+ <View
479
+ style={[
480
+ styles.pickInner,
481
+ {
482
+ borderRadius: Math.min(20, tileR),
483
+ backgroundColor: selected ? tint : pressed ? colors.secondary : variant === 'tiles' ? colors.muted : 'transparent',
484
+ borderColor: isNow && !selected ? alpha(tint, 0.45) : 'transparent',
485
+ opacity: disabled ? 0.35 : 1,
486
+ },
487
+ ]}
488
+ >
489
+ <Text style={[styles.pickText, { color: selected ? readableOn(tint) : isNow ? tint : colors.foreground }]}>{label}</Text>
490
+ </View>
491
+ )}
492
+ </Pressable>
493
+ );
494
+ })}
495
+ </Animated.View>
496
+ )}
497
+ </View>
498
+ );
499
+ }
500
+
501
+ /* ───────────────────────────────────── grid ───────────────────────────────────── */
502
+
503
+ interface MonthGridProps {
504
+ month: Date;
505
+ weekStartsOn: number;
506
+ fixedWeeks: boolean;
507
+ showOutsideDays: boolean;
508
+ showToday: boolean;
509
+ minDate?: Date;
510
+ maxDate?: Date;
511
+ isDateDisabled?: (date: Date) => boolean;
512
+ markedDates?: CalendarMarks;
513
+ highlightedDates?: Record<string, string>;
514
+ tiles: boolean;
515
+ singleKey?: string;
516
+ rangeStart?: number;
517
+ rangeEnd?: number;
518
+ manyKeys?: string;
519
+ tint: string;
520
+ fullLabel: (d: Date) => string;
521
+ onDayPress: (d: Date) => void;
522
+ }
523
+
524
+ const MonthGrid = memo(function MonthGrid({
525
+ month,
526
+ weekStartsOn,
527
+ fixedWeeks,
528
+ showOutsideDays,
529
+ showToday,
530
+ minDate,
531
+ maxDate,
532
+ isDateDisabled,
533
+ markedDates,
534
+ highlightedDates,
535
+ tiles,
536
+ singleKey,
537
+ rangeStart,
538
+ rangeEnd,
539
+ manyKeys,
540
+ tint,
541
+ fullLabel,
542
+ onDayPress,
543
+ }: MonthGridProps) {
544
+ const weeks = useMemo(() => buildWeeks(month, weekStartsOn, fixedWeeks), [month, weekStartsOn, fixedWeeks]);
545
+ const many = useMemo(() => (manyKeys ? new Set(manyKeys.split('|')) : null), [manyKeys]);
546
+ const todayKey = toDateKey(new Date());
547
+ const min = minDate ? startOfDay(minDate).getTime() : -Infinity;
548
+ const max = maxDate ? startOfDay(maxDate).getTime() : Infinity;
549
+ const m = month.getMonth();
550
+ const hasEnd = rangeStart !== undefined && rangeEnd !== undefined && rangeEnd > rangeStart;
551
+
552
+ return (
553
+ <View>
554
+ {weeks.map((week, r) => (
555
+ <View key={r} style={styles.week}>
556
+ {week.map((d, c) => {
557
+ const inMonth = d.getMonth() === m;
558
+ if (!inMonth && !showOutsideDays) return <View key={c} style={styles.cell} />;
559
+ const key = toDateKey(d);
560
+ const t = d.getTime();
561
+ const disabled = t < min || t > max || !!isDateDisabled?.(d);
562
+ const isStart = rangeStart === t;
563
+ const isEnd = rangeEnd === t;
564
+ const selected = key === singleKey || isStart || isEnd || !!many?.has(key);
565
+ const inBand = hasEnd && t >= rangeStart! && t <= rangeEnd!;
566
+ // A band edge is rounded at the start / end of a week row, or where hidden outside days begin.
567
+ const firstVisible = c === 0 || (!showOutsideDays && d.getDate() === 1);
568
+ const lastVisible = c === 6 || (!showOutsideDays && addDays(d, 1).getMonth() !== m);
569
+ return (
570
+ <DayCell
571
+ key={c}
572
+ date={d}
573
+ label={String(d.getDate())}
574
+ a11y={`${fullLabel(d)}${key === todayKey ? ', today' : ''}${markedDates?.[key] ? ', has events' : ''}`}
575
+ outside={!inMonth}
576
+ disabled={disabled}
577
+ selected={selected}
578
+ today={showToday && key === todayKey}
579
+ band={
580
+ // No half-band off the edge of a row: a start in the last column, an end in the first
581
+ // column or a one-day range has nothing to connect to.
582
+ !inBand || (isStart && isEnd) || (isStart && lastVisible) || (isEnd && firstVisible)
583
+ ? 'none'
584
+ : isStart
585
+ ? 'start'
586
+ : isEnd
587
+ ? 'end'
588
+ : 'mid'
589
+ }
590
+ roundLeft={firstVisible}
591
+ roundRight={lastVisible}
592
+ marks={markedDates?.[key]}
593
+ highlight={highlightedDates?.[key]}
594
+ tile={tiles && inMonth && !inBand}
595
+ tint={tint}
596
+ onPress={onDayPress}
597
+ />
598
+ );
599
+ })}
600
+ </View>
601
+ ))}
602
+ </View>
603
+ );
604
+ });
605
+
606
+ interface DayCellProps {
607
+ date: Date;
608
+ label: string;
609
+ a11y: string;
610
+ outside: boolean;
611
+ disabled: boolean;
612
+ selected: boolean;
613
+ today: boolean;
614
+ band: 'none' | 'start' | 'mid' | 'end';
615
+ roundLeft: boolean;
616
+ roundRight: boolean;
617
+ marks?: string | string[];
618
+ highlight?: string;
619
+ tile: boolean;
620
+ tint: string;
621
+ onPress: (d: Date) => void;
622
+ }
623
+
624
+ const DayCell = memo(function DayCell({
625
+ date,
626
+ label,
627
+ a11y,
628
+ outside,
629
+ disabled,
630
+ selected,
631
+ today,
632
+ band,
633
+ roundLeft,
634
+ roundRight,
635
+ marks,
636
+ highlight,
637
+ tile,
638
+ tint,
639
+ onPress,
640
+ }: DayCellProps) {
641
+ const { colors, radius } = useKit();
642
+ const r = tileRadius(radius.md);
643
+ const pop = useRef(new Animated.Value(selected ? 1 : 0)).current; // native: circle scale + opacity
644
+ const bandIn = useRef(new Animated.Value(band === 'none' ? 0 : 1)).current; // native: band opacity
645
+ useEffect(() => {
646
+ Animated.spring(pop, { toValue: selected ? 1 : 0, useNativeDriver: true, ...motion.bouncy }).start();
647
+ }, [selected, pop]);
648
+ useEffect(() => {
649
+ Animated.timing(bandIn, { toValue: band === 'none' ? 0 : 1, duration: motion.normal, useNativeDriver: true }).start();
650
+ }, [band, bandIn]);
651
+
652
+ const on = readableOn(tint);
653
+ const dots = marks ? (Array.isArray(marks) ? marks : [marks]).slice(0, 3) : null;
654
+ const textColor = selected ? on : highlight ? readableOn(highlight) : disabled ? colors.mutedForeground : today ? tint : colors.foreground;
655
+ const bandRound = r;
656
+
657
+ return (
658
+ <Pressable
659
+ onPress={() => onPress(date)}
660
+ disabled={disabled}
661
+ accessibilityRole="button"
662
+ accessibilityLabel={a11y}
663
+ accessibilityState={{ selected, disabled }}
664
+ style={styles.cell}
665
+ >
666
+ {({ pressed, hovered }: { pressed: boolean; hovered?: boolean }) => (
667
+ <>
668
+ {band !== 'none' ? (
669
+ <Animated.View
670
+ pointerEvents="none"
671
+ style={[
672
+ styles.band,
673
+ {
674
+ backgroundColor: alpha(tint, 0.14),
675
+ opacity: bandIn,
676
+ left: band === 'start' ? '50%' : 0,
677
+ right: band === 'end' ? '50%' : 0,
678
+ borderTopLeftRadius: band === 'mid' && roundLeft ? bandRound : 0,
679
+ borderBottomLeftRadius: band === 'mid' && roundLeft ? bandRound : 0,
680
+ borderTopRightRadius: band === 'mid' && roundRight ? bandRound : 0,
681
+ borderBottomRightRadius: band === 'mid' && roundRight ? bandRound : 0,
682
+ },
683
+ ]}
684
+ />
685
+ ) : null}
686
+ <View
687
+ pointerEvents="none"
688
+ style={[
689
+ styles.circle,
690
+ {
691
+ borderRadius: r,
692
+ backgroundColor:
693
+ !selected && !disabled && (pressed || hovered)
694
+ ? highlight
695
+ ? alpha(highlight, 0.8)
696
+ : tile
697
+ ? colors.secondary
698
+ : colors.muted
699
+ : highlight ?? (tile ? colors.muted : 'transparent'),
700
+ borderColor: today && !selected ? tint : 'transparent',
701
+ opacity: disabled ? 0.45 : 1,
702
+ },
703
+ ]}
704
+ />
705
+ <Animated.View
706
+ pointerEvents="none"
707
+ style={[
708
+ styles.circle,
709
+ styles.fill,
710
+ {
711
+ borderRadius: r,
712
+ backgroundColor: tint,
713
+ opacity: pop.interpolate({ inputRange: [0, 0.4], outputRange: [0, 1], extrapolate: 'clamp' }),
714
+ transform: [{ scale: pop.interpolate({ inputRange: [0, 1], outputRange: [0.45, 1] }) }],
715
+ },
716
+ ]}
717
+ />
718
+ <View pointerEvents="none" style={styles.label}>
719
+ <Text
720
+ style={[
721
+ styles.day,
722
+ { color: textColor },
723
+ (today || selected) && styles.dayStrong,
724
+ outside && !selected && styles.outside,
725
+ disabled && styles.disabledText,
726
+ ]}
727
+ >
728
+ {label}
729
+ </Text>
730
+ </View>
731
+ {dots ? (
732
+ <View pointerEvents="none" style={styles.dots}>
733
+ {dots.map((color, i) => (
734
+ <View key={i} style={[styles.dot, { backgroundColor: color, opacity: disabled ? 0.5 : 1 }]} />
735
+ ))}
736
+ </View>
737
+ ) : null}
738
+ </>
739
+ )}
740
+ </Pressable>
741
+ );
742
+ });
743
+
744
+ /* ───────────────────────────────────── bits ───────────────────────────────────── */
745
+
746
+ function NavButton({ dir, disabled, onPress }: { dir: 1 | -1; disabled: boolean; onPress: () => void }) {
747
+ const { colors } = useKit();
748
+ return (
749
+ <Pressable
750
+ onPress={onPress}
751
+ disabled={disabled}
752
+ hitSlop={4}
753
+ accessibilityRole="button"
754
+ accessibilityLabel={dir < 0 ? 'Previous' : 'Next'}
755
+ accessibilityState={{ disabled }}
756
+ style={({ pressed }) => [styles.nav, { backgroundColor: pressed ? colors.secondary : colors.muted, opacity: disabled ? 0.3 : 1 }]}
757
+ >
758
+ <Svg width={16} height={16} viewBox="0 0 24 24" fill="none">
759
+ <Path
760
+ d={dir < 0 ? 'm14.5 6-6 6 6 6' : 'm9.5 6 6 6-6 6'}
761
+ stroke={colors.foreground}
762
+ strokeWidth={2}
763
+ strokeLinecap="round"
764
+ strokeLinejoin="round"
765
+ />
766
+ </Svg>
767
+ </Pressable>
768
+ );
769
+ }
770
+
771
+ function Chevron({ turned, color }: { turned: boolean; color: string }) {
772
+ const turn = useRef(new Animated.Value(turned ? 1 : 0)).current; // native
773
+ useEffect(() => {
774
+ Animated.spring(turn, { toValue: turned ? 1 : 0, useNativeDriver: true, ...motion.snappy }).start();
775
+ }, [turned, turn]);
776
+ return (
777
+ <Animated.View style={{ transform: [{ rotate: turn.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '180deg'] }) }] }}>
778
+ <Svg width={16} height={16} viewBox="0 0 24 24" fill="none">
779
+ <Path d="m6 9 6 6 6-6" stroke={color} strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
780
+ </Svg>
781
+ </Animated.View>
782
+ );
783
+ }
784
+
785
+ const styles = StyleSheet.create({
786
+ root: { alignSelf: 'stretch' },
787
+ header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10, minHeight: 40 },
788
+ titleBtn: { flexDirection: 'row', alignItems: 'center', gap: 4, paddingHorizontal: 6, paddingVertical: 6, marginLeft: -6, flexShrink: 1 },
789
+ title: { fontSize: 17, fontWeight: '400', letterSpacing: -0.3, flexShrink: 1 },
790
+ navs: { flexDirection: 'row', alignItems: 'center', gap: 6 },
791
+ nav: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
792
+ todayBtn: { height: 32, paddingHorizontal: 12, alignItems: 'center', justifyContent: 'center' },
793
+ todayText: { fontSize: 13, fontWeight: '400' },
794
+ weekRow: { flexDirection: 'row', marginBottom: 6 },
795
+ weekday: { flex: 1, textAlign: 'center', fontSize: 13, fontWeight: '400', letterSpacing: 0.2 },
796
+ gridClip: { overflow: 'hidden' },
797
+ leaving: { position: 'absolute', top: 0, left: 0, right: 0 },
798
+ week: { flexDirection: 'row', marginVertical: 1 },
799
+ cell: { flex: 1, height: CELL_H, alignItems: 'center' },
800
+ label: { position: 'absolute', top: CIRCLE_TOP, height: CIRCLE, left: 0, right: 0, alignItems: 'center', justifyContent: 'center' },
801
+ band: { position: 'absolute', top: CIRCLE_TOP, height: CIRCLE },
802
+ circle: {
803
+ position: 'absolute',
804
+ top: CIRCLE_TOP,
805
+ width: CIRCLE,
806
+ height: CIRCLE,
807
+ borderRadius: CIRCLE / 2,
808
+ borderWidth: 1.5,
809
+ alignSelf: 'center',
810
+ },
811
+ fill: { borderWidth: 0 },
812
+ day: { fontSize: 15, fontWeight: '400', fontVariant: ['tabular-nums'] },
813
+ dayStrong: { fontWeight: '400' },
814
+ outside: { opacity: 0.4 },
815
+ disabledText: { opacity: 0.35 },
816
+ dots: { position: 'absolute', top: CIRCLE_TOP + CIRCLE + 3, flexDirection: 'row', gap: 2.5 },
817
+ dot: { width: 5, height: 5, borderRadius: 2.5 },
818
+ pickGrid: { flexDirection: 'row', flexWrap: 'wrap', alignContent: 'center', paddingTop: 4 },
819
+ pickCell: { width: '33.333%', height: 56, alignItems: 'center', justifyContent: 'center' },
820
+ pickInner: { minWidth: 76, height: 40, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center', borderWidth: 1.5 },
821
+ pickText: { fontSize: 15, fontWeight: '400' },
822
+ });