panelui-native 0.22.6 → 0.28.1

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 (258) hide show
  1. package/README.md +1 -0
  2. package/lib/module/components/accordion/index.js +8 -8
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/alert/index.js +2 -2
  5. package/lib/module/components/alert/index.js.map +1 -1
  6. package/lib/module/components/area-chart/index.js +958 -0
  7. package/lib/module/components/area-chart/index.js.map +1 -0
  8. package/lib/module/components/attachment/index.js +2 -1
  9. package/lib/module/components/attachment/index.js.map +1 -1
  10. package/lib/module/components/avatar/index.js +4 -4
  11. package/lib/module/components/avatar/index.js.map +1 -1
  12. package/lib/module/components/badge/index.js +4 -4
  13. package/lib/module/components/badge/index.js.map +1 -1
  14. package/lib/module/components/bar-chart/index.js +994 -0
  15. package/lib/module/components/bar-chart/index.js.map +1 -0
  16. package/lib/module/components/bottom-sheet/index.js +213 -24
  17. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  18. package/lib/module/components/breadcrumb/index.js +275 -0
  19. package/lib/module/components/breadcrumb/index.js.map +1 -0
  20. package/lib/module/components/button/index.js +11 -6
  21. package/lib/module/components/button/index.js.map +1 -1
  22. package/lib/module/components/calendar/index.js +557 -0
  23. package/lib/module/components/calendar/index.js.map +1 -0
  24. package/lib/module/components/carousel/index.js +680 -0
  25. package/lib/module/components/carousel/index.js.map +1 -0
  26. package/lib/module/components/chip/index.js +6 -6
  27. package/lib/module/components/chip/index.js.map +1 -1
  28. package/lib/module/components/date-picker/index.js +158 -0
  29. package/lib/module/components/date-picker/index.js.map +1 -0
  30. package/lib/module/components/dialog/index.js +7 -2
  31. package/lib/module/components/dialog/index.js.map +1 -1
  32. package/lib/module/components/direction/index.js +15 -20
  33. package/lib/module/components/direction/index.js.map +1 -1
  34. package/lib/module/components/empty-state/index.js +2 -2
  35. package/lib/module/components/empty-state/index.js.map +1 -1
  36. package/lib/module/components/field/index.js +5 -5
  37. package/lib/module/components/field/index.js.map +1 -1
  38. package/lib/module/components/flow/index.js +3 -3
  39. package/lib/module/components/flow/index.js.map +1 -1
  40. package/lib/module/components/frame/index.js +22 -5
  41. package/lib/module/components/frame/index.js.map +1 -1
  42. package/lib/module/components/heatmap-chart/index.js +28 -8
  43. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  44. package/lib/module/components/input-group/index.js +5 -4
  45. package/lib/module/components/input-group/index.js.map +1 -1
  46. package/lib/module/components/item/index.js +9 -9
  47. package/lib/module/components/item/index.js.map +1 -1
  48. package/lib/module/components/label/index.js +4 -4
  49. package/lib/module/components/label/index.js.map +1 -1
  50. package/lib/module/components/line-chart/index.js +165 -184
  51. package/lib/module/components/line-chart/index.js.map +1 -1
  52. package/lib/module/components/loader/index.js +758 -0
  53. package/lib/module/components/loader/index.js.map +1 -0
  54. package/lib/module/components/map/basemap.js +265 -0
  55. package/lib/module/components/map/basemap.js.map +1 -0
  56. package/lib/module/components/map/index.js +850 -0
  57. package/lib/module/components/map/index.js.map +1 -0
  58. package/lib/module/components/map/maplibre.js +87 -0
  59. package/lib/module/components/map/maplibre.js.map +1 -0
  60. package/lib/module/components/marker/index.js +3 -3
  61. package/lib/module/components/marker/index.js.map +1 -1
  62. package/lib/module/components/message/index.js +15 -14
  63. package/lib/module/components/message/index.js.map +1 -1
  64. package/lib/module/components/message-scroller/index.js +5 -4
  65. package/lib/module/components/message-scroller/index.js.map +1 -1
  66. package/lib/module/components/otp-input/index.js +2 -2
  67. package/lib/module/components/popover/index.js +39 -7
  68. package/lib/module/components/popover/index.js.map +1 -1
  69. package/lib/module/components/progress/index.js +8 -2
  70. package/lib/module/components/progress/index.js.map +1 -1
  71. package/lib/module/components/radio-group/index.js +2 -2
  72. package/lib/module/components/radio-group/index.js.map +1 -1
  73. package/lib/module/components/ring-chart/index.js +423 -0
  74. package/lib/module/components/ring-chart/index.js.map +1 -0
  75. package/lib/module/components/section-rail/index.js +6 -6
  76. package/lib/module/components/section-rail/index.js.map +1 -1
  77. package/lib/module/components/select/index.js +10 -4
  78. package/lib/module/components/select/index.js.map +1 -1
  79. package/lib/module/components/separator/index.js +76 -22
  80. package/lib/module/components/separator/index.js.map +1 -1
  81. package/lib/module/components/shimmer/index.js +10 -1
  82. package/lib/module/components/shimmer/index.js.map +1 -1
  83. package/lib/module/components/slider/index.js +19 -5
  84. package/lib/module/components/slider/index.js.map +1 -1
  85. package/lib/module/components/steps/index.js +5 -5
  86. package/lib/module/components/steps/index.js.map +1 -1
  87. package/lib/module/components/switch/index.js +6 -1
  88. package/lib/module/components/switch/index.js.map +1 -1
  89. package/lib/module/components/tabs/index.js +7 -7
  90. package/lib/module/components/tabs/index.js.map +1 -1
  91. package/lib/module/components/timeline/index.js +4 -4
  92. package/lib/module/components/timeline/index.js.map +1 -1
  93. package/lib/module/components/toast/index.js +2 -2
  94. package/lib/module/components/toast/index.js.map +1 -1
  95. package/lib/module/components/toggle-button/index.js +5 -5
  96. package/lib/module/components/toggle-button/index.js.map +1 -1
  97. package/lib/module/components/tooltip/index.js +492 -0
  98. package/lib/module/components/tooltip/index.js.map +1 -0
  99. package/lib/module/components/typography/index.js +7 -2
  100. package/lib/module/components/typography/index.js.map +1 -1
  101. package/lib/module/hooks/index.js +1 -0
  102. package/lib/module/hooks/index.js.map +1 -1
  103. package/lib/module/hooks/use-back-handler.js +37 -0
  104. package/lib/module/hooks/use-back-handler.js.map +1 -0
  105. package/lib/module/hooks/use-direction.js +43 -0
  106. package/lib/module/hooks/use-direction.js.map +1 -0
  107. package/lib/module/icons/index.js +180 -2
  108. package/lib/module/icons/index.js.map +1 -1
  109. package/lib/module/index.js +12 -2
  110. package/lib/module/index.js.map +1 -1
  111. package/lib/module/primitives/keyboard-avoider.js +2 -1
  112. package/lib/module/primitives/keyboard-avoider.js.map +1 -1
  113. package/lib/module/primitives/text.js +55 -11
  114. package/lib/module/primitives/text.js.map +1 -1
  115. package/lib/module/utils/chart.js +279 -0
  116. package/lib/module/utils/chart.js.map +1 -0
  117. package/lib/module/utils/date.js +386 -0
  118. package/lib/module/utils/date.js.map +1 -0
  119. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  120. package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
  121. package/lib/typescript/src/components/area-chart/index.d.ts +237 -0
  122. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -0
  123. package/lib/typescript/src/components/attachment/index.d.ts.map +1 -1
  124. package/lib/typescript/src/components/bar-chart/index.d.ts +270 -0
  125. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -0
  126. package/lib/typescript/src/components/bottom-sheet/index.d.ts +75 -4
  127. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  128. package/lib/typescript/src/components/breadcrumb/index.d.ts +100 -0
  129. package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -0
  130. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  131. package/lib/typescript/src/components/calendar/index.d.ts +108 -0
  132. package/lib/typescript/src/components/calendar/index.d.ts.map +1 -0
  133. package/lib/typescript/src/components/carousel/index.d.ts +179 -0
  134. package/lib/typescript/src/components/carousel/index.d.ts.map +1 -0
  135. package/lib/typescript/src/components/chip/index.d.ts.map +1 -1
  136. package/lib/typescript/src/components/date-picker/index.d.ts +91 -0
  137. package/lib/typescript/src/components/date-picker/index.d.ts.map +1 -0
  138. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  139. package/lib/typescript/src/components/direction/index.d.ts +11 -10
  140. package/lib/typescript/src/components/direction/index.d.ts.map +1 -1
  141. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -1
  142. package/lib/typescript/src/components/field/index.d.ts.map +1 -1
  143. package/lib/typescript/src/components/frame/index.d.ts +17 -0
  144. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  145. package/lib/typescript/src/components/heatmap-chart/index.d.ts +22 -3
  146. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  147. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
  148. package/lib/typescript/src/components/item/index.d.ts.map +1 -1
  149. package/lib/typescript/src/components/label/index.d.ts.map +1 -1
  150. package/lib/typescript/src/components/line-chart/index.d.ts +42 -2
  151. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  152. package/lib/typescript/src/components/loader/index.d.ts +38 -0
  153. package/lib/typescript/src/components/loader/index.d.ts.map +1 -0
  154. package/lib/typescript/src/components/map/basemap.d.ts +76 -0
  155. package/lib/typescript/src/components/map/basemap.d.ts.map +1 -0
  156. package/lib/typescript/src/components/map/index.d.ts +367 -0
  157. package/lib/typescript/src/components/map/index.d.ts.map +1 -0
  158. package/lib/typescript/src/components/map/maplibre.d.ts +193 -0
  159. package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -0
  160. package/lib/typescript/src/components/marker/index.d.ts.map +1 -1
  161. package/lib/typescript/src/components/message/index.d.ts.map +1 -1
  162. package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
  163. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  164. package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
  165. package/lib/typescript/src/components/ring-chart/index.d.ts +156 -0
  166. package/lib/typescript/src/components/ring-chart/index.d.ts.map +1 -0
  167. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  168. package/lib/typescript/src/components/separator/index.d.ts +36 -1
  169. package/lib/typescript/src/components/separator/index.d.ts.map +1 -1
  170. package/lib/typescript/src/components/shimmer/index.d.ts.map +1 -1
  171. package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
  172. package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
  173. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  174. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  175. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  176. package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
  177. package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -1
  178. package/lib/typescript/src/components/tooltip/index.d.ts +125 -0
  179. package/lib/typescript/src/components/tooltip/index.d.ts.map +1 -0
  180. package/lib/typescript/src/components/typography/index.d.ts +6 -0
  181. package/lib/typescript/src/components/typography/index.d.ts.map +1 -1
  182. package/lib/typescript/src/hooks/index.d.ts +1 -0
  183. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  184. package/lib/typescript/src/hooks/use-back-handler.d.ts +19 -0
  185. package/lib/typescript/src/hooks/use-back-handler.d.ts.map +1 -0
  186. package/lib/typescript/src/hooks/use-direction.d.ts +20 -0
  187. package/lib/typescript/src/hooks/use-direction.d.ts.map +1 -0
  188. package/lib/typescript/src/icons/index.d.ts +8 -0
  189. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  190. package/lib/typescript/src/index.d.ts +14 -4
  191. package/lib/typescript/src/index.d.ts.map +1 -1
  192. package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
  193. package/lib/typescript/src/primitives/text.d.ts +20 -0
  194. package/lib/typescript/src/primitives/text.d.ts.map +1 -1
  195. package/lib/typescript/src/utils/chart.d.ts +96 -0
  196. package/lib/typescript/src/utils/chart.d.ts.map +1 -0
  197. package/lib/typescript/src/utils/date.d.ts +116 -0
  198. package/lib/typescript/src/utils/date.d.ts.map +1 -0
  199. package/package.json +9 -1
  200. package/src/components/accordion/index.tsx +8 -12
  201. package/src/components/alert/index.tsx +2 -2
  202. package/src/components/area-chart/index.tsx +1147 -0
  203. package/src/components/attachment/index.tsx +2 -2
  204. package/src/components/avatar/index.tsx +4 -4
  205. package/src/components/badge/index.tsx +6 -6
  206. package/src/components/bar-chart/index.tsx +1208 -0
  207. package/src/components/bottom-sheet/index.tsx +282 -21
  208. package/src/components/breadcrumb/index.tsx +363 -0
  209. package/src/components/button/index.tsx +12 -8
  210. package/src/components/calendar/index.tsx +703 -0
  211. package/src/components/carousel/index.tsx +788 -0
  212. package/src/components/chip/index.tsx +6 -8
  213. package/src/components/date-picker/index.tsx +237 -0
  214. package/src/components/dialog/index.tsx +7 -2
  215. package/src/components/direction/index.tsx +22 -22
  216. package/src/components/empty-state/index.tsx +2 -2
  217. package/src/components/field/index.tsx +3 -5
  218. package/src/components/flow/index.tsx +3 -3
  219. package/src/components/frame/index.tsx +22 -7
  220. package/src/components/heatmap-chart/index.tsx +58 -11
  221. package/src/components/input-group/index.tsx +5 -4
  222. package/src/components/item/index.tsx +9 -9
  223. package/src/components/label/index.tsx +2 -2
  224. package/src/components/line-chart/index.tsx +194 -223
  225. package/src/components/loader/index.tsx +812 -0
  226. package/src/components/map/basemap.ts +304 -0
  227. package/src/components/map/index.tsx +1116 -0
  228. package/src/components/map/maplibre.ts +201 -0
  229. package/src/components/marker/index.tsx +3 -3
  230. package/src/components/message/index.tsx +15 -14
  231. package/src/components/message-scroller/index.tsx +5 -4
  232. package/src/components/otp-input/index.tsx +2 -2
  233. package/src/components/popover/index.tsx +43 -7
  234. package/src/components/progress/index.tsx +10 -2
  235. package/src/components/radio-group/index.tsx +2 -2
  236. package/src/components/ring-chart/index.tsx +543 -0
  237. package/src/components/section-rail/index.tsx +6 -6
  238. package/src/components/select/index.tsx +10 -4
  239. package/src/components/separator/index.tsx +77 -19
  240. package/src/components/shimmer/index.tsx +10 -1
  241. package/src/components/slider/index.tsx +20 -5
  242. package/src/components/steps/index.tsx +5 -5
  243. package/src/components/switch/index.tsx +6 -1
  244. package/src/components/tabs/index.tsx +7 -9
  245. package/src/components/timeline/index.tsx +4 -4
  246. package/src/components/toast/index.tsx +2 -2
  247. package/src/components/toggle-button/index.tsx +5 -7
  248. package/src/components/tooltip/index.tsx +610 -0
  249. package/src/components/typography/index.tsx +7 -2
  250. package/src/hooks/index.ts +1 -0
  251. package/src/hooks/use-back-handler.ts +37 -0
  252. package/src/hooks/use-direction.ts +45 -0
  253. package/src/icons/index.tsx +111 -0
  254. package/src/index.ts +129 -0
  255. package/src/primitives/keyboard-avoider.tsx +2 -1
  256. package/src/primitives/text.tsx +53 -8
  257. package/src/utils/chart.ts +355 -0
  258. package/src/utils/date.ts +446 -0
