@delacour/react-native-bottom-sheet 0.1.0-alpha.0 → 0.1.0-alpha.20261006140348

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 (76) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/animation/AGENTS.md +1 -1
  4. package/src/animation/animation.types.ts +1 -1
  5. package/src/animation/use-animate-to.ts +4 -4
  6. package/src/animation/use-settle-callbacks.ts +3 -3
  7. package/src/components/AGENTS.md +2 -2
  8. package/src/components/bottom-sheet-background.tsx +1 -1
  9. package/src/components/bottom-sheet-content.tsx +3 -3
  10. package/src/components/bottom-sheet-footer.tsx +3 -3
  11. package/src/components/bottom-sheet-handle.tsx +5 -5
  12. package/src/components/bottom-sheet-overlay.tsx +1 -1
  13. package/src/components/bottom-sheet.context.tsx +1 -1
  14. package/src/components/bottom-sheet.tsx +13 -11
  15. package/src/components/bottom-sheet.types.ts +15 -15
  16. package/src/core/AGENTS.md +17 -17
  17. package/src/core/backdrop/backdrop-opacity.test.ts +2 -2
  18. package/src/core/backdrop/backdrop-opacity.ts +1 -1
  19. package/src/core/footer/body-inset.test.ts +3 -3
  20. package/src/core/footer/body-inset.ts +3 -3
  21. package/src/core/geometry/position.ts +1 -1
  22. package/src/core/geometry/surface-height.test.ts +5 -5
  23. package/src/core/geometry/surface-height.ts +7 -7
  24. package/src/core/haptic/crossed-snap-point.test.ts +43 -0
  25. package/src/core/haptic/crossed-snap-point.ts +33 -0
  26. package/src/core/index.ts +14 -9
  27. package/src/core/intent/layout-ready.ts +1 -1
  28. package/src/core/intent/resolve-intent.test.ts +9 -9
  29. package/src/core/intent/resolve-intent.ts +10 -10
  30. package/src/core/keyboard/container-layout-guard.ts +1 -1
  31. package/src/core/keyboard/keyboard-step.ts +2 -2
  32. package/src/core/machine/machine.types.ts +2 -2
  33. package/src/core/machine/step-transition.ts +2 -2
  34. package/src/core/scroll/scroll-lock.test.ts +2 -2
  35. package/src/core/scroll/scroll-lock.ts +2 -2
  36. package/src/core/scroll/scroll-pan.test.ts +14 -14
  37. package/src/core/scroll/scroll-pan.ts +14 -14
  38. package/src/core/sheet.types.ts +5 -5
  39. package/src/core/snap-points/dynamic-snap-point.test.ts +44 -0
  40. package/src/core/{detents/dynamic-detent.ts → snap-points/dynamic-snap-point.ts} +5 -5
  41. package/src/core/snap-points/index-for-height.test.ts +65 -0
  42. package/src/core/{detents → snap-points}/index-for-height.ts +11 -11
  43. package/src/core/snap-points/normalize-snap-points.test.ts +72 -0
  44. package/src/core/{detents/normalize-detents.ts → snap-points/normalize-snap-points.ts} +9 -9
  45. package/src/core/{detents → snap-points}/select-snap-height.test.ts +10 -8
  46. package/src/core/{detents → snap-points}/select-snap-height.ts +5 -5
  47. package/src/core/snap-points/sheet-state.test.ts +42 -0
  48. package/src/core/{detents → snap-points}/sheet-state.ts +6 -6
  49. package/src/gesture/AGENTS.md +11 -11
  50. package/src/gesture/gesture.types.ts +3 -3
  51. package/src/gesture/use-scroll-lock.ts +3 -3
  52. package/src/gesture/use-sheet-pan.ts +43 -32
  53. package/src/keyboard/AGENTS.md +2 -2
  54. package/src/keyboard/use-sheet-keyboard.ts +6 -6
  55. package/src/layout/AGENTS.md +5 -5
  56. package/src/portal/AGENTS.md +14 -5
  57. package/src/portal/bottom-sheet-provider.tsx +19 -7
  58. package/src/scrollable/AGENTS.md +5 -5
  59. package/src/scrollable/bottom-sheet-scroll-view.tsx +1 -1
  60. package/src/scrollable/create-bottom-sheet-scrollable.tsx +3 -3
  61. package/src/state/AGENTS.md +4 -4
  62. package/src/state/state.types.ts +11 -11
  63. package/src/state/use-sheet-geometry.ts +11 -11
  64. package/src/state/use-sheet-intents.ts +12 -9
  65. package/src/state/use-sheet-state.ts +9 -9
  66. package/src/state/{use-detents.ts → use-snap-points.ts} +13 -13
  67. package/src/steps/AGENTS.md +7 -7
  68. package/src/steps/bottom-sheet-steps.tsx +9 -9
  69. package/src/core/detents/dynamic-detent.test.ts +0 -40
  70. package/src/core/detents/index-for-height.test.ts +0 -65
  71. package/src/core/detents/normalize-detents.test.ts +0 -72
  72. package/src/core/detents/sheet-state.test.ts +0 -42
  73. package/src/core/haptic/crossed-detent.test.ts +0 -43
  74. package/src/core/haptic/crossed-detent.ts +0 -33
  75. /package/src/core/{detents → snap-points}/over-drag.test.ts +0 -0
  76. /package/src/core/{detents → snap-points}/over-drag.ts +0 -0
