@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,585 @@
1
+ import * as Haptics from 'expo-haptics';
2
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
3
+ import {
4
+ AccessibilityInfo,
5
+ Animated,
6
+ Easing,
7
+ Keyboard,
8
+ Modal,
9
+ PanResponder,
10
+ Platform,
11
+ Pressable,
12
+ ScrollView,
13
+ StyleSheet,
14
+ Text,
15
+ View,
16
+ useWindowDimensions,
17
+ type KeyboardEvent as RNKeyboardEvent,
18
+ type PanResponderGestureState,
19
+ type StyleProp,
20
+ type ViewStyle,
21
+ } from 'react-native';
22
+
23
+ import { alpha, motion, useKit } from '@/lib/kit';
24
+
25
+ export type SnapPoint = number | `${number}%`;
26
+
27
+ export interface BottomSheetProps {
28
+ open: boolean;
29
+ /** Called when the sheet has been dragged, flung, tapped or back-buttoned closed. */
30
+ onClose: () => void;
31
+ /**
32
+ * Heights the sheet rests at: points or percentages of the screen (sorted for you). Omit to size
33
+ * the sheet to its content (capped below the status bar; taller content scrolls).
34
+ */
35
+ snapPoints?: SnapPoint[];
36
+ /** Which snap point it opens at (default 0, the lowest). */
37
+ initialSnap?: number;
38
+ /** Controlled snap index: change it while open to move the sheet. */
39
+ snapIndex?: number;
40
+ onSnapChange?: (index: number) => void;
41
+ /** Allow closing by dragging down / tapping the backdrop (default true). */
42
+ dismissible?: boolean;
43
+ /** Heading under the grabber. */
44
+ title?: string;
45
+ /** Muted line under the title. */
46
+ subtitle?: string;
47
+ /** Node at the trailing end of the title row (e.g. a Reset or Done link). */
48
+ headerRight?: React.ReactNode;
49
+ /** Round ✕ button in the header (default: shown with a title when dismissible and there's no headerRight). */
50
+ showClose?: boolean;
51
+ /** Show the grabber pill (default true). */
52
+ handle?: boolean;
53
+ /**
54
+ * Wrap the body in a ScrollView. Below the top snap point a vertical drag on the body resizes
55
+ * the sheet; at the top snap point the body scrolls, and pulling down from its top collapses it.
56
+ */
57
+ scrollable?: boolean;
58
+ /** Let vertical drags on the body move the sheet (default: true when `scrollable`). Keep false if children contain their own ScrollView. */
59
+ dragFromContent?: boolean;
60
+ /** Pinned to the bottom of the visible sheet at every snap point (e.g. an Apply button). */
61
+ footer?: React.ReactNode;
62
+ /** Lift the sheet above the on-screen keyboard (default true). */
63
+ avoidKeyboard?: boolean;
64
+ /** Light impact on open and a selection tick on each snap (default true). */
65
+ haptics?: boolean;
66
+ /** Max backdrop dim at the highest snap point, 0–1 (default 1 = theme overlay). */
67
+ backdropOpacity?: number;
68
+ /** Extra bottom padding for the home indicator (default: platform safe inset). */
69
+ bottomInset?: number;
70
+ accessibilityLabel?: string;
71
+ children?: React.ReactNode;
72
+ style?: StyleProp<ViewStyle>;
73
+ contentStyle?: StyleProp<ViewStyle>;
74
+ }
75
+
76
+ const DEFAULT_BOTTOM_INSET = Platform.select({ ios: 20, android: 12, default: 16 }) ?? 16;
77
+ /** Kit gentle, firmed up — glides between detents without bouncing past them. */
78
+ const SNAP_SPRING = { ...motion.gentle, damping: 26, stiffness: 220, mass: 0.9 };
79
+ /** Extra sheet below the screen edge, so pulling up past the top snap never shows a gap. */
80
+ const OVERDRAG = 120;
81
+ /** Never let the sheet's top go closer than this to the top of the screen. */
82
+ const TOP_GAP = 24;
83
+
84
+ function toPx(p: SnapPoint, screen: number) {
85
+ return typeof p === 'number' ? p : (parseFloat(p) / 100) * screen;
86
+ }
87
+
88
+ /** Diminishing resistance past the limits (approaches `dimension`). */
89
+ function rubber(distance: number, dimension: number) {
90
+ return (distance * dimension * 0.55) / (dimension + 0.55 * distance);
91
+ }
92
+
93
+ /**
94
+ * Draggable bottom sheet with snap points. It follows your finger, rubber-bands past its limits,
95
+ * settles on the snap point projected from your release velocity (a fling skips ahead or
96
+ * dismisses) with a haptic tick, and the scrim dims in step with its height. Content can scroll
97
+ * once the sheet is fully expanded, a footer stays pinned at every detent, and the keyboard lifts
98
+ * it. PanResponder only.
99
+ */
100
+ export function BottomSheet({
101
+ open,
102
+ onClose,
103
+ snapPoints,
104
+ initialSnap = 0,
105
+ snapIndex,
106
+ onSnapChange,
107
+ dismissible = true,
108
+ title,
109
+ subtitle,
110
+ headerRight,
111
+ showClose,
112
+ handle = true,
113
+ scrollable = false,
114
+ dragFromContent,
115
+ footer,
116
+ avoidKeyboard = true,
117
+ haptics = true,
118
+ backdropOpacity = 1,
119
+ bottomInset = DEFAULT_BOTTOM_INSET,
120
+ accessibilityLabel,
121
+ children,
122
+ style,
123
+ contentStyle,
124
+ }: BottomSheetProps) {
125
+ const { colors, radius, dark } = useKit();
126
+ const reduceMotion = useReduceMotion();
127
+ const { height: screen } = useWindowDimensions();
128
+
129
+ // No snap points: one detent at the content's own height, measured before the sheet slides in.
130
+ const auto = !snapPoints || snapPoints.length === 0;
131
+ const [headerH, setHeaderH] = useState(0);
132
+ const [contentH, setContentH] = useState(0);
133
+ const [footerH, setFooterH] = useState(0);
134
+ const measured = !auto || (headerH > 0 && contentH > 0);
135
+ const autoH = Math.min(headerH + contentH + (footer ? 0 : bottomInset), screen - TOP_GAP);
136
+
137
+ // Snap heights in px, ascending, capped below the status bar. Keyed by value, not array identity.
138
+ const snapKey = auto ? `auto:${Math.round(autoH)}` : snapPoints.join('|');
139
+ const heights = useMemo(
140
+ () =>
141
+ auto
142
+ ? [Math.max(1, autoH)]
143
+ : snapKey
144
+ .split('|')
145
+ .map((p) => Math.min(toPx(p.endsWith('%') ? (p as `${number}%`) : Number(p), screen), screen - TOP_GAP))
146
+ .sort((a, b) => a - b),
147
+ // eslint-disable-next-line react-hooks/exhaustive-deps
148
+ [snapKey, screen],
149
+ );
150
+ const max = heights[heights.length - 1];
151
+ // translateY for each snap (0 = top snap) and for closed.
152
+ const stops = heights.map((h) => max - h);
153
+ const closed = max + 24;
154
+ const last = stops.length - 1;
155
+
156
+ // Native driver: sheet position, keyboard lift, grabber stretch — separate values.
157
+ const y = useRef(new Animated.Value(closed)).current;
158
+ const lift = useRef(new Animated.Value(0)).current;
159
+ const grip = useRef(new Animated.Value(0)).current;
160
+
161
+ const [mounted, setMounted] = useState(open);
162
+ const [expanded, setExpanded] = useState(false); // at the top snap → body scrolls
163
+ const [kbPad, setKbPad] = useState(0);
164
+ const shown = useRef(false); // auto: slid in once measured
165
+ const index = useRef(initialSnap);
166
+ const scrollY = useRef(0);
167
+ const rootRef = useRef<View>(null);
168
+
169
+ const bodyDrags = dragFromContent ?? scrollable;
170
+ const live = useRef({ stops, closed, max, dismissible, onClose, onSnapChange, haptics, reduceMotion, scrollable, bodyDrags, expanded });
171
+ live.current = { stops, closed, max, dismissible, onClose, onSnapChange, haptics, reduceMotion, scrollable, bodyDrags, expanded };
172
+
173
+ const pos = useRef(closed); // last known resting / dragged position (JS mirror of `y`)
174
+ const moving = useRef(false);
175
+
176
+ const settle = (to: number, velocity = 0, after?: () => void) => {
177
+ pos.current = to;
178
+ moving.current = true;
179
+ const anim = live.current.reduceMotion
180
+ ? Animated.timing(y, { toValue: to, duration: 200, easing: Easing.out(Easing.cubic), useNativeDriver: true })
181
+ : Animated.spring(y, {
182
+ toValue: to,
183
+ velocity,
184
+ useNativeDriver: true,
185
+ ...SNAP_SPRING,
186
+ overshootClamping: to === live.current.closed,
187
+ });
188
+ anim.start(({ finished }) => {
189
+ moving.current = false;
190
+ if (finished) after?.();
191
+ });
192
+ };
193
+
194
+ const snapTo = (i: number, velocity = 0) => {
195
+ const s = live.current.stops;
196
+ const k = Math.max(0, Math.min(s.length - 1, i));
197
+ if (index.current !== k) {
198
+ live.current.onSnapChange?.(k);
199
+ if (live.current.haptics) Haptics.selectionAsync().catch(() => {});
200
+ }
201
+ index.current = k;
202
+ setExpanded(k === s.length - 1);
203
+ settle(s[k], velocity);
204
+ };
205
+
206
+ const dismiss = (velocity = 0) => settle(live.current.closed, velocity, () => live.current.onClose());
207
+
208
+ const enter = () => {
209
+ shown.current = true;
210
+ y.setValue(closed);
211
+ lift.setValue(0);
212
+ pos.current = closed;
213
+ scrollY.current = 0;
214
+ index.current = Math.max(0, Math.min(last, snapIndex ?? initialSnap));
215
+ setExpanded(index.current === last);
216
+ if (haptics) Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
217
+ settle(stops[index.current]);
218
+ };
219
+
220
+ useEffect(() => {
221
+ if (open) {
222
+ setMounted(true);
223
+ shown.current = false;
224
+ // Off-screen until there is a height to open to.
225
+ y.setValue(screen);
226
+ if (measured) enter();
227
+ } else if (mounted) {
228
+ // Exit is quicker than the entrance: a short ease-in, no spring.
229
+ Keyboard.dismiss();
230
+ Animated.timing(lift, { toValue: 0, duration: 160, useNativeDriver: true }).start();
231
+ Animated.timing(y, { toValue: closed, duration: 200, easing: Easing.in(Easing.quad), useNativeDriver: true }).start(() =>
232
+ setMounted(false),
233
+ );
234
+ }
235
+ // eslint-disable-next-line react-hooks/exhaustive-deps
236
+ }, [open]);
237
+
238
+ // Auto height: slide in as soon as the content has been measured; afterwards follow its size.
239
+ useEffect(() => {
240
+ if (!open || !auto || !measured) return;
241
+ if (!shown.current) enter();
242
+ else if (!moving.current && index.current === 0) {
243
+ pos.current = 0;
244
+ y.setValue(0);
245
+ }
246
+ // eslint-disable-next-line react-hooks/exhaustive-deps
247
+ }, [open, auto, measured, autoH]);
248
+
249
+ // Controlled snap index.
250
+ useEffect(() => {
251
+ if (open && mounted && snapIndex !== undefined && snapIndex !== index.current) snapTo(snapIndex);
252
+ // eslint-disable-next-line react-hooks/exhaustive-deps
253
+ }, [snapIndex]);
254
+
255
+ // Keyboard: lift the sheet by however much the keyboard overlaps the screen (0 when the window
256
+ // already resized for it), but never push its top past TOP_GAP — the rest becomes scroll padding.
257
+ useEffect(() => {
258
+ if (!mounted || !avoidKeyboard) return;
259
+ const ios = Platform.OS === 'ios';
260
+ const apply = (overlap: number, duration: number) => {
261
+ const visible = live.current.max - pos.current;
262
+ const room = Math.max(0, screen - TOP_GAP - visible);
263
+ const up = Math.min(overlap, room);
264
+ setKbPad(overlap - up);
265
+ Animated.timing(lift, { toValue: -up, duration, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
266
+ };
267
+ const show = Keyboard.addListener(ios ? 'keyboardWillShow' : 'keyboardDidShow', (e: RNKeyboardEvent) => {
268
+ const duration = e.duration || 220;
269
+ const kbTop = e.endCoordinates.screenY;
270
+ if (!rootRef.current) return apply(e.endCoordinates.height, duration);
271
+ rootRef.current.measureInWindow((_x, top, _w, h) => apply(Math.max(0, top + h - kbTop), duration));
272
+ });
273
+ const hide = Keyboard.addListener(ios ? 'keyboardWillHide' : 'keyboardDidHide', (e: RNKeyboardEvent) => {
274
+ setKbPad(0);
275
+ Animated.timing(lift, { toValue: 0, duration: e?.duration || 200, easing: Easing.out(Easing.cubic), useNativeDriver: true }).start();
276
+ });
277
+ return () => {
278
+ show.remove();
279
+ hide.remove();
280
+ };
281
+ }, [mounted, avoidKeyboard, screen, lift]);
282
+
283
+ // Escape closes on web (the Modal handles Android back).
284
+ useEffect(() => {
285
+ if (!mounted || Platform.OS !== 'web' || typeof document === 'undefined') return;
286
+ const onKey = (e: KeyboardEvent) => {
287
+ if (e.key === 'Escape' && live.current.dismissible) dismiss();
288
+ };
289
+ document.addEventListener('keydown', onKey);
290
+ return () => document.removeEventListener('keydown', onKey);
291
+ // eslint-disable-next-line react-hooks/exhaustive-deps
292
+ }, [mounted]);
293
+
294
+ // One drag model for the grabber/header (claims any touch) and the body (captures vertical
295
+ // drags only, so taps, horizontal scrollers and — at the top snap — scrolling still work).
296
+ const pans = useRef(
297
+ (() => {
298
+ const origin = { start: 0 };
299
+ const base = {
300
+ onPanResponderTerminationRequest: () => false,
301
+ onPanResponderGrant: () => {
302
+ origin.start = pos.current;
303
+ Animated.spring(grip, { toValue: 1, useNativeDriver: true, ...motion.snappy }).start();
304
+ if (!moving.current) return;
305
+ moving.current = false;
306
+ y.stopAnimation((v) => {
307
+ if (typeof v === 'number') origin.start = v;
308
+ });
309
+ },
310
+ onPanResponderMove: (_: unknown, g: PanResponderGestureState) => {
311
+ const { closed: bottom, stops: s, dismissible: canClose } = live.current;
312
+ let next = origin.start + g.dy;
313
+ if (next < 0) next = -rubber(-next, OVERDRAG);
314
+ if (!canClose && next > s[0]) next = s[0] + rubber(next - s[0], OVERDRAG);
315
+ pos.current = Math.min(next, bottom + 40);
316
+ y.setValue(pos.current);
317
+ },
318
+ onPanResponderRelease: (_: unknown, g: PanResponderGestureState) => {
319
+ Animated.spring(grip, { toValue: 0, useNativeDriver: true, ...motion.bouncy }).start();
320
+ const { stops: s, closed: bottom, dismissible: canClose } = live.current;
321
+ const current = origin.start + g.dy;
322
+ const projected = current + g.vy * 220;
323
+ const targets = canClose ? [...s, bottom] : s;
324
+ let best = 0;
325
+ targets.forEach((t, i) => {
326
+ if (Math.abs(t - projected) < Math.abs(targets[best] - projected)) best = i;
327
+ });
328
+ if (canClose && (best === s.length || (g.vy > 1.6 && current > s[0]))) dismiss(g.vy);
329
+ else snapTo(Math.min(best, s.length - 1), g.vy);
330
+ },
331
+ onPanResponderTerminate: () => {
332
+ Animated.spring(grip, { toValue: 0, useNativeDriver: true, ...motion.bouncy }).start();
333
+ snapTo(index.current);
334
+ },
335
+ };
336
+ const grab = PanResponder.create({
337
+ ...base,
338
+ onStartShouldSetPanResponder: () => true,
339
+ onMoveShouldSetPanResponder: () => true,
340
+ });
341
+ const body = PanResponder.create({
342
+ ...base,
343
+ onMoveShouldSetPanResponderCapture: (_, g) => {
344
+ const l = live.current;
345
+ if (!l.bodyDrags) return false;
346
+ if (Math.abs(g.dy) < 8 || Math.abs(g.dy) < Math.abs(g.dx) * 1.3) return false;
347
+ if (!l.scrollable || !l.expanded) return true;
348
+ // Fully expanded: the list scrolls; pulling down from its very top collapses the sheet.
349
+ return g.dy > 0 && scrollY.current <= 0;
350
+ },
351
+ });
352
+ return { grab, body };
353
+ })(),
354
+ ).current;
355
+
356
+ if (!mounted) return null;
357
+
358
+ const lowest = Math.max(1, stops[0]);
359
+ const dim = y.interpolate({
360
+ inputRange: [0, lowest, closed],
361
+ outputRange: [backdropOpacity, backdropOpacity * (heights.length > 1 ? 0.7 : 1), 0],
362
+ extrapolate: 'clamp',
363
+ });
364
+ // Keeps the footer on the screen's bottom edge at every detent; it follows the sheet down when closing.
365
+ const footerY = y.interpolate({ inputRange: [0, lowest], outputRange: [0, -lowest], extrapolate: 'clamp' });
366
+
367
+ const surface = dark ? colors.popover : colors.card;
368
+ const hairline = alpha(colors.foreground, dark ? 0.1 : 0.06);
369
+ const corner = radius.scale(28);
370
+ const closeButton = showClose ?? (!!title && dismissible && !headerRight);
371
+ const hasHeader = !!(title || subtitle || headerRight || closeButton);
372
+
373
+ // Auto height always scrolls when its content is taller than the screen allows.
374
+ const scrolls = scrollable || auto;
375
+ const body = scrolls ? (
376
+ <ScrollView
377
+ style={styles.flex}
378
+ onContentSizeChange={auto ? (_w, h) => setContentH(Math.ceil(h)) : undefined}
379
+ contentContainerStyle={[styles.content, { paddingBottom: 12 + (footer ? footerH : 0) + kbPad }, contentStyle]}
380
+ scrollEnabled={expanded}
381
+ bounces={false}
382
+ overScrollMode="never"
383
+ showsVerticalScrollIndicator={expanded && (!auto || headerH + contentH + bottomInset > screen - TOP_GAP)}
384
+ keyboardShouldPersistTaps="handled"
385
+ scrollEventThrottle={16}
386
+ onScroll={(e) => (scrollY.current = e.nativeEvent.contentOffset.y)}
387
+ >
388
+ {children}
389
+ </ScrollView>
390
+ ) : (
391
+ <View
392
+ onLayout={auto ? (e) => setContentH(Math.ceil(e.nativeEvent.layout.height)) : undefined}
393
+ style={[styles.content, !auto && styles.flex, styles.contentEnd, footer ? { paddingBottom: footerH } : null, contentStyle]}
394
+ >
395
+ {children}
396
+ </View>
397
+ );
398
+
399
+ return (
400
+ <Modal
401
+ visible
402
+ transparent
403
+ animationType="none"
404
+ statusBarTranslucent
405
+ onRequestClose={() => (dismissible ? dismiss() : snapTo(0))}
406
+ >
407
+ <View ref={rootRef} style={StyleSheet.absoluteFill} pointerEvents="box-none">
408
+ <Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: colors.overlay, opacity: dim }]}>
409
+ <Pressable
410
+ style={StyleSheet.absoluteFill}
411
+ accessibilityRole="button"
412
+ accessibilityLabel="Close sheet"
413
+ onPress={() => dismissible && dismiss()}
414
+ />
415
+ </Animated.View>
416
+ <View style={styles.dock} pointerEvents="box-none">
417
+ <Animated.View
418
+ accessibilityViewIsModal
419
+ accessibilityLabel={accessibilityLabel ?? title}
420
+ onAccessibilityEscape={() => dismissible && dismiss()}
421
+ style={[
422
+ styles.sheet,
423
+ {
424
+ height: max + OVERDRAG,
425
+ opacity: measured ? 1 : 0,
426
+ shadowOpacity: dark ? 0.5 : 0.12,
427
+ transform: [{ translateY: Animated.add(y, lift) }],
428
+ },
429
+ style,
430
+ ]}
431
+ >
432
+ <View
433
+ style={[
434
+ styles.surface,
435
+ {
436
+ paddingBottom: OVERDRAG + (footer ? 0 : bottomInset),
437
+ backgroundColor: surface,
438
+ borderTopLeftRadius: corner,
439
+ borderTopRightRadius: corner,
440
+ borderColor: hairline,
441
+ shadowOpacity: dark ? 0.35 : 0.06,
442
+ },
443
+ ]}
444
+ >
445
+ <View {...pans.grab.panHandlers} style={styles.grab} onLayout={auto ? (e) => setHeaderH(Math.ceil(e.nativeEvent.layout.height)) : undefined}>
446
+ {handle ? (
447
+ <View
448
+ style={styles.handleArea}
449
+ accessible
450
+ accessibilityRole="adjustable"
451
+ accessibilityLabel="Sheet grabber"
452
+ accessibilityHint="Swipe up or down to resize"
453
+ accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
454
+ onAccessibilityAction={(e) => {
455
+ if (e.nativeEvent.actionName === 'increment') snapTo(index.current + 1);
456
+ else if (index.current === 0 && dismissible) dismiss();
457
+ else snapTo(index.current - 1);
458
+ }}
459
+ >
460
+ <Animated.View
461
+ style={[
462
+ styles.handle,
463
+ {
464
+ borderRadius: radius.capsule(5),
465
+ backgroundColor: alpha(colors.foreground, dark ? 0.26 : 0.18),
466
+ transform: [{ scaleX: grip.interpolate({ inputRange: [0, 1], outputRange: [1, 1.3] }) }],
467
+ },
468
+ ]}
469
+ />
470
+ </View>
471
+ ) : null}
472
+ {hasHeader ? (
473
+ <View style={[styles.header, !handle && { paddingTop: 20 }]}>
474
+ <View style={styles.headerText}>
475
+ {title ? (
476
+ <Text accessibilityRole="header" style={[styles.title, { color: colors.foreground }]} numberOfLines={1}>
477
+ {title}
478
+ </Text>
479
+ ) : null}
480
+ {subtitle ? (
481
+ <Text style={[styles.subtitle, { color: colors.mutedForeground }]} numberOfLines={1}>
482
+ {subtitle}
483
+ </Text>
484
+ ) : null}
485
+ </View>
486
+ {headerRight}
487
+ {closeButton ? (
488
+ <Pressable
489
+ onPress={() => (dismissible ? dismiss() : onClose())}
490
+ hitSlop={10}
491
+ accessibilityRole="button"
492
+ accessibilityLabel="Close"
493
+ style={({ pressed }) => [styles.close, { backgroundColor: alpha(colors.foreground, pressed ? 0.14 : 0.08) }]}
494
+ >
495
+ <View style={[styles.closeBar, { backgroundColor: colors.mutedForeground, transform: [{ rotate: '45deg' }] }]} />
496
+ <View style={[styles.closeBar, { backgroundColor: colors.mutedForeground, transform: [{ rotate: '-45deg' }] }]} />
497
+ </Pressable>
498
+ ) : null}
499
+ </View>
500
+ ) : null}
501
+ </View>
502
+ <View {...pans.body.panHandlers} style={styles.flex}>
503
+ {body}
504
+ </View>
505
+ {footer ? (
506
+ <Animated.View
507
+ onLayout={(e) => setFooterH(e.nativeEvent.layout.height)}
508
+ style={[
509
+ styles.footer,
510
+ {
511
+ bottom: OVERDRAG,
512
+ paddingBottom: bottomInset,
513
+ backgroundColor: surface,
514
+ borderTopColor: hairline,
515
+ transform: [{ translateY: footerY }],
516
+ },
517
+ ]}
518
+ >
519
+ {footer}
520
+ </Animated.View>
521
+ ) : null}
522
+ </View>
523
+ </Animated.View>
524
+ </View>
525
+ </View>
526
+ </Modal>
527
+ );
528
+ }
529
+
530
+ function useReduceMotion() {
531
+ const [reduce, setReduce] = useState(false);
532
+ useEffect(() => {
533
+ let alive = true;
534
+ AccessibilityInfo.isReduceMotionEnabled()
535
+ .then((v) => alive && setReduce(v))
536
+ .catch(() => {});
537
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
538
+ return () => {
539
+ alive = false;
540
+ sub.remove();
541
+ };
542
+ }, []);
543
+ return reduce;
544
+ }
545
+
546
+ const styles = StyleSheet.create({
547
+ flex: { flex: 1 },
548
+ // No text selection while dragging with a mouse on web.
549
+ grab: Platform.OS === 'web' ? ({ userSelect: 'none', cursor: 'grab' } as object) : {},
550
+ dock: { position: 'absolute', left: 0, right: 0, bottom: -OVERDRAG, alignItems: 'center' },
551
+ sheet: {
552
+ width: '100%',
553
+ maxWidth: 640,
554
+ shadowColor: '#000',
555
+ shadowRadius: 32,
556
+ shadowOffset: { width: 0, height: -8 },
557
+ elevation: 24,
558
+ },
559
+ surface: {
560
+ flex: 1,
561
+ borderWidth: StyleSheet.hairlineWidth,
562
+ borderBottomWidth: 0,
563
+ shadowColor: '#000',
564
+ shadowRadius: 3,
565
+ shadowOffset: { width: 0, height: -1 },
566
+ },
567
+ handleArea: { alignItems: 'center', paddingTop: 8, paddingBottom: 10, minHeight: 28 },
568
+ handle: { width: 36, height: 5 },
569
+ header: { flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 20, paddingBottom: 10 },
570
+ headerText: { flex: 1, minWidth: 0, gap: 2 },
571
+ title: { fontSize: 18, lineHeight: 24, fontWeight: '400', letterSpacing: -0.3 },
572
+ close: { width: 30, height: 30, borderRadius: 15, alignItems: 'center', justifyContent: 'center' },
573
+ closeBar: { position: 'absolute', width: 12, height: 1.8, borderRadius: 1 },
574
+ contentEnd: { paddingBottom: 6 },
575
+ subtitle: { fontSize: 13.5, lineHeight: 18, fontWeight: '400' },
576
+ content: { paddingHorizontal: 20, paddingTop: 6 },
577
+ footer: {
578
+ position: 'absolute',
579
+ left: 0,
580
+ right: 0,
581
+ paddingHorizontal: 20,
582
+ paddingTop: 12,
583
+ borderTopWidth: StyleSheet.hairlineWidth,
584
+ },
585
+ });