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

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
package/README.md CHANGED
@@ -104,7 +104,7 @@ import { BottomSheet } from "@delacour/react-native-bottom-sheet";
104
104
 
105
105
  `Container` is the panel that moves; `Content` is the static body inside it.
106
106
  Swap the body for `BottomSheet.ScrollView`, `FlatList` or `SectionList` and
107
- the list's content size becomes the sheet's own detent; swap it for
107
+ the list's content size becomes the sheet's own snap point; swap it for
108
108
  `BottomSheet.Steps` with a controller from `useSheetMachine` and the sheet's
109
109
  height glides between the steps of a typed machine. `Footer` is sticky by
110
110
  default, standing on the safe-area band and riding the keyboard's edge.
@@ -119,7 +119,7 @@ take `asChild` to hand their behaviour to an element of yours.
119
119
 
120
120
  ## The maths on its own
121
121
 
122
- `@delacour/react-native-bottom-sheet/core` is every detent, keyboard, footer
122
+ `@delacour/react-native-bottom-sheet/core` is every snap point, keyboard, footer
123
123
  and geometry calculation in the package, plus the step machine, importable
124
124
  with no native module in the module graph:
125
125
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delacour/react-native-bottom-sheet",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.20261006140500",
4
4
  "description": "Headless bottom sheet engine for React Native — no tokens, no className",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,7 +29,7 @@ index the sheet never reached. Settling writes `currentIndex` and schedules
29
29
  `animateOnMount: false`.
30
30
 
31
31
  `settleAt` is the completion alone: no `onAnimate`, no target recorded, `base`
32
- written to the detent it is already within a settle tolerance of, then the same
32
+ written to the snap point it is already within a settle tolerance of, then the same
33
33
  `currentIndex` write and `onSettle`. The content pan uses it for a release the
34
34
  list owns — the finger carried the sheet to the top and kept scrolling, so
35
35
  nothing animated and nothing else would report the index.
@@ -22,7 +22,7 @@ export type JumpTo = (target: number, source: AnimationSource) => void;
22
22
  */
23
23
  export type SettleAt = (target: number, source: AnimationSource) => void;
24
24
 
25
- /** What the JS thread hears when an animation lands — `count` is how many detents there were, for the handle's accessibility value. */
25
+ /** What the JS thread hears when an animation lands — `count` is how many snap points there were, for the handle's accessibility value. */
26
26
  export type SettleListener = (index: number, height: number, source: AnimationSource, count: number) => void;
27
27
 
28
28
  /** What the JS thread hears when an animation starts. */