@@ -5,16 +5,16 @@ import { cancelAnimation, useSharedValue } from "react-native-reanimated";
5
5
  import { scheduleOnRN } from "react-native-worklets";
6
6
  import type { AnimateTo, SettleAt } from "../animation/animation.types";
7
7
  import {
8
- crossedDetent,
9
- detentUnder,
8
+ crossedSnapPoint,
10
9
  GESTURE_SOURCE,
11
10
  type GestureSource,
12
11
  listDragHeight,
13
12
  listOwnsRelease,
14
13
  resistOverDrag,
15
- restingDetent,
14
+ restingSnapPoint,
16
15
  SCROLLABLE_TYPE,
17
16
  selectSnapHeight,
17
+ snapPointUnder,
18
18
  } from "../core";
19
19
  import { ANIM_STATUS, type SheetGeometry, type SheetSharedState } from "../state/state.types";
20
20
  import {
@@ -27,8 +27,8 @@ import {
27
27
 
28
28
  type PanEvent = { translationY: number; velocityY: number };
29
29
 
30
- /** Settle tolerance: a sheet within this of a detent is on it. */
31
- const AT_DETENT = 0.5;
30
+ /** Settle tolerance: a sheet within this of a snap point is on it. */
31
+ const AT_SNAP_POINT = 0.5;
32
32
 
33
33
  /** JS-thread, so `scheduleOnRN` has a plain function to call; `dismiss` returns a promise nobody awaits. */
34
34
  function dismissKeyboard(): void {
@@ -40,16 +40,16 @@ function dismissKeyboard(): void {
40
40
  *
41
41
  * Everything runs on the UI thread. `onStart` cancels whatever animation owns
42
42
  * `base` and remembers where the drag began; `onChange` turns the finger's
43
- * travel into a height, rubber-banded past the detents by `resistOverDrag`
44
- * and never above the container; `onFinalize` picks the detent the finger was
43
+ * travel into a height, rubber-banded past the snap points by `resistOverDrag`
44
+ * and never above the container; `onFinalize` picks the snap point the finger was
45
45
  * heading for with `selectSnapHeight` and hands it to `animateTo` with the
46
46
  * release velocity. The close is a candidate only when `enablePanDownToClose`.
47
47
  *
48
48
  * A content pan the list owns at release — the sheet at its top, the list
49
49
  * scrolled — does not snap, but it still settles: the finger moved `base` by
50
50
  * hand, so no animation ran and nothing else writes `currentIndex`. `settleAt`
51
- * on the detent under `base` is the bookkeeping of a finished animation
52
- * without the motion; between detents (which the list ownership test should
51
+ * on the snap point under `base` is the bookkeeping of a finished animation
52
+ * without the motion; between snap points (which the list ownership test should
53
53
  * rule out) the sheet snaps to the nearest one with no velocity instead, since
54
54
  * the release velocity is the list's momentum.
55
55
  *
@@ -62,7 +62,7 @@ function dismissKeyboard(): void {
62
62
  * worklets in the ordinary way. See the package `AGENTS.md`: that is legal
63
63
  * precisely because these are not module-scope worklets.
64
64
  *
65
- * Per-gesture memory — where the drag began, the detent last under it,
65
+ * Per-gesture memory — where the drag began, the snap point last under it,
66
66
  * whether it is over-dragging — lives in shared values rather than closure
67
67
  * variables. Each worklet gets its own copy of a captured `let`, so a write in
68
68
  * `onStart` would never be seen by `onChange`.
@@ -75,12 +75,17 @@ export function useSheetPan(
75
75
  options: SheetPanOptions
76
76
  ): SheetPans {
77
77
  const startBase = useSharedValue(0);
78
- const lastDetent = useSharedValue(-1);
78
+ const lastSnapPoint = useSharedValue(-1);
79
79
  const overDragging = useSharedValue(false);
80
80
  const startScroll = useSharedValue(0);
81
81
  const listHeld = useSharedValue(false);
82
- const { enableHandlePanningGesture, enableContentPanningGesture, onDetentHaptic, onCloseHaptic, onOverDragHaptic } =
83
- options;
82
+ const {
83
+ enableHandlePanningGesture,
84
+ enableContentPanningGesture,
85
+ onSnapPointHaptic,
86
+ onCloseHaptic,
87
+ onOverDragHaptic,
88
+ } = options;
84
89
 
85
90
  return useMemo(() => {
86
91
  const begin = (source: GestureSource): void => {
@@ -91,9 +96,9 @@ export function useSheetPan(
91
96
  startBase.value = state.base.value;
92
97
  startScroll.value = state.scrollOffsetY.value;
93
98
  // A list scrolled and below the top is held by the lock: it has no travel
94
- // to spend until the sheet reaches the highest detent.
95
- listHeld.value = state.scrollOffsetY.value > 0 && state.base.value < geometry.highest.value - AT_DETENT;
96
- lastDetent.value = detentUnder(state.base.value, geometry.detents.value);
99
+ // to spend until the sheet reaches the highest snap point.
100
+ listHeld.value = state.scrollOffsetY.value > 0 && state.base.value < geometry.highest.value - AT_SNAP_POINT;
101
+ lastSnapPoint.value = snapPointUnder(state.base.value, geometry.snapPoints.value);
97
102
  overDragging.value = false;
98
103
  // A finger on the sheet is a reason to put the keyboard away; the lift
99
104
  // comes off as it goes and the drag continues from wherever `base` is.
@@ -102,15 +107,21 @@ export function useSheetPan(
102
107
  }
103
108
  };
104
109
 
105
- const haptics = (raw: number, next: number, lowest: number, highest: number, detents: readonly number[]): void => {
110
+ const haptics = (
111
+ raw: number,
112
+ next: number,
113
+ lowest: number,
114
+ highest: number,
115
+ snapPoints: readonly number[]
116
+ ): void => {
106
117
  "worklet";
107
118
  const over = raw > highest || raw < lowest;
108
119
  if (over && !overDragging.value && onOverDragHaptic) onOverDragHaptic();
109
120
  overDragging.value = over;
110
121
 
111
- if (crossedDetent(lastDetent.value, next, detents)) {
112
- lastDetent.value = detentUnder(next, detents);
113
- if (onDetentHaptic) onDetentHaptic();
122
+ if (crossedSnapPoint(lastSnapPoint.value, next, snapPoints)) {
123
+ lastSnapPoint.value = snapPointUnder(next, snapPoints);
124
+ if (onSnapPointHaptic) onSnapPointHaptic();
114
125
  }
115
126
  };
116
127
 
@@ -138,18 +149,18 @@ export function useSheetPan(
138
149
  "worklet";
139
150
  if (state.gestureSource.value === GESTURE_SOURCE.NONE) return;
140
151
  const config = state.config.value;
141
- const detents = geometry.detents.value;
152
+ const snapPoints = geometry.snapPoints.value;
142
153
  const closed = geometry.closedHeight.value;
143
154
  const highest = geometry.highest.value;
144
155
  const raw = dragged(event.translationY, highest);
145
156
 
146
- const lowest = config.enablePanDownToClose || detents.length === 0 ? closed : (detents[0] as number);
157
+ const lowest = config.enablePanDownToClose || snapPoints.length === 0 ? closed : (snapPoints[0] as number);
147
158
  const factor = config.enableOverDrag ? config.overDragResistanceFactor : 0;
148
159
  const next = Math.min(resistOverDrag(raw, lowest, highest, factor), geometry.maxHeight.value);
149
160
 
150
- haptics(raw, next, lowest, highest, detents);
161
+ haptics(raw, next, lowest, highest, snapPoints);
151
162
  state.base.value = next;
152
- if (listHeld.value && next >= highest - AT_DETENT) listHeld.value = false;
163
+ if (listHeld.value && next >= highest - AT_SNAP_POINT) listHeld.value = false;
153
164
  };
154
165
 
155
166
  // The haptic for a close, the keyboard for a downward release — a release
@@ -162,11 +173,11 @@ export function useSheetPan(
162
173
  animateTo(target, "gesture", velocity / 2);
163
174
  };
164
175
 
165
- // A release the list owns settles the sheet on the detent under `base`
166
- // with no motion; `false` when it sits between detents and has to snap.
176
+ // A release the list owns settles the sheet on the snap point under `base`
177
+ // with no motion; `false` when it sits between snap points and has to snap.
167
178
  const settleResting = (): boolean => {
168
179
  "worklet";
169
- const resting = restingDetent(state.base.value, geometry.detents.value);
180
+ const resting = restingSnapPoint(state.base.value, geometry.snapPoints.value);
170
181
  if (resting === null) return false;
171
182
  settleAt(resting, "gesture");
172
183
  return true;
@@ -180,7 +191,7 @@ export function useSheetPan(
180
191
  const config = state.config.value;
181
192
  const closed = geometry.closedHeight.value;
182
193
 
183
- // The finger was scrolling rows at the top detent: the release is the
194
+ // The finger was scrolling rows at the top snap point: the release is the
184
195
  // list's momentum, not a snap — but the sheet still settles where it is.
185
196
  const listOwns = listOwnsRelease({
186
197
  scrollable: overList(source),
@@ -190,13 +201,13 @@ export function useSheetPan(
190
201
  });
191
202
  if (listOwns && settleResting()) return;
192
203
 
193
- // A list-owned release between detents (which `listOwnsRelease` should
204
+ // A list-owned release between snap points (which `listOwnsRelease` should
194
205
  // rule out) snaps to the nearest with no velocity and never closes.
195
206
  const velocity = listOwns ? 0 : -event.velocityY;
196
207
  const target = selectSnapHeight({
197
208
  height: state.base.value,
198
209
  velocity,
199
- detents: geometry.detents.value,
210
+ snapPoints: geometry.snapPoints.value,
200
211
  closedHeight: config.enablePanDownToClose && !listOwns ? closed : null,
201
212
  projection: SNAP_PROJECTION,
202
213
  });
@@ -239,11 +250,11 @@ export function useSheetPan(
239
250
  startBase,
240
251
  startScroll,
241
252
  listHeld,
242
- lastDetent,
253
+ lastSnapPoint,
243
254
  overDragging,
244
255
  enableHandlePanningGesture,
245
256
  enableContentPanningGesture,
246
- onDetentHaptic,
257
+ onSnapPointHaptic,
247
258
  onCloseHaptic,
248
259
  onOverDragHaptic,
249
260
  ]);
@@ -55,8 +55,8 @@ stops a tap from one field to the next reading as a close and a reopen.
55
55
  | `keyboardBehavior` | On show | `keyboardBlurBehavior: "restore"` on hide |
56
56
  | --- | --- | --- |
57
57
  | `interactive` (default) | Nothing — `keyboardLift` is a derivation over `progress`, so the sheet rides the keyboard up and follows an interactive dismiss down | Nothing to restore; `base` never moved |
58
- | `extend` | `animateTo(highest, "keyboard")` | Back to the detent held before |
59
- | `fillParent` | `animateTo(maxHeight, "keyboard")`; no lift; the body shrinks by the keyboard | Back to the detent held before |
58
+ | `extend` | `animateTo(highest, "keyboard")` | Back to the snap point held before |
59
+ | `fillParent` | `animateTo(maxHeight, "keyboard")`; no lift; the body shrinks by the keyboard | Back to the snap point held before |
60
60
  | `none` | Nothing; the keyboard is not the sheet's problem | Nothing |
61
61
 
62
62
  The behaviour reaction samples `progress` and `keyboardOwned` and hands two
@@ -48,9 +48,9 @@ export type UseSheetKeyboardOptions = {
48
48
  *
49
49
  * The second is the behaviour. `interactive` and `none` need none: one lifts
50
50
  * by derivation, the other ignores the keyboard. `extend` and `fillParent`
51
- * snap — to the highest detent or the container — when an owned keyboard
51
+ * snap — to the highest snap point or the container — when an owned keyboard
52
52
  * rises, and `keyboardBlurBehavior: "restore"` sends the sheet back to the
53
- * detent it held when the keyboard leaves. `keyboardStep` in the core makes
53
+ * snap point it held when the keyboard leaves. `keyboardStep` in the core makes
54
54
  * that call from two consecutive samples; this reaction only carries it out,
55
55
  * and stays out of the way of a gesture that owns `base`.
56
56
  */
@@ -139,7 +139,7 @@ export function useSheetKeyboard({
139
139
  if (dragging) return;
140
140
  applied.value = true;
141
141
  indexBefore.value = running
142
- ? Math.round(indexForHeight(state.animTarget.value, geometry.detents.value, closed))
142
+ ? Math.round(indexForHeight(state.animTarget.value, geometry.snapPoints.value, closed))
143
143
  : state.currentIndex.value;
144
144
  const target = config.keyboardBehavior === "fillParent" ? geometry.maxHeight.value : geometry.highest.value;
145
145
  if (running || Math.abs(target - state.base.value) > 0.5) animateTo(target, "keyboard", 0);
@@ -151,9 +151,9 @@ export function useSheetKeyboard({
151
151
  indexBefore.value = CLOSED_INDEX;
152
152
  if (step !== "restore" || dragging || !sheetOpen || index < 0) return;
153
153
 
154
- const detents = geometry.detents.value;
155
- if (detents.length === 0) return;
156
- const target = heightForIndex(Math.min(index, detents.length - 1), detents, closed);
154
+ const snapPoints = geometry.snapPoints.value;
155
+ if (snapPoints.length === 0) return;
156
+ const target = heightForIndex(Math.min(index, snapPoints.length - 1), snapPoints, closed);
157
157
  if (Math.abs(target - state.base.value) > 0.5) animateTo(target, "keyboard", 0);
158
158
  },
159
159
  [state, geometry, animateTo, progress, applied, indexBefore]
@@ -13,11 +13,11 @@ How the sheet learns the sizes the geometry needs.
13
13
 
14
14
  | Value | By | Read for |
15
15
  | --- | --- | --- |
16
- | `containerHeight`, `containerWidth` | the frame `Portal` renders | every detent, `position`, the detached frame |
16
+ | `containerHeight`, `containerWidth` | the frame `Portal` renders | every snap point, `position`, the detached frame |
17
17
  | `containerBottomOffset` | the same frame, via `measureInWindow` | how much of a keyboard overlaps the sheet at all (BSHEET-3) |
18
- | `handleHeight` | `Handle` | the dynamic detent, `contentArea` |
19
- | `contentHeight` | `Content`'s inner box | the dynamic detent |
20
- | `footerContentHeight` | `Footer`'s inner box (BSHEET-3) | the dynamic detent, `footerTop` |
18
+ | `handleHeight` | `Handle` | the dynamic snap point, `contentArea` |
19
+ | `contentHeight` | `Content`'s inner box | the dynamic snap point |
20
+ | `footerContentHeight` | `Footer`'s inner box (BSHEET-3) | the dynamic snap point, `footerTop` |
21
21
 
22
22
  Scrollables (BSHEET-4) feed `contentHeight` from `onContentSizeChange`
23
23
  instead of a layout.
@@ -40,7 +40,7 @@ The frame's height goes through `acceptContainerLayout`, which refuses
40
40
  Android's adjustResize double-count while a keyboard is up. Inert until
41
41
  BSHEET-3 writes `keyboardHeight`, and correct once it does. A height that is
42
42
  accepted while the sheet is open makes the intent reaction jump the sheet to
43
- the same index in the re-derived detents (`state/`).
43
+ the same index in the re-derived snap points (`state/`).
44
44
 
45
45
  ## No synchronous first read
46
46
 
@@ -7,7 +7,7 @@ context and the registry that `Portal` reads.
7
7
 
8
8
  | Path | What |
9
9
  | --- | --- |
10
- | `bottom-sheet-provider.tsx` | `BottomSheetProvider` — teleport's `PortalProvider`, the registry provider and the host-name context set to `"root"`. The app mounts it once |
10
+ | `bottom-sheet-provider.tsx` | `BottomSheetProvider` — teleport's `PortalProvider`, the registry provider and the host-name context set to `"root"`. The app mounts it once. `hasPortalProvider` skips the teleport provider when one is already above |
11
11
  | `bottom-sheet-host.tsx` | `BottomSheet.Host` — a teleport `PortalHost` (absolute fill, `box-none`) that provides its own name as the nearest one. The recipe for a native modal |
12
12
  | `host.context.tsx` | `BottomSheetHostNameContext` — the nearest host's name, `null` with no provider; `useBottomSheetHostName` / `useOptionalBottomSheetHostName` |
13
13
  | `sheet-registry.ts` | The pure reducer: `reduceRegistry`, `topOf`, `zIndexOf`, `isTop`, `INITIAL_REGISTRY`. Tested without a renderer |
@@ -86,16 +86,25 @@ thread, turns `overflow` visible so a card's shadow is not clipped, and is
86
86
  overlay. The frame is `overflow: visible` too. The overlay is unchanged — it
87
87
  fills the frame, margins and gap included, so a tap there closes. The rest is
88
88
  `core/`: `closedHeight` is `−restingBottom`, so closed is `translateY =
89
- containerHeight` and the card is fully off-screen; `%` detents resolve against
89
+ containerHeight` and the card is fully off-screen; `%` snap points resolve against
90
90
  the height above the resting line; `keyboardLift` subtracts the gap. The
91
91
  corners are the consumer's `Background` style.
92
92
 
93
93
  **The card moves as one rigid body.** The panel is as tall as the frame, so a
94
94
  detached `Background` is sized by the geometry's `surfaceHeight`
95
- (`core/geometry/surface-height.ts`): the sheet's `height` between its detents,
96
- the first detent's height below them and the last's above. A drag down to
97
- close, the close animation, a rubber-band under the lowest detent and an
95
+ (`core/geometry/surface-height.ts`): the sheet's `height` between its snap points,
96
+ the first snap point's height below them and the last's above. A drag down to
97
+ close, the close animation, a rubber-band under the lowest snap point and an
98
98
  over-drag past the highest therefore leave the surface, the body and the
99
99
  footer exactly as they were and let `translateY` alone move the card, bottom
100
100
  corners included, through the gap and off the screen. `Content` clamps its
101
101
  body to `surfaceHeight − handleHeight` for the same reason.
102
+
103
+ ## Sharing teleport's provider
104
+
105
+ There must be one teleport `PortalProvider` per app: teleport registers hosts natively by name,
106
+ and two providers would mean two hosts called `"root"`. `hasPortalProvider` lets an app that
107
+ already mounts one — `@delacour/react-native-ui`'s `OverlayProvider` is the case it was added for
108
+ — mount the sheet registry and host name alone. Sheets then teleport into the outer provider's
109
+ `"root"`, and their registry `z` (from 1) still orders them among themselves; the skin's overlays
110
+ draw in bands above 1000. The skin's `BottomSheetProvider` passes the flag for you.
@@ -5,6 +5,13 @@ import { SheetRegistryProvider } from "./sheet-registry.context";
5
5
 
6
6
  export type BottomSheetProviderProps = {
7
7
  children?: ReactNode;
8
+ /**
9
+ * A teleport `PortalProvider` is already mounted above, so do not mount a
10
+ * second. Teleport registers hosts natively by name, and two providers
11
+ * would mean two hosts called `"root"`. Sheets then draw into the outer
12
+ * provider's `"root"` host. Default `false`.
13
+ */
14
+ hasPortalProvider?: boolean;
8
15
  };
9
16
 
10
17
  /**
@@ -18,16 +25,21 @@ export type BottomSheetProviderProps = {
18
25
  * without a ref; and the host-name context, set to `"root"`, which is what
19
26
  * makes a `Portal` written anywhere below teleport there without being told.
20
27
  *
28
+ * `hasPortalProvider` drops the first of the three, for an app that already
29
+ * mounts teleport's provider for something else — `@delacour/react-native-ui`'s
30
+ * `OverlayProvider` is the case it exists for.
31
+ *
21
32
  * Nothing else. `GestureHandlerRootView`, `SafeAreaProvider` and
22
33
  * `KeyboardProvider` are the app's — see the package `AGENTS.md`.
23
34
  */
24
- export function BottomSheetProvider({ children }: BottomSheetProviderProps): ReactElement {
25
- return (
26
- <PortalProvider>
27
- <SheetRegistryProvider>
28
- <BottomSheetHostNameContext.Provider value="root">{children}</BottomSheetHostNameContext.Provider>
29
- </SheetRegistryProvider>
30
- </PortalProvider>
35
+ export function BottomSheetProvider({ children, hasPortalProvider = false }: BottomSheetProviderProps): ReactElement {
36
+ const registry = (
37
+ <SheetRegistryProvider>
38
+ <BottomSheetHostNameContext.Provider value="root">{children}</BottomSheetHostNameContext.Provider>
39
+ </SheetRegistryProvider>
31
40
  );
41
+ if (hasPortalProvider) return registry;
42
+
43
+ return <PortalProvider>{registry}</PortalProvider>;
32
44
  }
33
45
  BottomSheetProvider.displayName = "DelacourBottomSheet.BottomSheet.Provider";
@@ -24,12 +24,12 @@ The content pan and the list's native scroll are **simultaneous**: the list's
24
24
  `Native` gesture is declared `simultaneousWithExternalGesture(pans.content)`,
25
25
  so both see every touch. The rule for who moves is the sheet's state:
26
26
 
27
- - **Below the highest detent — locked.** Every scroll event is answered with
27
+ - **Below the highest snap point — locked.** Every scroll event is answered with
28
28
  `scrollTo(ref, 0, scrollLockedAt, false)` and the offset the pan reads is
29
29
  pinned there, so the list consumes nothing and the pan moves the sheet. The
30
30
  indicator is hidden, `bounces` is off and `decelerationRate` is `0`, so a
31
31
  fling has no momentum for the lock to fight.
32
- - **At the highest detent — unlocked.** The list scrolls. The pan treats the
32
+ - **At the highest snap point — unlocked.** The list scrolls. The pan treats the
33
33
  offset the list *began* the gesture with as a budget the finger spends
34
34
  downward before the sheet moves (`listDragHeight`): the list scrolls
35
35
  one-for-one under the same finger, so it reaches its top exactly as the
@@ -38,7 +38,7 @@ so both see every touch. The rule for who moves is the sheet's state:
38
38
  over-dragging.
39
39
  - **A release while scrolled at the top** belongs to the list
40
40
  (`listOwnsRelease`): the velocity is the list's momentum, not a snap. The
41
- library this replaces snapped here and hopped a detent under interrupted
41
+ library this replaces snapped here and hopped a snap point under interrupted
42
42
  scrolls.
43
43
 
44
44
  The budget is the offset at the *start* of the gesture, never the live one.
@@ -78,14 +78,14 @@ earlier answer and is gone: it ended the list above the footer with the rows
78
78
  cut at that edge and an empty band under them.
79
79
 
80
80
  `onContentSizeChange`, less the spacer's own measured height
81
- (`scrollContentHeight`), writes `contentHeight`, which is the dynamic detent's
81
+ (`scrollContentHeight`), writes `contentHeight`, which is the dynamic snap point's
82
82
  measurement. A `BottomSheet.ScrollView` of forty rows therefore needs neither
83
83
  `snapPoints` nor `dynamicSizing={false}`: it sizes to its rows, capped by
84
84
  `maxDynamicContentSize`, and scrolls inside that. The content size and the
85
85
  spacer's layout arrive as two events from one commit; both are held in refs
86
86
  and folded into a single write after the batch, because the first open
87
87
  resolves on whatever `contentHeight` says first and the mount animation, once
88
- running, ignores a corrected detent list — a frame of rows-plus-spacer would
88
+ running, ignores a corrected snap point list — a frame of rows-plus-spacer would
89
89
  open the sheet a footer too tall. `contentHeight` goes back to `UNMEASURED` on
90
90
  unmount for the same reason `useMeasureHeight` resets it.
91
91
 
@@ -4,7 +4,7 @@ import { createBottomSheetScrollable } from "./create-bottom-sheet-scrollable";
4
4
  import type { BottomSheetScrollViewComponent } from "./scrollable.types";
5
5
 
6
6
  /**
7
- * A `ScrollView` as the sheet's body: locked below the highest detent, sized
7
+ * A `ScrollView` as the sheet's body: locked below the highest snap point, sized
8
8
  * to its content when the sheet is dynamic. `Animated.ScrollView` is
9
9
  * Reanimated's own, made once at module scope.
10
10
  */
@@ -58,7 +58,7 @@ function footerElement(footer: ScrollableListFooter | undefined): ReactElement |
58
58
  * object `Content` uses; the inner is a `Native` gesture on the list itself,
59
59
  * declared simultaneous with that pan and kept alive when the finger leaves
60
60
  * the view. Both run at once, and the pan subtracts what the list consumed.
61
- * - The scroll lock (`useScrollLock`). Below the highest detent the list is
61
+ * - The scroll lock (`useScrollLock`). Below the highest snap point the list is
62
62
  * held where it was, so the pan moves the sheet; at the highest it scrolls,
63
63
  * and a drag down at the top hands back to the sheet. The indicator hides,
64
64
  * bounce switches off and momentum is cut while locked.
@@ -72,11 +72,11 @@ function footerElement(footer: ScrollableListFooter | undefined): ReactElement |
72
72
  * shows below the footer's top edge, at rest or while the sheet is dragged
73
73
  * down; the list itself does not shrink to that clip (see `styles.list`).
74
74
  * - Content size. `onContentSizeChange` less the spacer's own measured height
75
- * feeds `contentHeight`, which is the dynamic detent's measurement: a
75
+ * feeds `contentHeight`, which is the dynamic snap point's measurement: a
76
76
  * `ScrollView` of forty rows needs no `snapPoints` and no
77
77
  * `dynamicSizing={false}` — it sizes to its rows up to
78
78
  * `maxDynamicContentSize`, then scrolls. The two layout events land in one
79
- * batch and are folded into one write, so the detent never sees the rows
79
+ * batch and are folded into one write, so the snap point never sees the rows
80
80
  * plus the spacer for a frame — a first open resolved on that sum would
81
81
  * animate to it and ignore the correction.
82
82
  * - Registration. On focus the list tells the sheet what kind it is, and on
@@ -9,9 +9,9 @@ queue that moves them.
9
9
  | --- | --- |
10
10
  | `state.types.ts` | `SheetSharedState` (the raw shared values), `SheetGeometry` (the derived ones), `SheetWorkletConfig` (the props a worklet reads), `ANIM_STATUS` |
11
11
  | `use-sheet-state.ts` | Allocates the raw values once per sheet; mirrors `snapPoints` and the config into them, keyed on serialisation rather than identity |
12
- | `use-detents.ts` | `detents` — `normalizeDetents` plus the dynamic detent once handle and content have measured |
12
+ | `use-snap-points.ts` | `snapPoints` — `normalizeSnapPoints` plus the dynamic snap point once handle and content have measured |
13
13
  | `use-sheet-geometry.ts` | Every formula in `core/AGENTS.md` as a `useDerivedValue`, in dependency order |
14
- | `use-sheet-intents.ts` | The JS-thread `dispatch`, the reaction that resolves an intent on the UI thread, and the reaction that follows a detent change or a container resize |
14
+ | `use-sheet-intents.ts` | The JS-thread `dispatch`, the reaction that resolves an intent on the UI thread, and the reaction that follows a snap point change or a container resize |
15
15
 
16
16
  ## The model
17
17
 
@@ -47,9 +47,9 @@ would see a closed sheet, resolve to nothing, and let the sheet finish opening.
47
47
  The first open a sheet resolves is reported as `"mount"` and, with
48
48
  `animateOnMount: false`, jumps. `mountPending` is that flag.
49
49
 
50
- ## Detents moving under a settled sheet
50
+ ## Snap points moving under a settled sheet
51
51
 
52
- The second reaction watches the detent list and the container height. When
52
+ The second reaction watches the snap point list and the container height. When
53
53
  the list changes with the sheet idle and open, the sheet animates to the same
54
54
  index in the new list — dynamic content growing, a `snapPoints` change. When
55
55
  the container changed, it jumps there instead: a rotation is not something to
@@ -2,7 +2,6 @@ import type { SharedValue } from "react-native-reanimated";
2
2
  import type {
3
3
  AnimationSource,
4
4
  DetachedOptions,
5
- DetentSpec,
6
5
  GestureSource,
7
6
  KeyboardBehavior,
8
7
  KeyboardBlurBehavior,
@@ -10,6 +9,7 @@ import type {
10
9
  ScrollableType,
11
10
  SheetIntent,
12
11
  SheetState,
12
+ SnapPointSpec,
13
13
  } from "../core";
14
14
 
15
15
  /** Whether an animation is running. Numeric so the UI thread never serialises a string. */
@@ -68,7 +68,7 @@ export type SheetSharedState = {
68
68
  gestureSource: SharedValue<GestureSource>;
69
69
  /** The scrollable's live offset, written by its scroll handler; negative while bounced past the top. */
70
70
  scrollOffsetY: SharedValue<number>;
71
- /** Where the scrollable is held while the sheet is below its highest detent. */
71
+ /** Where the scrollable is held while the sheet is below its highest snap point. */
72
72
  scrollLockedAt: SharedValue<number>;
73
73
  /** Which scrollable is the body, `NONE` for static content. Set by `createBottomSheetScrollable` on focus. */
74
74
  scrollableType: SharedValue<ScrollableType>;
@@ -82,7 +82,7 @@ export type SheetSharedState = {
82
82
  /** Whether the next open is the mount open — reported as `"mount"` and skipped by `animateOnMount: false`. */
83
83
  mountPending: SharedValue<boolean>;
84
84
  /** The `snapPoints` prop, string-keyed so identity churn is harmless. */
85
- detentSpec: SharedValue<readonly DetentSpec[]>;
85
+ snapPointSpec: SharedValue<readonly SnapPointSpec[]>;
86
86
  config: SharedValue<SheetWorkletConfig>;
87
87
  };
88
88
 
@@ -93,14 +93,14 @@ export type SheetSharedState = {
93
93
  export type SheetGeometry = {
94
94
  restingBottom: SharedValue<number>;
95
95
  closedHeight: SharedValue<number>;
96
- /** The height the sheet may occupy; what a `%` detent resolves against. */
96
+ /** The height the sheet may occupy; what a `%` snap point resolves against. */
97
97
  maxHeight: SharedValue<number>;
98
98
  /** The safe-area band an attached sheet reserves. */
99
99
  band: SharedValue<number>;
100
100
  bandNow: SharedValue<number>;
101
- /** Ascending, unique, the dynamic detent included when sizing to content. */
102
- detents: SharedValue<readonly number[]>;
103
- /** The last detent, or the closed height when there is none yet. */
101
+ /** Ascending, unique, the dynamic snap point included when sizing to content. */
102
+ snapPoints: SharedValue<readonly number[]>;
103
+ /** The last snap point, or the closed height when there is none yet. */
104
104
  highest: SharedValue<number>;
105
105
  keyboardLift: SharedValue<number>;
106
106
  /** `base + keyboardLift`, clamped. What the view shows. */
@@ -111,7 +111,7 @@ export type SheetGeometry = {
111
111
  index: SharedValue<number>;
112
112
  sheetState: SharedValue<SheetState>;
113
113
  layoutReady: SharedValue<boolean>;
114
- /** What the body may fill above the footer or the band, sized against its detent. */
114
+ /** What the body may fill above the footer or the band, sized against its snap point. */
115
115
  contentArea: SharedValue<number>;
116
116
  footerHeight: SharedValue<number>;
117
117
  footerTop: SharedValue<number>;
@@ -121,12 +121,12 @@ export type SheetGeometry = {
121
121
  * How tall the body's clipping box is: `contentArea + bodyInset` — the body
122
122
  * is laid out to the sheet's bottom line — held under a sticky footer to
123
123
  * the footer's live top edge, so nothing of the body shows below it while
124
- * the sheet is lower than its detent.
124
+ * the sheet is lower than its snap point.
125
125
  */
126
126
  bodyClip: SharedValue<number>;
127
127
  /**
128
- * How tall a detached card's surface is: `height` between the detents, the
129
- * first detent's height below them and the last's above, so a close, a
128
+ * How tall a detached card's surface is: `height` between the snap points, the
129
+ * first snap point's height below them and the last's above, so a close, a
130
130
  * rubber-band and an over-drag move the card as a rigid body. Equal to
131
131
  * `height` when attached.
132
132
  */
@@ -20,7 +20,7 @@ import {
20
20
  surfaceHeight,
21
21
  } from "../core";
22
22
  import type { SheetGeometry, SheetSharedState } from "./state.types";
23
- import { useDetents } from "./use-detents";
23
+ import { useSnapPoints } from "./use-snap-points";
24
24
 
25
25
  /**
26
26
  * Where an unmeasured panel goes: far enough down that no frame of it shows
@@ -46,9 +46,9 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
46
46
  const maxHeight = useDerivedValue(() => availableHeight(state.containerHeight.value, resting.value));
47
47
  const band = useDerivedValue(() => bottomBand(state.config.value.detached !== null, state.config.value.bottomInset));
48
48
  const bandCurrent = useDerivedValue(() => bandNow(band.value, state.keyboardProgress.value));
49
- const detents = useDetents(state, maxHeight, band);
49
+ const snapPoints = useSnapPoints(state, maxHeight, band);
50
50
  const highest = useDerivedValue(() => {
51
- const list = detents.value;
51
+ const list = snapPoints.value;
52
52
  return list.length > 0 ? (list[list.length - 1] as number) : closed.value;
53
53
  });
54
54
  // The keyboard the sheet answers to: owned, and under a behaviour that does
@@ -71,9 +71,9 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
71
71
  if (!(container > 0)) return OFFSCREEN;
72
72
  return positionFor(container, resting.value, height.value);
73
73
  });
74
- const index = useDerivedValue(() => indexForHeight(state.base.value, detents.value, closed.value));
74
+ const index = useDerivedValue(() => indexForHeight(state.base.value, snapPoints.value, closed.value));
75
75
  const sheetStateValue = useDerivedValue(() =>
76
- sheetState(state.base.value, height.value, detents.value, closed.value, maxHeight.value)
76
+ sheetState(state.base.value, height.value, snapPoints.value, closed.value, maxHeight.value)
77
77
  );
78
78
  const layoutReady = useDerivedValue(() =>
79
79
  isLayoutReady({
@@ -88,9 +88,9 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
88
88
  const footer = useDerivedValue(() =>
89
89
  footerHeight(state.config.value.hasFooter, state.footerContentHeight.value, bandCurrent.value)
90
90
  );
91
- // The body is sized against the highest detent rather than `base`, so it
92
- // never reflows during a snap between detents — but a `fillParent` snap
93
- // takes `base` above every detent, and the body follows it up.
91
+ // The body is sized against the highest snap point rather than `base`, so it
92
+ // never reflows during a snap between snap points — but a `fillParent` snap
93
+ // takes `base` above every snap point, and the body follows it up.
94
94
  const area = useDerivedValue(() => {
95
95
  const sheetHeight = Math.min(maxHeight.value, Math.max(highest.value, state.base.value) + lift.value);
96
96
  return contentArea({
@@ -126,7 +126,7 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
126
126
 
127
127
  const surface = useDerivedValue(() => {
128
128
  if (state.config.value.detached === null) return height.value;
129
- const list = detents.value;
129
+ const list = snapPoints.value;
130
130
  const lowest = list.length > 0 ? (list[0] as number) : 0;
131
131
  return surfaceHeight(height.value, lowest, highest.value);
132
132
  });
@@ -138,7 +138,7 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
138
138
  maxHeight,
139
139
  band,
140
140
  bandNow: bandCurrent,
141
- detents,
141
+ snapPoints,
142
142
  highest,
143
143
  keyboardLift: lift,
144
144
  height,
@@ -159,7 +159,7 @@ export function useSheetGeometry(state: SheetSharedState): SheetGeometry {
159
159
  maxHeight,
160
160
  band,
161
161
  bandCurrent,
162
- detents,
162
+ snapPoints,
163
163
  highest,
164
164
  lift,
165
165
  height,