@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,580 @@
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
+ Modal,
8
+ PanResponder,
9
+ Platform,
10
+ Pressable,
11
+ StyleSheet,
12
+ Text,
13
+ View,
14
+ useWindowDimensions,
15
+ type StyleProp,
16
+ type ViewStyle,
17
+ } from 'react-native';
18
+ import Svg, { Path } from 'react-native-svg';
19
+
20
+ import { alpha, motion, readableOn, useKit } from '@/lib/kit';
21
+
22
+ /**
23
+ * `inset` — iOS: grouped rounded cards floating above the home indicator, with a separate Cancel card (default).
24
+ * `list` — Material: an edge-to-edge sheet with a drag handle and icon rows.
25
+ * `grid` — share-sheet tiles.
26
+ */
27
+ export type ActionSheetVariant = 'inset' | 'list' | 'grid';
28
+
29
+ const ICONS = {
30
+ camera: ['M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z', 'M12 17a4 4 0 100-8 4 4 0 000 8z'],
31
+ image: [
32
+ 'M5 3h14a2 2 0 012 2v14a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2z',
33
+ 'M8.5 10a1.5 1.5 0 100-3 1.5 1.5 0 000 3z',
34
+ 'M21 15l-5-5L5 21',
35
+ ],
36
+ trash: ['M3 6h18', 'M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6', 'M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2', 'M10 11v6M14 11v6'],
37
+ share: ['M4 12v8a2 2 0 002 2h12a2 2 0 002-2v-8', 'M16 6l-4-4-4 4', 'M12 2v13'],
38
+ link: ['M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71', 'M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71'],
39
+ copy: [
40
+ 'M11 9h9a2 2 0 012 2v9a2 2 0 01-2 2h-9a2 2 0 01-2-2v-9a2 2 0 012-2z',
41
+ 'M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1',
42
+ ],
43
+ mail: ['M4 4h16a2 2 0 012 2v12a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2z', 'M22 6l-10 7L2 6'],
44
+ message: ['M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z'],
45
+ download: ['M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4', 'M7 10l5 5 5-5', 'M12 15V3'],
46
+ flag: ['M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z', 'M4 22v-7'],
47
+ edit: ['M12 20h9', 'M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4 12.5-12.5z'],
48
+ user: ['M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2', 'M12 11a4 4 0 100-8 4 4 0 000 8z'],
49
+ bookmark: ['M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z'],
50
+ 'eye-off': [
51
+ 'M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94',
52
+ 'M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19',
53
+ 'M14.12 14.12a3 3 0 11-4.24-4.24',
54
+ 'M1 1l22 22',
55
+ ],
56
+ send: ['M22 2L11 13', 'M22 2l-7 20-4-9-9-4 20-7z'],
57
+ more: ['M5 12h.01M12 12h.01M19 12h.01'],
58
+ reply: ['M9 17l-5-5 5-5', 'M20 18v-2a4 4 0 00-4-4H4'],
59
+ forward: ['M15 17l5-5-5-5', 'M4 18v-2a4 4 0 014-4h12'],
60
+ pin: [
61
+ 'M12 17v5',
62
+ 'M9 10.8a2 2 0 01-1.1 1.8l-1.8.9A2 2 0 005 15.2V17h14v-1.8a2 2 0 00-1.1-1.8l-1.8-.9a2 2 0 01-1.1-1.8V6h1a2 2 0 000-4H8a2 2 0 000 4h1z',
63
+ ],
64
+ archive: ['M21 8v11a2 2 0 01-2 2H5a2 2 0 01-2-2V8', 'M2 3h20v5H2z', 'M10 12h4'],
65
+ printer: ['M6 9V2h12v7', 'M6 18H4a2 2 0 01-2-2v-5a2 2 0 012-2h16a2 2 0 012 2v5a2 2 0 01-2 2h-2', 'M6 14h12v8H6z'],
66
+ } as const;
67
+
68
+ export type ActionSheetIconName = keyof typeof ICONS;
69
+
70
+ export interface ActionSheetAction {
71
+ label: string;
72
+ /** A built-in icon name or your own node. */
73
+ icon?: ActionSheetIconName | React.ReactNode;
74
+ /** Danger colour; use for actions that delete or can't be undone. */
75
+ destructive?: boolean;
76
+ disabled?: boolean;
77
+ /** Tile colour in the 'grid' variant. */
78
+ color?: string;
79
+ /** Return `false` to keep the sheet open. */
80
+ onPress?: () => void | boolean;
81
+ }
82
+
83
+ export interface ActionSheetProps {
84
+ open?: boolean;
85
+ defaultOpen?: boolean;
86
+ onOpenChange?: (open: boolean) => void;
87
+ title?: string;
88
+ message?: string;
89
+ actions: ActionSheetAction[];
90
+ /** Label of the separate cancel button; `null` hides it (default 'Cancel'). */
91
+ cancelLabel?: string | null;
92
+ /** Called on Cancel, backdrop tap, drag-to-dismiss, back and Escape. */
93
+ onCancel?: () => void;
94
+ /** 'inset' iOS grouped cards · 'list' Material edge-to-edge list · 'grid' share-sheet tiles. */
95
+ variant?: ActionSheetVariant;
96
+ /** Extra bottom padding for the home indicator. Defaults to a safe phone inset; pass `useSafeAreaInsets().bottom` to match the device. */
97
+ bottomInset?: number;
98
+ /** Colour of regular text-only actions and Cancel in 'inset' (default: theme accent). */
99
+ accent?: string;
100
+ /** Close on backdrop tap and drag-down (default true). When false the sheet only rubber-bands. */
101
+ dismissible?: boolean;
102
+ /** Light impact on open, selection tick on picking an action (default true). */
103
+ haptics?: boolean;
104
+ style?: StyleProp<ViewStyle>;
105
+ }
106
+
107
+ const DEFAULT_BOTTOM_INSET = Platform.select({ ios: 20, android: 12, default: 16 }) ?? 16;
108
+
109
+ /** iOS-style resistance: moves 1:1 at first, then ever more slowly towards `limit`. */
110
+ function rubber(x: number, limit: number) {
111
+ return (1 - 1 / ((x * 0.55) / limit + 1)) * limit;
112
+ }
113
+
114
+ /**
115
+ * A bottom action sheet. iOS look by default: grouped rounded cards with hairline separators,
116
+ * an optional title and message, and a separate Cancel card. It springs up over a fading scrim
117
+ * and follows your finger anywhere on the sheet — fling it or pull past a third of its height to
118
+ * dismiss; pulling up rubber-bands. Rows highlight on touch and tick a selection haptic.
119
+ * Also a Material edge-to-edge list and a share-sheet grid.
120
+ */
121
+ export function ActionSheet({
122
+ open,
123
+ defaultOpen = false,
124
+ onOpenChange,
125
+ title,
126
+ message,
127
+ actions,
128
+ cancelLabel = 'Cancel',
129
+ onCancel,
130
+ variant = 'inset',
131
+ bottomInset = DEFAULT_BOTTOM_INSET,
132
+ accent,
133
+ dismissible = true,
134
+ haptics = true,
135
+ style,
136
+ }: ActionSheetProps) {
137
+ const { colors, radius, dark } = useKit();
138
+ const { height: winH } = useWindowDimensions();
139
+ const reduceMotion = useReduceMotion();
140
+ const tint = accent ?? colors.accent;
141
+ const [inner, setInner] = useState(defaultOpen);
142
+ const isOpen = open ?? inner;
143
+ // Stays mounted while the exit animation runs; cleared in its completion callback.
144
+ const keep = useRef(isOpen);
145
+ const [, rerender] = useState(0);
146
+ const mounted = isOpen || keep.current;
147
+
148
+ // Both native-driven, on the sheet node: open progress (0 → 1) and finger offset.
149
+ const t = useRef(new Animated.Value(0)).current;
150
+ const drag = useRef(new Animated.Value(0)).current;
151
+ const sheetH = useRef(320);
152
+ const flung = useRef(false);
153
+
154
+ const setOpen = (next: boolean) => {
155
+ if (open === undefined) setInner(next);
156
+ onOpenChange?.(next);
157
+ };
158
+ const dismiss = () => {
159
+ if (!dismissible) return;
160
+ onCancel?.();
161
+ setOpen(false);
162
+ };
163
+ const live = useRef({ dismiss, dismissible });
164
+ live.current = { dismiss, dismissible };
165
+
166
+ useEffect(() => {
167
+ if (isOpen) {
168
+ keep.current = true;
169
+ drag.setValue(0);
170
+ flung.current = false;
171
+ if (haptics) Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {});
172
+ (reduceMotion
173
+ ? Animated.timing(t, { toValue: 1, duration: 200, easing: Easing.out(Easing.quad), useNativeDriver: true })
174
+ : Animated.spring(t, { toValue: 1, useNativeDriver: true, ...motion.gentle })
175
+ ).start();
176
+ } else {
177
+ // Exit is quicker than the entry; after a fling it keeps the finger's momentum (ease-out).
178
+ Animated.timing(t, {
179
+ toValue: 0,
180
+ duration: reduceMotion ? 160 : 240,
181
+ easing: flung.current ? Easing.out(Easing.cubic) : Easing.in(Easing.quad),
182
+ useNativeDriver: true,
183
+ }).start(({ finished }) => {
184
+ if (!finished) return;
185
+ keep.current = false;
186
+ rerender((n) => n + 1);
187
+ });
188
+ }
189
+ // eslint-disable-next-line react-hooks/exhaustive-deps
190
+ }, [isOpen]);
191
+
192
+ // Escape closes on web.
193
+ useEffect(() => {
194
+ if (!isOpen || Platform.OS !== 'web' || typeof document === 'undefined') return;
195
+ const onKey = (e: KeyboardEvent) => e.key === 'Escape' && live.current.dismiss();
196
+ document.addEventListener('keydown', onKey);
197
+ return () => document.removeEventListener('keydown', onKey);
198
+ }, [isOpen]);
199
+
200
+ // Drag anywhere on the sheet: a vertical move steals the touch from the row under the finger.
201
+ const pan = useMemo(() => {
202
+ const settle = (velocity = 0) =>
203
+ Animated.spring(drag, { toValue: 0, velocity, useNativeDriver: true, ...motion.snappy }).start();
204
+ return PanResponder.create({
205
+ onMoveShouldSetPanResponderCapture: (_, g) => Math.abs(g.dy) > 8 && Math.abs(g.dy) > Math.abs(g.dx) * 1.4,
206
+ onPanResponderGrant: () => drag.stopAnimation(),
207
+ onPanResponderMove: (_, g) => {
208
+ const down = live.current.dismissible ? g.dy : rubber(g.dy, 90);
209
+ drag.setValue(g.dy > 0 ? down : -rubber(-g.dy, 70));
210
+ },
211
+ onPanResponderRelease: (_, g) => {
212
+ const far = g.dy > sheetH.current * 0.3;
213
+ const flick = g.vy > 0.9 && g.dy > 12;
214
+ if (live.current.dismissible && (far || flick)) {
215
+ flung.current = true;
216
+ live.current.dismiss();
217
+ } else settle(g.vy);
218
+ },
219
+ onPanResponderTerminationRequest: () => false,
220
+ onPanResponderTerminate: () => settle(),
221
+ });
222
+ }, [drag]);
223
+
224
+ const scrim = Animated.multiply(
225
+ t,
226
+ drag.interpolate({ inputRange: [0, Math.max(200, sheetH.current)], outputRange: [1, 0], extrapolate: 'clamp' }),
227
+ );
228
+
229
+ const pick = (a: ActionSheetAction) => {
230
+ if (a.disabled) return;
231
+ if (haptics) Haptics.selectionAsync().catch(() => {});
232
+ if (a.onPress?.() === false) return;
233
+ setOpen(false);
234
+ };
235
+
236
+ const edge = alpha(colors.foreground, dark ? 0.1 : 0.06);
237
+ const separator = alpha(colors.foreground, dark ? 0.12 : 0.09);
238
+ const highlight = alpha(colors.foreground, dark ? 0.1 : 0.06);
239
+ const dangerHighlight = alpha(colors.danger, dark ? 0.16 : 0.09);
240
+ const icon = (a: ActionSheetAction, color: string, size: number) =>
241
+ typeof a.icon === 'string' ? <SheetIcon name={a.icon as ActionSheetIconName} color={color} size={size} /> : a.icon;
242
+
243
+ const list = variant === 'list';
244
+ const header =
245
+ title || message ? (
246
+ <View style={list ? styles.headLeft : styles.head}>
247
+ {title ? (
248
+ <Text
249
+ accessibilityRole="header"
250
+ style={[list ? styles.titleLarge : styles.title, { color: list ? colors.foreground : colors.mutedForeground }]}
251
+ >
252
+ {title}
253
+ </Text>
254
+ ) : null}
255
+ {message ? (
256
+ <Text style={[styles.message, { color: colors.mutedForeground, textAlign: list ? 'left' : 'center' }]}>
257
+ {message}
258
+ </Text>
259
+ ) : null}
260
+ </View>
261
+ ) : null;
262
+
263
+ const corner = radius.scale(16);
264
+ const cancelCard =
265
+ cancelLabel !== null ? (
266
+ <Card corner={corner} bg={colors.popover} edge={edge} dark={dark}>
267
+ <Row onPress={dismiss} highlight={highlight} label={cancelLabel} style={styles.row}>
268
+ <Text style={[styles.rowLabel, styles.center, { color: variant === 'inset' ? tint : colors.foreground }]}>
269
+ {cancelLabel}
270
+ </Text>
271
+ </Row>
272
+ </Card>
273
+ ) : null;
274
+
275
+ let body: React.ReactNode;
276
+ if (variant === 'grid') {
277
+ body = (
278
+ <>
279
+ <Card corner={corner} bg={colors.popover} edge={edge} dark={dark}>
280
+ {header}
281
+ <View style={[styles.grid, header ? { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: separator } : null]}>
282
+ {actions.map((a) => {
283
+ const fill = a.destructive ? colors.danger : (a.color ?? alpha(colors.foreground, dark ? 0.1 : 0.06));
284
+ const fg = a.destructive || a.color ? readableOn(fill) : colors.foreground;
285
+ return (
286
+ <Row
287
+ key={a.label}
288
+ onPress={() => pick(a)}
289
+ disabled={a.disabled}
290
+ label={a.label}
291
+ highlight={highlight}
292
+ corner={radius.scale(14)}
293
+ style={styles.tile}
294
+ >
295
+ <View style={[styles.tileIcon, { backgroundColor: fill, borderRadius: radius.scale(16) }]}>
296
+ {icon(a, fg, 24)}
297
+ </View>
298
+ <Text numberOfLines={2} style={[styles.tileLabel, { color: a.destructive ? colors.danger : colors.foreground }]}>
299
+ {a.label}
300
+ </Text>
301
+ </Row>
302
+ );
303
+ })}
304
+ </View>
305
+ </Card>
306
+ {cancelCard}
307
+ </>
308
+ );
309
+ } else if (list) {
310
+ body = (
311
+ <View
312
+ style={[
313
+ styles.listSheet,
314
+ {
315
+ backgroundColor: colors.popover,
316
+ borderTopLeftRadius: radius.scale(28),
317
+ borderTopRightRadius: radius.scale(28),
318
+ borderColor: edge,
319
+ shadowOpacity: dark ? 0.5 : 0.12,
320
+ },
321
+ ]}
322
+ >
323
+ <View style={[styles.handle, { backgroundColor: alpha(colors.foreground, dark ? 0.3 : 0.2), borderRadius: radius.capsule(4) }]} />
324
+ {header}
325
+ {actions.map((a) => {
326
+ const color = a.destructive ? colors.danger : colors.foreground;
327
+ return (
328
+ <Row
329
+ key={a.label}
330
+ onPress={() => pick(a)}
331
+ disabled={a.disabled}
332
+ label={a.label}
333
+ highlight={a.destructive ? dangerHighlight : highlight}
334
+ corner={radius.md}
335
+ style={styles.listRow}
336
+ >
337
+ {a.icon ? (
338
+ <View style={styles.listIcon}>{icon(a, a.destructive ? colors.danger : colors.mutedForeground, 22)}</View>
339
+ ) : null}
340
+ <Text style={[styles.listLabel, { color }]} numberOfLines={1}>
341
+ {a.label}
342
+ </Text>
343
+ </Row>
344
+ );
345
+ })}
346
+ {cancelLabel !== null ? (
347
+ <>
348
+ <View style={[styles.divider, { backgroundColor: separator }]} />
349
+ <Row onPress={dismiss} highlight={highlight} corner={radius.md} label={cancelLabel} style={styles.listRow}>
350
+ <Text style={[styles.listLabel, styles.center, { color: colors.mutedForeground }]}>{cancelLabel}</Text>
351
+ </Row>
352
+ </>
353
+ ) : null}
354
+ <View style={{ height: 8 + bottomInset }} />
355
+ {/* Skirt below the sheet so the spring's overshoot never shows a gap at the bottom edge. */}
356
+ <View style={[styles.skirt, { backgroundColor: colors.popover }]} />
357
+ </View>
358
+ );
359
+ } else {
360
+ // iOS: text-only rows are centred in the accent colour; rows with icons read like a menu
361
+ // (label leading, icon trailing, label colour).
362
+ const withIcons = actions.some((a) => a.icon);
363
+ body = (
364
+ <>
365
+ <Card corner={corner} bg={colors.popover} edge={edge} dark={dark}>
366
+ {header}
367
+ {actions.map((a, i) => {
368
+ const color = a.destructive ? colors.danger : withIcons ? colors.foreground : tint;
369
+ return (
370
+ <Row
371
+ key={a.label}
372
+ onPress={() => pick(a)}
373
+ disabled={a.disabled}
374
+ label={a.label}
375
+ highlight={highlight}
376
+ style={[
377
+ styles.row,
378
+ withIcons && styles.rowMenu,
379
+ (i > 0 || header) && { borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: separator },
380
+ ]}
381
+ >
382
+ <Text style={[styles.rowLabel, withIcons ? styles.rowLabelMenu : styles.center, { color }]} numberOfLines={1}>
383
+ {a.label}
384
+ </Text>
385
+ {withIcons ? <View style={styles.rowIcon}>{a.icon ? icon(a, color, 22) : null}</View> : null}
386
+ </Row>
387
+ );
388
+ })}
389
+ </Card>
390
+ {cancelCard}
391
+ </>
392
+ );
393
+ }
394
+
395
+ const inset = !list;
396
+ const grabber = inset ? (
397
+ <View style={[styles.grabber, { backgroundColor: alpha('#ffffff', dark ? 0.3 : 0.75), borderRadius: radius.capsule(5) }]} />
398
+ ) : null;
399
+
400
+ return (
401
+ <Modal visible={mounted} transparent animationType="none" statusBarTranslucent onRequestClose={dismiss}>
402
+ <Animated.View style={[StyleSheet.absoluteFill, { backgroundColor: colors.overlay, opacity: scrim }]}>
403
+ <Pressable
404
+ style={StyleSheet.absoluteFill}
405
+ onPress={dismiss}
406
+ accessibilityRole="button"
407
+ accessibilityLabel="Dismiss"
408
+ importantForAccessibility={cancelLabel !== null ? 'no' : 'auto'}
409
+ accessible={cancelLabel === null}
410
+ />
411
+ </Animated.View>
412
+ <View style={styles.host} pointerEvents="box-none">
413
+ <Animated.View
414
+ {...pan.panHandlers}
415
+ accessibilityViewIsModal
416
+ onLayout={(e) => {
417
+ sheetH.current = Math.max(1, Math.round(e.nativeEvent.layout.height));
418
+ }}
419
+ style={[
420
+ styles.sheet,
421
+ inset && { paddingHorizontal: 8, paddingBottom: 8 + bottomInset },
422
+ {
423
+ opacity: reduceMotion ? t : 1,
424
+ transform: [
425
+ {
426
+ translateY: reduceMotion
427
+ ? drag
428
+ : Animated.add(t.interpolate({ inputRange: [0, 1], outputRange: [winH, 0] }), drag),
429
+ },
430
+ ],
431
+ },
432
+ style,
433
+ ]}
434
+ >
435
+ {grabber}
436
+ {body}
437
+ </Animated.View>
438
+ </View>
439
+ </Modal>
440
+ );
441
+ }
442
+
443
+ /** Rounded group with a hairline edge and two shadow layers (soft ambient + tight contact). */
444
+ function Card({
445
+ corner,
446
+ bg,
447
+ edge,
448
+ dark,
449
+ children,
450
+ }: {
451
+ corner: number;
452
+ bg: string;
453
+ edge: string;
454
+ dark: boolean;
455
+ children: React.ReactNode;
456
+ }) {
457
+ return (
458
+ <View style={[styles.cardAmbient, { borderRadius: corner, backgroundColor: bg, shadowOpacity: dark ? 0.5 : 0.14 }]}>
459
+ <View style={[styles.cardContact, { borderRadius: corner, backgroundColor: bg, shadowOpacity: dark ? 0.3 : 0.06 }]}>
460
+ <View style={{ borderRadius: corner, overflow: 'hidden', borderWidth: StyleSheet.hairlineWidth, borderColor: edge }}>
461
+ {children}
462
+ </View>
463
+ </View>
464
+ </View>
465
+ );
466
+ }
467
+
468
+ /** Pressable row whose highlight appears instantly on touch and fades out on release (native driver). */
469
+ function Row({
470
+ onPress,
471
+ disabled,
472
+ label,
473
+ highlight,
474
+ corner = 0,
475
+ style,
476
+ children,
477
+ }: {
478
+ onPress: () => void;
479
+ disabled?: boolean;
480
+ label: string;
481
+ highlight: string;
482
+ corner?: number;
483
+ style?: StyleProp<ViewStyle>;
484
+ children: React.ReactNode;
485
+ }) {
486
+ const hl = useRef(new Animated.Value(0)).current;
487
+ return (
488
+ <Pressable
489
+ onPress={onPress}
490
+ disabled={disabled}
491
+ onPressIn={() => {
492
+ hl.stopAnimation();
493
+ hl.setValue(1);
494
+ }}
495
+ onPressOut={() => Animated.timing(hl, { toValue: 0, duration: 280, useNativeDriver: true }).start()}
496
+ accessibilityRole="button"
497
+ accessibilityLabel={label}
498
+ accessibilityState={{ disabled: !!disabled }}
499
+ style={[style, disabled ? styles.disabled : null]}
500
+ >
501
+ <Animated.View
502
+ pointerEvents="none"
503
+ style={[StyleSheet.absoluteFill, { backgroundColor: highlight, borderRadius: corner, opacity: hl }]}
504
+ />
505
+ {children}
506
+ </Pressable>
507
+ );
508
+ }
509
+
510
+ function SheetIcon({ name, color, size }: { name: ActionSheetIconName; color: string; size: number }) {
511
+ return (
512
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
513
+ {ICONS[name].map((d) => (
514
+ <Path
515
+ key={d}
516
+ d={d}
517
+ stroke={color}
518
+ strokeWidth={name === 'more' ? 3.2 : 1.8}
519
+ strokeLinecap="round"
520
+ strokeLinejoin="round"
521
+ />
522
+ ))}
523
+ </Svg>
524
+ );
525
+ }
526
+
527
+ function useReduceMotion() {
528
+ const [reduce, setReduce] = useState(false);
529
+ useEffect(() => {
530
+ let alive = true;
531
+ AccessibilityInfo.isReduceMotionEnabled()
532
+ .then((v) => alive && setReduce(v))
533
+ .catch(() => {});
534
+ const sub = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduce);
535
+ return () => {
536
+ alive = false;
537
+ sub.remove();
538
+ };
539
+ }, []);
540
+ return reduce;
541
+ }
542
+
543
+ const styles = StyleSheet.create({
544
+ host: { flex: 1, justifyContent: 'flex-end', alignItems: 'center' },
545
+ sheet: { width: '100%', maxWidth: 480, gap: 8 },
546
+ cardAmbient: { shadowColor: '#000', shadowRadius: 24, shadowOffset: { width: 0, height: 10 }, elevation: 10 },
547
+ cardContact: { shadowColor: '#000', shadowRadius: 2, shadowOffset: { width: 0, height: 1 } },
548
+ head: { paddingHorizontal: 24, paddingTop: 15, paddingBottom: 14, alignItems: 'center', gap: 4 },
549
+ headLeft: { paddingHorizontal: 20, paddingTop: 6, paddingBottom: 10, gap: 4 },
550
+ title: { fontSize: 13.5, lineHeight: 18, fontWeight: '400', textAlign: 'center' },
551
+ titleLarge: { fontSize: 17, lineHeight: 22, fontWeight: '400', letterSpacing: -0.2 },
552
+ message: { fontSize: 13, lineHeight: 18, fontWeight: '400' },
553
+ row: { minHeight: 56, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 18 },
554
+ rowMenu: { justifyContent: 'space-between', gap: 12 },
555
+ rowIcon: { width: 24, alignItems: 'center' },
556
+ rowLabel: { flexShrink: 1, fontSize: 18, lineHeight: 23, fontWeight: '400', letterSpacing: -0.3 },
557
+ rowLabelMenu: { flex: 1, fontSize: 17, letterSpacing: -0.2 },
558
+ center: { textAlign: 'center' },
559
+ grabber: { alignSelf: 'center', width: 36, height: 5, marginTop: 2, marginBottom: 2 },
560
+ handle: { alignSelf: 'center', width: 32, height: 4, marginTop: 10, marginBottom: 14 },
561
+ listSheet: {
562
+ borderWidth: StyleSheet.hairlineWidth,
563
+ borderBottomWidth: 0,
564
+ paddingHorizontal: 8,
565
+ shadowColor: '#000',
566
+ shadowRadius: 24,
567
+ shadowOffset: { width: 0, height: -6 },
568
+ elevation: 16,
569
+ },
570
+ listRow: { minHeight: 56, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, gap: 16 },
571
+ listIcon: { width: 24, alignItems: 'center' },
572
+ listLabel: { flex: 1, fontSize: 16, lineHeight: 22, fontWeight: '400', letterSpacing: -0.1 },
573
+ divider: { height: StyleSheet.hairlineWidth, marginVertical: 6, marginHorizontal: 16 },
574
+ skirt: { position: 'absolute', left: 0, right: 0, top: '100%', height: 160 },
575
+ grid: { flexDirection: 'row', flexWrap: 'wrap', paddingHorizontal: 6, paddingVertical: 12 },
576
+ tile: { width: '25%', alignItems: 'center', gap: 7, paddingHorizontal: 2, paddingVertical: 8 },
577
+ tileIcon: { width: 56, height: 56, alignItems: 'center', justifyContent: 'center' },
578
+ tileLabel: { fontSize: 12, lineHeight: 15, fontWeight: '400', textAlign: 'center' },
579
+ disabled: { opacity: 0.38 },
580
+ });