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