@@ -7,6 +7,7 @@ import {
7
7
  useEffect,
8
8
  useMemo,
9
9
  useState,
10
+ type ComponentProps,
10
11
  type ReactElement,
11
12
  type ReactNode,
12
13
  } from 'react';
@@ -19,10 +20,12 @@ import Animated, {
19
20
  SlideInDown,
20
21
  SlideOutDown,
21
22
  runOnJS,
23
+ useAnimatedScrollHandler,
22
24
  useAnimatedStyle,
23
25
  useSharedValue,
24
26
  withSpring,
25
27
  withTiming,
28
+ type SharedValue,
26
29
  } from 'react-native-reanimated';
27
30
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
28
31
  import { tv } from 'tailwind-variants';
@@ -30,6 +33,8 @@ import { getNativeUI } from '../../native';
30
33
  import { XIcon } from '../../icons';
31
34
  import { Portal } from '../../primitives/portal';
32
35
  import { Scrim } from '../../primitives/scrim';
36
+ import { Text, textChildren } from '../../primitives/text';
37
+ import { useBackHandler } from '../../hooks/use-back-handler';
33
38
  import { cn } from '../../utils/cn';
34
39
 
35
40
  const SPRING = { damping: 22, stiffness: 280, mass: 0.7 } as const;
@@ -134,6 +139,24 @@ const NativeSheetContext = createContext<{
134
139
  snapPoints: BottomSheetProps['snapPoints'];
135
140
  } | null>(null);