@@ -25,7 +25,7 @@ export type UseAnimateToOptions = {
25
25
  * timing whose completion writes `currentIndex` and reports the settle.
26
26
  * `jumpTo` is the same without the motion — `forceClose`, a container resize,
27
27
  * and `animateOnMount: false`. `settleAt` is the completion alone: no
28
- * `onAnimate`, no target recorded, `base` written to the detent it is already
28
+ * `onAnimate`, no target recorded, `base` written to the snap point it is already
29
29
  * within a settle tolerance of. It is for the content pan whose release the
30
30
  * list owns — the finger carried the sheet to the top and kept scrolling, so
31
31
  * nothing animated and nothing would otherwise write `currentIndex`.
@@ -57,9 +57,9 @@ export function useAnimateTo(options: UseAnimateToOptions): {
57
57
  const settle = (target: number, source: AnimationSource): void => {
58
58
  "worklet";
59
59
  state.animStatus.value = ANIM_STATUS.IDLE;
60
- const index = Math.round(indexForHeight(target, geometry.detents.value, geometry.closedHeight.value));
60
+ const index = Math.round(indexForHeight(target, geometry.snapPoints.value, geometry.closedHeight.value));
61
61
  state.currentIndex.value = index;
62
- scheduleOnRN(onSettle, index, target, source, geometry.detents.value.length);
62
+ scheduleOnRN(onSettle, index, target, source, geometry.snapPoints.value.length);
63
63
  };
64
64
 
65
65
  const begin = (target: number, source: AnimationSource): void => {
@@ -67,7 +67,7 @@ export function useAnimateTo(options: UseAnimateToOptions): {
67
67
  cancelAnimation(state.base);
68
68
  const fromHeight = state.base.value;
69
69
  const fromIndex = geometry.index.value;
70
- const toIndex = indexForHeight(target, geometry.detents.value, geometry.closedHeight.value);
70
+ const toIndex = indexForHeight(target, geometry.snapPoints.value, geometry.closedHeight.value);
71
71
  state.animStatus.value = ANIM_STATUS.RUNNING;
72
72
  state.animSource.value = source;
73
73
  state.animTarget.value = target;
@@ -7,9 +7,9 @@ export type UseSettleCallbacksOptions = {
7
7
  setOpen: (open: boolean) => void;
8
8
  /** Unmounts the portal's children once a close has landed, unless the sheet is kept mounted. */
9
9
  setPresented: (presented: boolean) => void;
10
- /** The settled index and the detent count, for the handle's accessibility value. */
10
+ /** The settled index and the snap point count, for the handle's accessibility value. */
11
11
  setIndex: (index: number) => void;
12
- setDetentCount: (count: number) => void;
12
+ setSnapPointCount: (count: number) => void;
13
13
  onIndexChange: ((index: number, height: number, source: AnimationSource) => void) | undefined;
14
14
  onClose: (() => void) | undefined;
15
15
  onAnimate: AnimateListener | undefined;
@@ -41,7 +41,7 @@ export function useSettleCallbacks(options: UseSettleCallbacksOptions): {
41
41
  const onSettle = useCallback<SettleListener>((index, height, source, count) => {
42
42
  const current = latest.current;
43
43
  current.setIndex(index);
44
- current.setDetentCount(count);
44
+ current.setSnapPointCount(count);
45
45
  if (index !== lastReported.current) {
46
46
  lastReported.current = index;
47
47
  current.onIndexChange?.(index, height, source);
@@ -67,8 +67,8 @@ No `accessible` container: one would collapse the whole sheet into a single
67
67
  VoiceOver element. Instead the panel is `accessibilityViewIsModal` while open
68
68
  under an overlay, its descendants are hidden while it is closed, and the title
69
69
  labels it through `accessibilityLabelledBy`. The handle is the sheet's one
70
- `adjustable` element: increment and decrement move a detent, escape closes,
71
- and its value reads `Detent i of n`. The overlay is hidden from assistive
70
+ `adjustable` element: increment and decrement move a snap point, escape closes,
71
+ and its value reads `SnapPoint i of n`. The overlay is hidden from assistive
72
72
  technology while it is transparent.
73
73
 
74
74
  ## Contexts
@@ -13,7 +13,7 @@ import type { BottomSheetBackgroundProps } from "./bottom-sheet.types";
13
13
  * The panel is as tall as the frame, so an attached surface fills it and its
14
14
  * bottom edge is always off-screen. A detached card has a visible bottom
15
15
  * edge: the surface is then the geometry's `surfaceHeight` tall, on the UI
16
- * thread — the sheet's height between its detents, and the nearest detent's
16
+ * thread — the sheet's height between its snap points, and the nearest snap point's
17
17
  * height beyond them — so the corners the skin rounds are on screen, the gap
18
18
  * under the card is the overlay's, and a close or an over-drag moves the
19
19
  * whole card as one rigid body rather than shrinking it onto the resting
@@ -20,10 +20,10 @@ const UNCLAMPED = 100_000;
20
20
  * Three boxes. The outermost is the content pan's detector and the clip: its
21
21
  * `maxHeight` follows the geometry's `bodyClip` on the UI thread, so under a
22
22
  * sticky footer nothing of the body is drawn below the footer's top edge, at
23
- * rest or while the sheet is dragged below its detent and the footer holds
23
+ * rest or while the sheet is dragged below its snap point and the footer holds
24
24
  * the screen's bottom. The middle box is the layout: `contentArea` plus the
25
25
  * `bodyInset` the body reserves at its end, which reaches the sheet's bottom
26
- * line, and it never reflows during a snap between detents. The innermost is
26
+ * line, and it never reflows during a snap between snap points. The innermost is
27
27
  * the caller's — it takes the `style` — and its measured height is what a
28
28
  * sheet sized to its content grows to. Yoga lays the inner box out at its
29
29
  * natural height whatever the outer clamps say, which is what makes the
@@ -56,7 +56,7 @@ export function BottomSheetContent({
56
56
 
57
57
  // A detached card's surface is only as tall as the sheet, so its body is
58
58
  // also held to what the surface shows, or the opening animation would draw
59
- // the body under the card. The surface keeps its detent's height while the
59
+ // the body under the card. The surface keeps its snap point's height while the
60
60
  // card slides closed, so the body slides with it, unchanged.
61
61
  const clip = useAnimatedStyle(() => {
62
62
  if (!layoutReady.value) return { maxHeight: UNCLAMPED };
@@ -17,10 +17,10 @@ import type { BottomSheetFooterProps } from "./bottom-sheet.types";
17
17
  * keyboard's top edge without ever moving. Its one view — the one that takes
18
18
  * `style` and `padding` — pads through the resting safe-area band as well, so
19
19
  * the surface a caller styles reaches the sheet's bottom line and its
20
- * background covers the band: while the sheet is dragged below its detent the
20
+ * background covers the band: while the sheet is dragged below its snap point the
21
21
  * footer holds the screen's bottom edge and the body slides down behind it,
22
22
  * and a transparent band would show every line that passed. The box is
23
- * measured less the band into `footerContentHeight`, which the dynamic detent
23
+ * measured less the band into `footerContentHeight`, which the dynamic snap point
24
24
  * counts. The band is constant on purpose — the footer's translate does the
25
25
  * collapsing, and padding that shrank as well would move the footer twice.
26
26
  *
@@ -53,7 +53,7 @@ export function BottomSheetFooter({
53
53
 
54
54
  // A layout effect, not a passive one: the flag has to reach the root's
55
55
  // config before the handle and the content report their heights, or the
56
- // first open resolves on a detent with no footer in it and the mount
56
+ // first open resolves on a snap point with no footer in it and the mount
57
57
  // animation, already running, keeps the corrected list from applying.
58
58
  useLayoutEffect(() => {
59
59
  if (!sticky) return;
@@ -15,9 +15,9 @@ const ACTIONS = [{ name: "increment" }, { name: "decrement" }, { name: "escape"
15
15
  * sized to its content counts it. A handle that is never written is a handle
16
16
  * of height zero; see `Container`.
17
17
  *
18
- * For a screen reader it is the sheet: `adjustable`, with the detent as its
19
- * value, increment and decrement moving one detent either way, and the escape
20
- * gesture closing. Nothing else in the sheet has to know about detents.
18
+ * For a screen reader it is the sheet: `adjustable`, with the snap point as its
19
+ * value, increment and decrement moving one snap point either way, and the escape
20
+ * gesture closing. Nothing else in the sheet has to know about snap points.
21
21
  */
22
22
  export function BottomSheetHandle({
23
23
  style,
@@ -27,7 +27,7 @@ export function BottomSheetHandle({
27
27
  ...props
28
28
  }: BottomSheetHandleProps): ReactElement {
29
29
  const { state, pans, handleMounted, enableHandlePanningGesture } = useBottomSheetInternal();
30
- const { index, detentCount: count, snapToIndex, close } = useBottomSheet();
30
+ const { index, snapPointCount: count, snapToIndex, close } = useBottomSheet();
31
31
  const onLayout = useMeasureHeight(state.handleHeight);
32
32
 
33
33
  useEffect(() => {
@@ -56,7 +56,7 @@ export function BottomSheetHandle({
56
56
  accessibilityActions={ACTIONS}
57
57
  accessibilityLabel={accessibilityLabel}
58
58
  accessibilityRole="adjustable"
59
- accessibilityValue={{ text: index < 0 ? "Closed" : `Detent ${index + 1} of ${count}` }}
59
+ accessibilityValue={{ text: index < 0 ? "Closed" : `Snap point ${index + 1} of ${count}` }}
60
60
  accessible
61
61
  onAccessibilityAction={onAccessibilityAction}
62
62
  onAccessibilityEscape={close}
@@ -9,7 +9,7 @@ import type { BottomSheetOverlayProps } from "./bottom-sheet.types";
9
9
  * The scrim over the app behind the sheet.
10
10
  *
11
11
  * Its opacity follows the keyboard-free index on the UI thread, so a keyboard
12
- * lift never dims the app and a higher detent is no darker. Omit it and the
12
+ * lift never dims the app and a higher snap point is no darker. Omit it and the
13
13
  * sheet has no scrim at all.
14
14
  *
15
15
  * **It is a React Native `Pressable`, never a Gesture Handler tap, and it is
@@ -70,7 +70,7 @@ export type BottomSheetInternalValue = {
70
70
  setHasOverlay: (has: boolean) => void;
71
71
  /** A handle sets this on mount; a container with none reports a zero handle height. */
72
72
  handleMounted: RefObject<boolean>;
73
- /** A sticky footer sets this on mount; the dynamic detent counts it and the body reserves space for it. */
73
+ /** A sticky footer sets this on mount; the dynamic snap point counts it and the body reserves space for it. */
74
74
  hasFooter: boolean;
75
75
  setHasFooter: (has: boolean) => void;
76
76
  /** Where `useBottomSheetTextInput` registers its field. */
@@ -118,7 +118,7 @@ function BottomSheetRoot({
118
118
  keepMounted = false,
119
119
  stackBehavior = "push",
120
120
  closeOnBack = true,
121
- onDetentHaptic,
121
+ onSnapPointHaptic,
122
122
  onCloseHaptic,
123
123
  onOverDragHaptic,
124
124
  }: BottomSheetProps): ReactElement {
@@ -129,7 +129,7 @@ function BottomSheetRoot({
129
129
  });
130
130
  const [presented, setPresented] = useState(isOpen);
131
131
  const [index, setIndex] = useState(CLOSED_INDEX);
132
- const [detentCount, setDetentCount] = useState(0);
132
+ const [snapPointCount, setSnapPointCount] = useState(0);
133
133
  const [hasOverlay, setHasOverlay] = useState(false);
134
134
  const [hasFooter, setHasFooter] = useState(false);
135
135
  const handleMounted = useRef(false);
@@ -140,7 +140,7 @@ function BottomSheetRoot({
140
140
 
141
141
  const detachedOptions = useMemo(() => resolveDetached(detached), [detached]);
142
142
 
143
- // A `Steps` body may override the detents and the dismissibility for the
143
+ // A `Steps` body may override the snap points and the dismissibility for the
144
144
  // step it is on. A step that names its `snapPoints` is sized by them, so
145
145
  // dynamic sizing is off while it is current.
146
146
  const [stepOverride, setStepOverride] = useState<SheetStepOverride | null>(null);
@@ -196,7 +196,7 @@ function BottomSheetRoot({
196
196
  setOpen,
197
197
  setPresented,
198
198
  setIndex,
199
- setDetentCount,
199
+ setSnapPointCount,
200
200
  onIndexChange,
201
201
  onClose,
202
202
  onAnimate,
@@ -214,7 +214,7 @@ function BottomSheetRoot({
214
214
  const pans = useSheetPan(state, geometry, animateTo, settleAt, {
215
215
  enableHandlePanningGesture,
216
216
  enableContentPanningGesture,
217
- onDetentHaptic,
217
+ onSnapPointHaptic,
218
218
  onCloseHaptic,
219
219
  onOverDragHaptic,
220
220
  });
@@ -246,7 +246,9 @@ function BottomSheetRoot({
246
246
  // the way there.
247
247
  const isEffectivelyOpen = useCallback((): boolean => {
248
248
  if (state.animStatus.value === ANIM_STATUS.RUNNING) {
249
- return indexForHeight(state.animTarget.value, geometry.detents.value, geometry.closedHeight.value) > CLOSED_INDEX;
249
+ return (
250
+ indexForHeight(state.animTarget.value, geometry.snapPoints.value, geometry.closedHeight.value) > CLOSED_INDEX
251
+ );
250
252
  }
251
253
  return state.currentIndex.value > CLOSED_INDEX;
252
254
  }, [state, geometry]);
@@ -262,7 +264,7 @@ function BottomSheetRoot({
262
264
  if (isOpen) {
263
265
  setPresented(true);
264
266
  // A move queued by the ref on a closed sheet is a more specific open;
265
- // it waits on the same layout and lands on its own detent.
267
+ // it waits on the same layout and lands on its own snap point.
266
268
  if (!moveQueued.current) dispatch({ kind: "open" });
267
269
  moveQueued.current = false;
268
270
  return;
@@ -288,7 +290,7 @@ function BottomSheetRoot({
288
290
  if (latestOpen.current) setOpen(false);
289
291
  };
290
292
  // A move on a closed sheet is an open: the state flips first so the
291
- // portal mounts, and the queued intent lands on the requested detent.
293
+ // portal mounts, and the queued intent lands on the requested snap point.
292
294
  const move = (request: IntentRequest): void => {
293
295
  if (!isEffectivelyOpen() && !latestOpen.current) {
294
296
  moveQueued.current = true;
@@ -335,8 +337,8 @@ function BottomSheetRoot({
335
337
  }, [presented, closeOnBack, registryTop, methods]);
336
338
 
337
339
  const contextValue = useMemo<BottomSheetContextValue>(
338
- () => ({ ...methods, isOpen, setOpen, index, detentCount }),
339
- [methods, isOpen, setOpen, index, detentCount]
340
+ () => ({ ...methods, isOpen, setOpen, index, snapPointCount }),
341
+ [methods, isOpen, setOpen, index, snapPointCount]
340
342
  );
341
343
 
342
344
  const animatedValue = useMemo<BottomSheetAnimatedValue>(
@@ -349,7 +351,7 @@ function BottomSheetRoot({
349
351
  contentHeight: state.contentHeight,
350
352
  footerHeight: geometry.footerHeight,
351
353
  keyboardLift: geometry.keyboardLift,
352
- detents: geometry.detents,
354
+ snapPoints: geometry.snapPoints,
353
355
  closedHeight: geometry.closedHeight,
354
356
  }),
355
357
  [geometry, state]
@@ -5,11 +5,11 @@ import type { SheetAnimation } from "../animation/animation.types";
5
5
  import type {
6
6
  AnimationSource,
7
7
  DetachedProp,
8
- DetentSpec,
9
8
  KeyboardBehavior,
10
9
  KeyboardBlurBehavior,
11
10
  KeyboardScope,
12
11
  ReduceMotionMode,
12
+ SnapPointSpec,
13
13
  } from "../core";
14
14
  import type { SheetHaptics } from "../gesture/gesture.types";
15
15
 
@@ -20,12 +20,12 @@ import type { SheetHaptics } from "../gesture/gesture.types";
20
20
  * `dismiss` is `close` under the name the library this replaces used. Every
21
21
  * method is an intent: none of them animates from the JS thread, and each is
22
22
  * a no-op when it makes no sense — `close` on a closed sheet, `snapToIndex(4)`
23
- * with two detents.
23
+ * with two snap points.
24
24
  */
25
25
  export type BottomSheetRef = {
26
26
  snapToIndex: (index: number) => void;
27
- /** A height or a `%` of the available height; clamped, never added as a detent. */
28
- snapToPosition: (position: DetentSpec) => void;
27
+ /** A height or a `%` of the available height; clamped, never added as a snap point. */
28
+ snapToPosition: (position: SnapPointSpec) => void;
29
29
  expand: () => void;
30
30
  collapse: () => void;
31
31
  close: () => void;
@@ -48,9 +48,9 @@ export type BottomSheetProps = SheetHaptics & {
48
48
  * `isOpen`. One callback for every path.
49
49
  */
50
50
  onOpenChange?: (isOpen: boolean) => void;
51
- /** Controlled detent index. Changing it snaps an open sheet. */
51
+ /** Controlled snap point index. Changing it snaps an open sheet. */
52
52
  index?: number;
53
- /** The detent `open` lands on. @default 0 */
53
+ /** The snap point `open` lands on. @default 0 */
54
54
  initialIndex?: number;
55
55
  /** The settled index changed — `-1` on close. */
56
56
  onIndexChange?: (index: number, height: number, source: AnimationSource) => void;
@@ -65,10 +65,10 @@ export type BottomSheetProps = SheetHaptics & {
65
65
  /** The sheet reached its closed height, by any path. `onOpenChange(false)` follows. */
66
66
  onClose?: () => void;
67
67
  /** Heights above the resting bottom, or `%` of the available height. @default [] */
68
- snapPoints?: readonly DetentSpec[];
69
- /** Adds the content's own height as a detent. @default true */
68
+ snapPoints?: readonly SnapPointSpec[];
69
+ /** Adds the content's own height as a snap point. @default true */
70
70
  dynamicSizing?: boolean;
71
- /** Caps the dynamic detent. */
71
+ /** Caps the dynamic snap point. */
72
72
  maxDynamicContentSize?: number;
73
73
  /** Pixels the frame leaves clear at the top. @default 0 */
74
74
  topInset?: number;
@@ -78,7 +78,7 @@ export type BottomSheetProps = SheetHaptics & {
78
78
  * A floating card: inset by `horizontalMargin` on both sides, resting
79
79
  * `bottomOffset` above `bottomInset`, every corner the `Background`'s to
80
80
  * round. `true` is `{ horizontalMargin: 16, bottomOffset: 16 }`. Closed is
81
- * fully off-screen, `%` detents resolve against the height above the resting
81
+ * fully off-screen, `%` snap points resolve against the height above the resting
82
82
  * line, and a tap in the margins or the gap closes.
83
83
  */
84
84
  detached?: DetachedProp;
@@ -88,7 +88,7 @@ export type BottomSheetProps = SheetHaptics & {
88
88
  enableHandlePanningGesture?: boolean;
89
89
  /** @default true */
90
90
  enableContentPanningGesture?: boolean;
91
- /** Rubber-band past the detents. @default true */
91
+ /** Rubber-band past the snap points. @default true */
92
92
  enableOverDrag?: boolean;
93
93
  /** Larger allows more travel. @default 2.5 */
94
94
  overDragResistanceFactor?: number;
@@ -152,7 +152,7 @@ export type BottomSheetOverlayProps = Omit<ViewProps, "style"> & {
152
152
  disappearsOnIndex?: number;
153
153
  /** Opacity at full appearance. @default 1 */
154
154
  opacity?: number;
155
- /** What a press does: close, collapse to the first detent, snap to an index, or nothing. @default "close" */
155
+ /** What a press does: close, collapse to the first snap point, snap to an index, or nothing. @default "close" */
156
156
  pressBehavior?: "close" | "collapse" | "none" | number;
157
157
  /** Let touches reach the app behind the scrim. @default false */
158
158
  enableTouchThrough?: boolean;
@@ -216,8 +216,8 @@ export type BottomSheetContextValue = BottomSheetRef & {
216
216
  setOpen: (open: boolean) => void;
217
217
  /** The settled index, `-1` closed. React state, updated on settle. */
218
218
  index: number;
219
- /** How many detents there were at the last settle. React state, for the handle's accessibility value. */
220
- detentCount: number;
219
+ /** How many snap points there were at the last settle. React state, for the handle's accessibility value. */
220
+ snapPointCount: number;
221
221
  };
222
222
 
223
223
  /** What `useBottomSheetAnimated()` returns: the shared values a skin animates against. */
@@ -230,6 +230,6 @@ export type BottomSheetAnimatedValue = {
230
230
  contentHeight: SharedValue<number>;
231
231
  footerHeight: SharedValue<number>;
232
232
  keyboardLift: SharedValue<number>;
233
- detents: SharedValue<readonly number[]>;
233
+ snapPoints: SharedValue<readonly number[]>;
234
234
  closedHeight: SharedValue<number>;
235
235
  };
@@ -17,7 +17,7 @@ failure mode makes a test necessary rather than a convention sufficient.
17
17
  ## Height space
18
18
 
19
19
  Every value here is a **height**: pixels of sheet visible above its resting
20
- bottom line. `0` is closed, detents ascend, `-1` (`UNMEASURED`) is a
20
+ bottom line. `0` is closed, snap points ascend, `-1` (`UNMEASURED`) is a
21
21
  measurement that has not happened. `translateY` is derived from a height only
22
22
  at the moment a view needs it, by `geometry/position.ts`, and nowhere else.
23
23
 
@@ -25,14 +25,14 @@ at the moment a view needs it, by `geometry/position.ts`, and nowhere else.
25
25
 
26
26
  | Path | What |
27
27
  | --- | --- |
28
- | `sheet.types.ts` | `DetentSpec`, the keyboard behaviours, `AnimationSource`, `SheetIntent`, the numeric enums and the two sentinels |
28
+ | `sheet.types.ts` | `SnapPointSpec`, the keyboard behaviours, `AnimationSource`, `SheetIntent`, the numeric enums and the two sentinels |
29
29
  | `result.ts` | The house `Result` union, copied from the private `@delacour/types` |
30
- | `detents/normalize-detents.ts` | `parseDetent` (JS, a `Result`) and W `normalizeDetents(spec, available)` — ascending, unique, clamped, invalid entries skipped |
31
- | `detents/dynamic-detent.ts` | W `dynamicDetent` — handle + content + footer content + band once, capped |
32
- | `detents/index-for-height.ts` | W `indexForHeight` / `heightForIndex` — piecewise-linear over `[closed, …detents]`, clamped |
33
- | `detents/select-snap-height.ts` | W `selectSnapHeight` — nearest candidate to `height + projection · velocity`; closes only when given a closed height |
34
- | `detents/over-drag.ts` | W `resistOverDrag` — slope 1 at the edge, square-root beyond; factor 0 clamps |
35
- | `detents/sheet-state.ts` | W `sheetState` — closed / opened / extended / overExtended / fill |
30
+ | `snap-points/normalize-snap-points.ts` | `parseSnapPoint` (JS, a `Result`) and W `normalizeSnapPoints(spec, available)` — ascending, unique, clamped, invalid entries skipped |
31
+ | `snap-points/dynamic-snap-point.ts` | W `dynamicSnapPoint` — handle + content + footer content + band once, capped |
32
+ | `snap-points/index-for-height.ts` | W `indexForHeight` / `heightForIndex` — piecewise-linear over `[closed, …snapPoints]`, clamped |
33
+ | `snap-points/select-snap-height.ts` | W `selectSnapHeight` — nearest candidate to `height + projection · velocity`; closes only when given a closed height |
34
+ | `snap-points/over-drag.ts` | W `resistOverDrag` — slope 1 at the edge, square-root beyond; factor 0 clamps |
35
+ | `snap-points/sheet-state.ts` | W `sheetState` — closed / opened / extended / overExtended / fill |
36
36
  | `keyboard/keyboard-lift.ts` | W `keyboardInContainer`, W `keyboardLift` — the lift, band and gap subtracted in step with progress |
37
37
  | `keyboard/keyboard-owner.ts` | W `isInputInsideSheet` — the `keyboardScope: "inside"` geometry test |
38
38
  | `keyboard/content-area.ts` | W `contentArea` — what the body may fill, never negative |
@@ -49,9 +49,9 @@ at the moment a view needs it, by `geometry/position.ts`, and nowhere else.
49
49
  | `backdrop/backdrop-opacity.ts` | W `backdropOpacity`, W `backdropInteractive` |
50
50
  | `intent/resolve-intent.ts` | W `resolveIntent(state, intent)` — `wait` / `animate` / `jump` / `null`; open means `currentIndex ≥ 0` **or** `base` above the closed height, so a stale index never strands a visible sheet |
51
51
  | `intent/layout-ready.ts` | W `isLayoutReady` |
52
- | `haptic/crossed-detent.ts` | W `crossedDetent` (boolean), W `detentUnder` (index) |
52
+ | `haptic/crossed-snap-point.ts` | W `crossedSnapPoint` (boolean), W `snapPointUnder` (index) |
53
53
  | `scroll/scroll-lock.ts` | W `shouldLockScroll`, W `contentPanDrivesSheet` |
54
- | `scroll/scroll-pan.ts` | W `listDragHeight` — the start offset as a budget the finger spends before the sheet moves; W `listOwnsRelease`; W `restingDetent` — the detent within the settle tolerance of a base, for a release the list owned; W `scrollLockTarget` |
54
+ | `scroll/scroll-pan.ts` | W `listDragHeight` — the start offset as a budget the finger spends before the sheet moves; W `listOwnsRelease`; W `restingSnapPoint` — the snap point within the settle tolerance of a base, for a release the list owned; W `scrollLockTarget` |
55
55
  | `animation/select-animation.ts` | `selectAnimation`, `IOS_SPRING`, `ANDROID_TIMING` — platform defaults as data, easing by name |
56
56
 
57
57
  W marks a module-scope `"worklet"`; each is flat (see the package `AGENTS.md`)
@@ -69,12 +69,12 @@ restingBottom = detached ? bottomOffset + inset : 0 closedHeight = −r
69
69
  available = C − restingBottom maxHeight = available
70
70
  band = detached ? 0 : inset bandNow = band · (1 − p)
71
71
  footerHeight = hasFooter ? footerContent + bandNow : 0
72
- dynamicDetent = min(handle + content + (hasFooter ? footerContent : 0) + band, maxDynamicContentSize, available)
73
- detents = sortAsc(unique(normalize(spec, available) ∪ {dynamicDetent if dynamicSizing}))
72
+ dynamicSnapPoint = min(handle + content + (hasFooter ? footerContent : 0) + band, maxDynamicContentSize, available)
73
+ snapPoints = sortAsc(unique(normalize(spec, available) ∪ {dynamicSnapPoint if dynamicSizing}))
74
74
  keyboardLift = behavior ∈ {interactive, extend} ? max(0, kb − band · p − (detached ? restingBottom : 0)) : 0
75
75
  height = clamp(base + keyboardLift, closedHeight, maxHeight)
76
76
  translateY = C − restingBottom − height
77
- index = piecewise(base, [closedHeight, …detents] → [−1, 0, 1, …]) // base, never height
77
+ index = piecewise(base, [closedHeight, …snapPoints] → [−1, 0, 1, …]) // base, never height
78
78
  contentArea = max(0, min(maxHeight, highest + keyboardLift) − handle − footerHeight − kb − (hasFooter ? 0 : bandNow))
79
79
  footerTop = max(0, height − kb − footerContent − bandNow)
80
80
  bodyInset = hasFooter ? footerHeight : bandNow
@@ -92,18 +92,18 @@ the footer or the band and its last row can still be scrolled clear of them.
92
92
  `bodyClip` is what of that may show: without a footer all of it, so a sheet
93
93
  slides as one body; under a footer only what is above the footer's live top
94
94
  edge — `height − kb − handle − footerHeight`, which is `footerTop − handle` —
95
- so while the sheet is dragged below its detent and the footer holds the
96
- screen's bottom edge, no line of the body is drawn under it. On the detent the
95
+ so while the sheet is dragged below its snap point and the footer holds the
96
+ screen's bottom edge, no line of the body is drawn under it. On the snap point the
97
97
  clip is exactly `contentArea`. A scrollable's `contentHeight` is
98
98
  `scrollContentHeight(contentSize, spacer)`: the spacer is the footer and the
99
- band, which `dynamicDetent` adds once already.
99
+ band, which `dynamicSnapPoint` adds once already.
100
100
 
101
101
  Two deviations from the plan's signatures, both on purpose:
102
102
 
103
103
  - `resolveIntent` returns `{ action } | null` rather than `number | null`,
104
104
  because `forceClose` needs a jump the caller can tell from an animate, and
105
105
  an open before layout needs a *wait* the caller can tell from a no-op.
106
- - `crossedDetent` returns a boolean and `detentUnder` the index, rather than
106
+ - `crossedSnapPoint` returns a boolean and `snapPointUnder` the index, rather than
107
107
  one function doing both — a number that means "unchanged" when it equals its
108
108
  input is a comparison the caller has to make anyway, spelled worse.
109
109
 
@@ -12,7 +12,7 @@ describe("backdropOpacity", () => {
12
12
  expect(backdropOpacity(-0.25, -1, 0, 0.6)).toBeCloseTo(0.45);
13
13
  });
14
14
 
15
- test("plateaus above the appearing index — a higher detent is no darker", () => {
15
+ test("plateaus above the appearing index — a higher snap point is no darker", () => {
16
16
  expect(backdropOpacity(1, -1, 0, 1)).toBe(1);
17
17
  expect(backdropOpacity(2.5, -1, 0, 0.7)).toBe(0.7);
18
18
  });
@@ -42,7 +42,7 @@ describe("backdropInteractive", () => {
42
42
  expect(backdropInteractive(0, -1)).toBe(true);
43
43
  });
44
44
 
45
- test("with a higher disappearing index the lowest detent leaves the app tappable", () => {
45
+ test("with a higher disappearing index the lowest snap point leaves the app tappable", () => {
46
46
  expect(backdropInteractive(0, 0)).toBe(false);
47
47
  expect(backdropInteractive(1, 0)).toBe(true);
48
48
  });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The overlay's opacity at an index: `0` up to `disappearsOn`, rising
3
- * linearly to `opacity` at `appearsOn`, and a plateau above — a higher detent
3
+ * linearly to `opacity` at `appearsOn`, and a plateau above — a higher snap point
4
4
  * is no darker. Fed the keyboard-free index, so a keyboard lift never dims
5
5
  * the app behind the sheet.
6
6
  *
@@ -24,18 +24,18 @@ describe("bodyClip", () => {
24
24
  const at = (sheetHeight: number) =>
25
25
  bodyClip({ contentArea: 486, inset: 90, hasFooter: true, sheetHeight, handleHeight: 24, keyboardHeight: 0 });
26
26
 
27
- test("at rest on its detent the body is clipped exactly at the footer's top edge", () => {
27
+ test("at rest on its snap point the body is clipped exactly at the footer's top edge", () => {
28
28
  // contentArea 486 = 600 − 24 − 90; the body is laid out 90 deeper, to the
29
29
  // sheet's bottom line, and the clip hides that 90 behind the footer.
30
30
  expect(at(600)).toBe(486);
31
31
  });
32
32
 
33
- test("dragged below its detent the clip follows the live height, so nothing shows under the footer", () => {
33
+ test("dragged below its snap point the clip follows the live height, so nothing shows under the footer", () => {
34
34
  expect(at(500)).toBe(386);
35
35
  expect(at(200)).toBe(86);
36
36
  });
37
37
 
38
- test("above its detent — an over-drag — the clip stays at the laid-out body", () => {
38
+ test("above its snap point — an over-drag — the clip stays at the laid-out body", () => {
39
39
  expect(at(700)).toBe(486 + 90);
40
40
  });
41
41
 
@@ -16,7 +16,7 @@ export function bodyInset(hasFooter: boolean, footerHeight: number, band: number
16
16
  }
17
17
 
18
18
  export type BodyClipInput = {
19
- /** The geometry's `contentArea` — the body sized against its detent. */
19
+ /** The geometry's `contentArea` — the body sized against its snap point. */
20
20
  contentArea: number;
21
21
  /** `bodyInset` — what the body reserves at its end. */
22
22
  inset: number;
@@ -35,7 +35,7 @@ export type BodyClipInput = {
35
35
  * — and without a footer that is the clip too: the sheet slides as one body
36
36
  * and nothing needs hiding. Under a sticky footer the clip also follows the
37
37
  * footer's top edge, `sheetHeight − keyboard − handle − inset`, which is where
38
- * `footerTop` puts it. On the detent the two agree and the clip is exactly
38
+ * `footerTop` puts it. On the snap point the two agree and the clip is exactly
39
39
  * `contentArea`; below it — a drag to close, the close animation — the footer
40
40
  * stays on the screen's bottom edge while the panel slides down under it, and
41
41
  * the clip shrinks in step so no line of the body is ever drawn below the
@@ -56,7 +56,7 @@ export function bodyClip(input: BodyClipInput): number {
56
56
  /**
57
57
  * The rows' own height: a scrollable's reported content size less the
58
58
  * trailing spacer it renders inside that content. This is what the dynamic
59
- * detent counts — the spacer is the footer and the band, which the detent
59
+ * snap point counts — the spacer is the footer and the band, which the snap point
60
60
  * already adds once. An unmeasured spacer subtracts nothing; never negative.
61
61
  */
62
62
  export function scrollContentHeight(contentSize: number, spacer: number): number {
@@ -5,7 +5,7 @@
5
5
  * Height space is the engine's coordinate system; this is the one place it is
6
6
  * converted to a transform. A detached sheet rests `restingBottom` above the
7
7
  * container's bottom edge, so its closed position is the full container
8
- * height and every detent sits that much higher.
8
+ * height and every snap point sits that much higher.
9
9
  */
10
10
  export function positionFor(containerHeight: number, restingBottom: number, height: number): number {
11
11
  "worklet";
@@ -2,28 +2,28 @@ import { describe, expect, test } from "bun:test";
2
2
  import { surfaceHeight } from "./surface-height";
3
3
 
4
4
  describe("surfaceHeight", () => {
5
- test("between the detents the surface follows the height", () => {
5
+ test("between the snap points the surface follows the height", () => {
6
6
  expect(surfaceHeight(300, 200, 400)).toBe(300);
7
7
  expect(surfaceHeight(200, 200, 400)).toBe(200);
8
8
  expect(surfaceHeight(400, 200, 400)).toBe(400);
9
9
  });
10
10
 
11
- test("below the first detent the surface keeps the detent's height, so a close slides the card rigidly", () => {
11
+ test("below the first snap point the surface keeps the snap point's height, so a close slides the card rigidly", () => {
12
12
  expect(surfaceHeight(120, 200, 400)).toBe(200);
13
13
  expect(surfaceHeight(0, 200, 400)).toBe(200);
14
14
  expect(surfaceHeight(-50, 200, 400)).toBe(200);
15
15
  });
16
16
 
17
- test("above the last detent the surface keeps the detent's height, so an over-drag lifts the card rigidly", () => {
17
+ test("above the last snap point the surface keeps the snap point's height, so an over-drag lifts the card rigidly", () => {
18
18
  expect(surfaceHeight(450, 200, 400)).toBe(400);
19
19
  });
20
20
 
21
- test("one detent is a rigid card at every height", () => {
21
+ test("one snap point is a rigid card at every height", () => {
22
22
  expect(surfaceHeight(10, 320, 320)).toBe(320);
23
23
  expect(surfaceHeight(500, 320, 320)).toBe(320);
24
24
  });
25
25
 
26
- test("no detents yet: never negative, never above zero", () => {
26
+ test("no snap points yet: never negative, never above zero", () => {
27
27
  expect(surfaceHeight(-16, 0, 0)).toBe(0);
28
28
  expect(surfaceHeight(-16, -16, -16)).toBe(0);
29
29
  });