136
141
 
142
+ /**
143
+ * The wiring between the sheet's drag and a scrolling body inside it. Set by
144
+ * `Content`, filled in by `Body` — the two gestures have to agree on where the
145
+ * list is scrolled to before either can decide whose drag it is.
146
+ */
147
+ /*
148
+ * Taken off the factory rather than imported by name: the package exports two
149
+ * different `NativeGesture` types, and the one reachable from its entry point
150
+ * is not the one `Gesture.Native()` returns.
151
+ */
152
+ type ScrollGesture = ReturnType<typeof Gesture.Native>;
153
+
154
+ const SheetSurfaceContext = createContext<{
155
+ scrollGesture: ScrollGesture;
156
+ scrollOffset: SharedValue<number>;
157
+ hasScrollable: SharedValue<boolean>;
158
+ } | null>(null);
159
+
137
160
  function BottomSheetRoot({
138
161
  children,
139
162
  open,
@@ -195,8 +218,10 @@ export interface BottomSheetContentProps extends ViewProps {
195
218
  /** Tap on the backdrop closes the sheet. Default true. */
196
219
  dismissible?: boolean;
197
220
  /**
198
- * Show a close button in the top-right corner. On by default for the styled
199
- * sheet; ignored by the native sheet, which has its own dismiss affordances.
221
+ * Show a close button in the top trailing corner — the right in a
222
+ * left-to-right app, the left in a right-to-left one. On by default for the
223
+ * styled sheet; ignored by the native sheet, which has its own dismiss
224
+ * affordances.
200
225
  */
201
226
  showClose?: boolean;
202
227
  /**
@@ -213,15 +238,35 @@ export interface BottomSheetContentProps extends ViewProps {
213
238
  * optional `expo-blur`; without it this dims, rather than failing.
214
239
  */
215
240
  blur?: boolean;
241
+ /**
242
+ * How tall the sheet opens.
243
+ *
244
+ * `auto` sizes to the content, which is right for a sheet that is a handful
245
+ * of rows. `half` and `full` fix the height instead, for content that has to
246
+ * be given the room rather than allowed to ask for it — a list, a form, a
247
+ * document. Either way the sheet is clamped to leave the status bar clear,
248
+ * so `full` is as tall as the screen allows rather than as tall as the screen.
249
+ *
250
+ * On the native sheet this maps onto the platform's detents.
251
+ */
252
+ size?: 'auto' | 'half' | 'full';
216
253
  children?: ReactNode;
217
254
  }
218
255
 
256
+ /**
257
+ * Fractions of the screen the sized sheets aim for. `full` is not 1: a sheet
258
+ * that reached the top would have nothing behind it to read as laid *over*,
259
+ * and the gap is what says the app is still there underneath.
260
+ */
261
+ const SIZE_FRACTION = { half: 0.5, full: 0.94 } as const;
262
+
219
263
  function BottomSheetContent({
220
264
  className,
221
265
  dismissible = true,
222
266
  showClose = true,
223
267
  detached = false,
224
268
  blur = false,
269
+ size = 'auto',
225
270
  children,
226
271
  ...props
227
272
  }: BottomSheetContentProps) {
@@ -233,8 +278,23 @@ function BottomSheetContent({
233
278
  const translateY = useSharedValue(0);
234
279
  const closeTint = useCSSVariable('--color-muted-foreground');
235
280
 
281
+ /*
282
+ * The scrolling body's gesture, if there is one. It is built here rather
283
+ * than in `BottomSheet.Body` because the drag below has to name it, and a
284
+ * gesture is composed before its children have mounted. `Body` attaches it
285
+ * to its own scroll view; with no `Body` it is simply never attached, and
286
+ * the drag reverts to owning every touch.
287
+ */
288
+ const scrollGesture = useMemo(() => Gesture.Native(), []);
289
+ const scrollOffset = useSharedValue(0);
290
+ const hasScrollable = useSharedValue(false);
291
+
236
292
  const close = useCallback(() => setOpen(false), [setOpen]);
237
293
 
294
+ // The sheet closes on the Android back button while it is up, the same as a
295
+ // backdrop tap — but only when it is dismissible.
296
+ useBackHandler(open && dismissible, close);
297
+
238
298
  /*
239
299
  * The offset survives a close — the early return below sits after every
240
300
  * hook, so this component keeps its state while the sheet is hidden. A
@@ -256,7 +316,25 @@ function BottomSheetContent({
256
316
  * is in the middle of, so a tap with any finger travel does nothing.
257
317
  */
258
318
  .activeOffsetY([-12, 12])
319
+ /*
320
+ * A scrolling body runs its own gesture, and without this the two
321
+ * compete: whichever wins takes the touch outright, so either the
322
+ * list never scrolls or the sheet never drags. Running them together
323
+ * lets the rule below decide which one a given drag belongs to.
324
+ */
325
+ .simultaneousWithExternalGesture(scrollGesture)
259
326
  .onChange((event) => {
327
+ /*
328
+ * The list gets the drag until it has nothing left to scroll. Once
329
+ * it is at the top the sheet takes over, which is the gesture people
330
+ * already expect: pulling down on a list that cannot go further
331
+ * pulls the sheet instead. The second clause keeps a sheet that is
332
+ * already part-way dragged following the finger, so releasing the
333
+ * drag halfway does not strand it.
334
+ */
335
+ if (hasScrollable.value && scrollOffset.value > 0 && translateY.value <= 0) {
336
+ return;
337
+ }
260
338
  // Rubber-band when dragging upward, follow the finger downward.
261
339
  const next = translateY.value + event.changeY;
262
340
  translateY.value = next > 0 ? next : next / 3;
@@ -276,15 +354,40 @@ function BottomSheetContent({
276
354
  // Rebuilt only when one of these changes. Built inline it would be a new
277
355
  // gesture on every render — and the sheet re-renders while it is being
278
356
  // used, each time re-attaching the handler and dropping the live touch.
279
- [close, screenHeight, translateY]
357
+ [close, screenHeight, translateY, scrollGesture, scrollOffset, hasScrollable]
280
358
  );
281
359
 
282
360
  const sheetStyle = useAnimatedStyle(() => ({
283
361
  transform: [{ translateY: translateY.value }],
284
362
  }));
285
363
 
364
+ /*
365
+ * A sized sheet is clamped rather than handed its fraction outright.
366
+ * `insets.top` is the status bar and the notch, and content that runs under
367
+ * those is unreadable at exactly the moment the sheet is at its tallest.
368
+ */
369
+ const sizedHeight =
370
+ size === 'auto'
371
+ ? undefined
372
+ : Math.min(
373
+ screenHeight * SIZE_FRACTION[size],
374
+ screenHeight - insets.top - (detached ? 24 : 8)
375
+ );
376
+
377
+ const surface = useMemo(
378
+ () => ({ scrollGesture, scrollOffset, hasScrollable }),
379
+ [scrollGesture, scrollOffset, hasScrollable]
380
+ );
381
+
286
382
  if (nativeSheet) {
287
383
  const { Host, BottomSheet: NativeBottomSheet, RNHostView } = nativeSheet.nativeUI;
384
+ /*
385
+ * `snapPoints` on the root is the finer control and wins; `size` is the
386
+ * shorthand, and maps onto the platform's own detents rather than a
387
+ * height, so a native sheet keeps the system's snapping.
388
+ */
389
+ const snapPoints =
390
+ nativeSheet.snapPoints ?? (size === 'auto' ? undefined : [size]);
288
391
  // The platform owns presentation, so this stays mounted and toggles
289
392
  // isPresented rather than unmounting on close.
290
393
  //
@@ -296,7 +399,7 @@ function BottomSheetContent({
296
399
  <NativeBottomSheet
297
400
  isPresented={open}
298
401
  onDismiss={dismissible ? close : () => {}}
299
- snapPoints={nativeSheet.snapPoints}
402
+ snapPoints={snapPoints}
300
403
  >
301
404
  <RNHostView matchContents>
302
405
  <BottomSheetContext.Provider value={context}>
@@ -316,11 +419,11 @@ function BottomSheetContent({
316
419
  // against, so `100%` measures against nothing and the
317
420
  // content lays out wider than the sheet it sits in.
318
421
  width: screenWidth,
319
- minHeight: detentFloor(nativeSheet.snapPoints, screenHeight),
422
+ minHeight: detentFloor(snapPoints, screenHeight),
320
423
  paddingBottom: Math.max(insets.bottom, 16),
321
424
  }}
322
425
  >
323
- {children}
426
+ {textChildren(children)}
324
427
  </View>
325
428
  </BottomSheetContext.Provider>
326
429
  </RNHostView>
@@ -364,24 +467,42 @@ function BottomSheetContent({
364
467
  // indicator, so it takes plain padding rather than stacking the
365
468
  // inset on top of the gap.
366
469
  { paddingBottom: detached ? 16 : Math.max(insets.bottom, 16) },
470
+ sizedHeight === undefined ? null : { height: sizedHeight },
367
471
  props.style,
368
472
  ]}
369
473
  >
370
- <View className="mb-3 self-center">
371
- <View className="h-1 w-10 rounded-full bg-muted-foreground/30" />
372
- </View>
373
- {showClose ? (
374
- <Pressable
375
- accessibilityRole="button"
376
- accessibilityLabel="Close"
377
- onPress={close}
378
- hitSlop={8}
379
- className="absolute right-4 top-3 h-8 w-8 items-center justify-center rounded-full bg-muted active:opacity-70"
380
- >
381
- <XIcon size={16} color={typeof closeTint === 'string' ? closeTint : undefined} />
382
- </Pressable>
383
- ) : null}
384
- {children}
474
+ <SheetSurfaceContext.Provider value={surface}>
475
+ <View className="mb-3 self-center">
476
+ <View className="h-1 w-10 rounded-full bg-muted-foreground/30" />
477
+ </View>
478
+ {textChildren(children)}
479
+ {/*
480
+ * Last, and lifted above the content.
481
+ *
482
+ * Drawn before the children it was covered by whichever of them
483
+ * reached the top-right corner — a title spanning the sheet's
484
+ * width is enough — and in React Native a later sibling wins
485
+ * the touch. The button still looked untouched, so the failure
486
+ * read as intermittent: taps landing on the sliver above the
487
+ * title worked, taps anywhere else went to the title and did
488
+ * nothing. `hitSlop` made it worse by growing the part of the
489
+ * target that was already buried.
490
+ */}
491
+ {showClose ? (
492
+ <Pressable
493
+ accessibilityRole="button"
494
+ accessibilityLabel="Close"
495
+ onPress={close}
496
+ hitSlop={8}
497
+ className="absolute end-4 top-3 z-10 h-8 w-8 items-center justify-center rounded-full bg-muted active:opacity-70"
498
+ >
499
+ <XIcon
500
+ size={16}
501
+ color={typeof closeTint === 'string' ? closeTint : undefined}
502
+ />
503
+ </Pressable>
504
+ ) : null}
505
+ </SheetSurfaceContext.Provider>
385
506
  </Animated.View>
386
507
  </GestureDetector>
387
508
  </View>
@@ -390,7 +511,147 @@ function BottomSheetContent({
390
511
  );
391
512
  }
392
513
 
514
+ export interface BottomSheetHeaderProps extends ViewProps {
515
+ className?: string;
516
+ /** Heading for the sheet. Strings are wrapped; anything else is drawn as given. */
517
+ title?: ReactNode;
518
+ /** A line under the title, for what the sheet is asking. */
519
+ description?: ReactNode;
520
+ children?: ReactNode;
521
+ }
522
+
523
+ /**
524
+ * A fixed heading above the sheet's body.
525
+ *
526
+ * It exists mostly to reserve the top-right corner. The close button is
527
+ * absolutely positioned there, so anything full-width at the top of a sheet
528
+ * sits under it — a title is exactly that shape, and the end padding here is
529
+ * what keeps the two from meeting. Being outside `Body` is the other half:
530
+ * it stays put while the content scrolls under it, so the sheet still says
531
+ * what it is once the list has moved.
532
+ */
533
+ function BottomSheetHeader({
534
+ className,
535
+ title,
536
+ description,
537
+ children,
538
+ ...props
539
+ }: BottomSheetHeaderProps) {
540
+ return (
541
+ <View className={cn('gap-1 pb-3 pe-12', className)} {...props}>
542
+ {textChildren(title, (text) => (
543
+ <Text size="lg" weight="semibold">
544
+ {text}
545
+ </Text>
546
+ ))}
547
+ {textChildren(description, (text) => (
548
+ <Text size="sm" muted>
549
+ {text}
550
+ </Text>
551
+ ))}
552
+ {textChildren(children)}
553
+ </View>
554
+ );
555
+ }
556
+ BottomSheetHeader.displayName = 'BottomSheet.Header';
557
+
558
+ export interface BottomSheetBodyProps
559
+ extends Omit<ComponentProps<typeof Animated.ScrollView>, 'ref'> {
560
+ className?: string;
561
+ children?: ReactNode;
562
+ }
563
+
564
+ /**
565
+ * The scrolling part of a sized sheet.
566
+ *
567
+ * A plain `ScrollView` works here too, but only one of the two gestures can
568
+ * win a given drag and neither knows about the other, so the list and the
569
+ * sheet fight over every downward swipe. This one reports where it is
570
+ * scrolled to, which is what lets the sheet hold off until the list has run
571
+ * out — pull down on a list at its top and the sheet comes with you, pull
572
+ * down anywhere else and the list scrolls.
573
+ */
574
+ function BottomSheetBody({
575
+ className,
576
+ children,
577
+ onScroll,
578
+ ...props
579
+ }: BottomSheetBodyProps) {
580
+ const surface = useContext(SheetSurfaceContext);
581
+
582
+ useEffect(() => {
583
+ if (!surface) return;
584
+ surface.hasScrollable.value = true;
585
+ return () => {
586
+ surface.hasScrollable.value = false;
587
+ surface.scrollOffset.value = 0;
588
+ };
589
+ }, [surface]);
590
+
591
+ /*
592
+ * Pulled out before the handler rather than read off `surface` inside it.
593
+ * A scroll handler is a worklet, and everything it closes over is copied to
594
+ * the UI thread — closing over the context object would drag the gesture
595
+ * along with it, and a gesture is not a value that can be copied.
596
+ */
597
+ const scrollOffset = surface?.scrollOffset;
598
+
599
+ const handler = useAnimatedScrollHandler((event) => {
600
+ if (scrollOffset) scrollOffset.value = event.contentOffset.y;
601
+ });
602
+
603
+ const body = (
604
+ <Animated.ScrollView
605
+ onScroll={handler}
606
+ scrollEventThrottle={16}
607
+ showsVerticalScrollIndicator={false}
608
+ className={cn('flex-1', className)}
609
+ {...props}
610
+ >
611
+ {textChildren(children)}
612
+ </Animated.ScrollView>
613
+ );
614
+
615
+ /*
616
+ * The detector is what gives the scroll a handler the sheet's drag can name.
617
+ * Without one the two are strangers, and the first to activate takes the
618
+ * touch outright — which is the whole bug this part exists to solve.
619
+ */
620
+ if (!surface) return body;
621
+ return <GestureDetector gesture={surface.scrollGesture}>{body}</GestureDetector>;
622
+ }
623
+ BottomSheetBody.displayName = 'BottomSheet.Body';
624
+
625
+ export interface BottomSheetFooterProps extends ViewProps {
626
+ className?: string;
627
+ children?: ReactNode;
628
+ }
629
+
630
+ /**
631
+ * A row pinned below the body, for whatever the sheet is asking to be decided.
632
+ * Outside `Body` so it stays reachable however far the content scrolls — an
633
+ * action that has to be scrolled to is one people do not find.
634
+ */
635
+ function BottomSheetFooter({
636
+ className,
637
+ children,
638
+ ...props
639
+ }: BottomSheetFooterProps) {
640
+ return (
641
+ <View
642
+ className={cn('gap-2 border-t border-border pt-3', className)}
643
+ {...props}
644
+ >
645
+ {textChildren(children)}
646
+ </View>
647
+ );
648
+ }
649
+ BottomSheetFooter.displayName = 'BottomSheet.Footer';
650
+
393
651
  export const BottomSheet = Object.assign(BottomSheetRoot, {
394
652
  Trigger: BottomSheetTrigger,
395
653
  Content: BottomSheetContent,
654
+ Header: BottomSheetHeader,
655
+ Body: BottomSheetBody,
656
+ Footer: BottomSheetFooter,
396
657
  });