@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
@@ -0,0 +1,65 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { heightForIndex, indexForHeight } from "./index-for-height";
3
+
4
+ const snapPoints = [200, 400, 800];
5
+
6
+ describe("indexForHeight", () => {
7
+ test("each snap point maps to its own integer index", () => {
8
+ expect(indexForHeight(200, snapPoints, 0)).toBe(0);
9
+ expect(indexForHeight(400, snapPoints, 0)).toBe(1);
10
+ expect(indexForHeight(800, snapPoints, 0)).toBe(2);
11
+ });
12
+
13
+ test("the closed height is -1", () => {
14
+ expect(indexForHeight(0, snapPoints, 0)).toBe(-1);
15
+ expect(indexForHeight(-32, snapPoints, -32)).toBe(-1);
16
+ });
17
+
18
+ test("between snap points the index is fractional, so the backdrop can follow a drag", () => {
19
+ expect(indexForHeight(100, snapPoints, 0)).toBeCloseTo(-0.5);
20
+ expect(indexForHeight(300, snapPoints, 0)).toBeCloseTo(0.5);
21
+ expect(indexForHeight(600, snapPoints, 0)).toBeCloseTo(1.5);
22
+ });
23
+
24
+ test("clamps beyond either end — an over-drag never reports a snap point that does not exist", () => {
25
+ expect(indexForHeight(-100, snapPoints, 0)).toBe(-1);
26
+ expect(indexForHeight(900, snapPoints, 0)).toBe(2);
27
+ });
28
+
29
+ test("with no snap points every height is closed", () => {
30
+ expect(indexForHeight(300, [], 0)).toBe(-1);
31
+ });
32
+
33
+ test("a snap point coinciding with the closed height does not divide by zero", () => {
34
+ expect(Number.isFinite(indexForHeight(0, [0, 400], 0))).toBe(true);
35
+ expect(indexForHeight(400, [0, 400], 0)).toBe(1);
36
+ });
37
+ });
38
+
39
+ describe("heightForIndex", () => {
40
+ test("round-trips every snap point", () => {
41
+ for (const [index, height] of snapPoints.entries()) {
42
+ expect(heightForIndex(index, snapPoints, 0)).toBe(height);
43
+ expect(indexForHeight(heightForIndex(index, snapPoints, 0), snapPoints, 0)).toBe(index);
44
+ }
45
+ });
46
+
47
+ test("-1 is the closed height, resting line included", () => {
48
+ expect(heightForIndex(-1, snapPoints, 0)).toBe(0);
49
+ expect(heightForIndex(-1, snapPoints, -32)).toBe(-32);
50
+ });
51
+
52
+ test("clamps an out-of-range index to the nearest end", () => {
53
+ expect(heightForIndex(5, snapPoints, 0)).toBe(800);
54
+ expect(heightForIndex(-3, snapPoints, 0)).toBe(0);
55
+ });
56
+
57
+ test("a fractional index interpolates, mirroring indexForHeight", () => {
58
+ expect(heightForIndex(0.5, snapPoints, 0)).toBe(300);
59
+ expect(heightForIndex(-0.5, snapPoints, 0)).toBe(100);
60
+ });
61
+
62
+ test("with no snap points every index is closed", () => {
63
+ expect(heightForIndex(0, [], -16)).toBe(-16);
64
+ });
65
+ });
@@ -1,21 +1,21 @@
1
1
  import { CLOSED_INDEX } from "../sheet.types";
2
2
 
3
3
  /**
4
- * The index a height corresponds to: `-1` closed, `0` the lowest detent,
4
+ * The index a height corresponds to: `-1` closed, `0` the lowest snap point,
5
5
  * fractional in between, clamped at both ends.
6
6
  *
7
- * Piecewise-linear over `[closedHeight, ...detents]` → `[-1, 0, 1, …]`. It is
7
+ * Piecewise-linear over `[closedHeight, ...snapPoints]` → `[-1, 0, 1, …]`. It is
8
8
  * fed `base`, never `height`, so a keyboard lift moves the sheet without
9
9
  * moving the index or the backdrop that follows it.
10
10
  */
11
- export function indexForHeight(height: number, detents: readonly number[], closedHeight: number): number {
11
+ export function indexForHeight(height: number, snapPoints: readonly number[], closedHeight: number): number {
12
12
  "worklet";
13
- const count = detents.length;
13
+ const count = snapPoints.length;
14
14
  if (count === 0 || height <= closedHeight) return CLOSED_INDEX;
15
15
 
16
16
  let lower = closedHeight;
17
17
  for (let index = 0; index < count; index += 1) {
18
- const upper = detents[index] as number;
18
+ const upper = snapPoints[index] as number;
19
19
  if (height <= upper) {
20
20
  const span = upper - lower;
21
21
  if (span <= 0) return index;
@@ -28,17 +28,17 @@ export function indexForHeight(height: number, detents: readonly number[], close
28
28
 
29
29
  /**
30
30
  * The inverse: the height at an index, `-1` being the closed height. Integers
31
- * land on detents, fractions interpolate, and an out-of-range index clamps.
31
+ * land on snap points, fractions interpolate, and an out-of-range index clamps.
32
32
  */
33
- export function heightForIndex(index: number, detents: readonly number[], closedHeight: number): number {
33
+ export function heightForIndex(index: number, snapPoints: readonly number[], closedHeight: number): number {
34
34
  "worklet";
35
- const count = detents.length;
35
+ const count = snapPoints.length;
36
36
  if (count === 0 || index <= CLOSED_INDEX) return closedHeight;
37
- if (index >= count - 1) return detents[count - 1] as number;
37
+ if (index >= count - 1) return snapPoints[count - 1] as number;
38
38
 
39
39
  const upperIndex = Math.ceil(index);
40
- const lower = upperIndex === 0 ? closedHeight : (detents[upperIndex - 1] as number);
41
- const upper = detents[upperIndex] as number;
40
+ const lower = upperIndex === 0 ? closedHeight : (snapPoints[upperIndex - 1] as number);
41
+ const upper = snapPoints[upperIndex] as number;
42
42
  const fraction = index - (upperIndex - 1);
43
43
  return lower + (upper - lower) * fraction;
44
44
  }
@@ -0,0 +1,72 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { SnapPointSpec } from "../sheet.types";
3
+ import { normalizeSnapPoints, parseSnapPoint } from "./normalize-snap-points";
4
+
5
+ describe("parseSnapPoint", () => {
6
+ test("a finite non-negative number is pixels", () => {
7
+ expect(parseSnapPoint(300)).toEqual({ success: true, data: { kind: "px", value: 300 } });
8
+ expect(parseSnapPoint(0)).toEqual({ success: true, data: { kind: "px", value: 0 } });
9
+ });
10
+
11
+ test("a percentage string is a fraction of the available height", () => {
12
+ expect(parseSnapPoint("50%")).toEqual({ success: true, data: { kind: "percent", value: 50 } });
13
+ expect(parseSnapPoint("12.5%")).toEqual({ success: true, data: { kind: "percent", value: 12.5 } });
14
+ });
15
+
16
+ test("refuses NaN, infinities and negatives by name", () => {
17
+ for (const bad of [Number.NaN, Number.POSITIVE_INFINITY, -1] as const) {
18
+ expect(parseSnapPoint(bad)).toEqual({ success: false, error: { code: "invalid-snap-point", value: bad } });
19
+ }
20
+ });
21
+
22
+ test("refuses a string that is not a percentage", () => {
23
+ for (const bad of ["50", "abc%", "%", "-10%", "50px"]) {
24
+ expect(parseSnapPoint(bad as SnapPointSpec)).toEqual({
25
+ success: false,
26
+ error: { code: "invalid-snap-point", value: bad },
27
+ });
28
+ }
29
+ });
30
+
31
+ test("refuses anything that is not a number or a string", () => {
32
+ expect(parseSnapPoint(undefined as unknown as SnapPointSpec)).toEqual({
33
+ success: false,
34
+ error: { code: "invalid-snap-point", value: undefined },
35
+ });
36
+ });
37
+ });
38
+
39
+ describe("normalizeSnapPoints", () => {
40
+ test("pixels pass through", () => {
41
+ expect(normalizeSnapPoints([200, 400], 800)).toEqual([200, 400]);
42
+ });
43
+
44
+ test("percentages resolve against the available height", () => {
45
+ expect(normalizeSnapPoints(["25%", "50%", "100%"], 800)).toEqual([200, 400, 800]);
46
+ });
47
+
48
+ test("mixed specs sort ascending regardless of authoring order", () => {
49
+ expect(normalizeSnapPoints(["100%", 100, "50%"], 800)).toEqual([100, 400, 800]);
50
+ });
51
+
52
+ test("duplicates collapse, including a pixel and a percentage that agree", () => {
53
+ expect(normalizeSnapPoints([400, "50%", 400], 800)).toEqual([400]);
54
+ });
55
+
56
+ test("clamps to the available height and to zero", () => {
57
+ expect(normalizeSnapPoints([1000, "150%"], 800)).toEqual([800]);
58
+ });
59
+
60
+ test("skips an invalid spec rather than poisoning the list", () => {
61
+ expect(normalizeSnapPoints([Number.NaN, "x%" as SnapPointSpec, 300], 800)).toEqual([300]);
62
+ });
63
+
64
+ test("an unmeasured or zero available height yields nothing to snap to", () => {
65
+ expect(normalizeSnapPoints(["50%"], -1)).toEqual([]);
66
+ expect(normalizeSnapPoints([300], 0)).toEqual([]);
67
+ });
68
+
69
+ test("an empty spec is an empty list", () => {
70
+ expect(normalizeSnapPoints([], 800)).toEqual([]);
71
+ });
72
+ });
@@ -1,21 +1,21 @@
1
1
  import { err, ok, type Result } from "../result";
2
- import type { DetentSpec } from "../sheet.types";
2
+ import type { SnapPointSpec } from "../sheet.types";
3
3
 
4
- /** A detent with its unit made explicit. */
5
- export type ParsedDetent = { kind: "px"; value: number } | { kind: "percent"; value: number };
4
+ /** A snap point with its unit made explicit. */
5
+ export type ParsedSnapPoint = { kind: "px"; value: number } | { kind: "percent"; value: number };
6
6
 
7
- /** Why a detent could not be read. `value` is what the consumer wrote. */
8
- export type DetentError = { code: "invalid-snap-point"; value: unknown };
7
+ /** Why a snap point could not be read. `value` is what the consumer wrote. */
8
+ export type SnapPointError = { code: "invalid-snap-point"; value: unknown };
9
9
 
10
10
  /**
11
- * Reads one detent as the consumer wrote it — `300` or `"50%"` — refusing
11
+ * Reads one snap point as the consumer wrote it — `300` or `"50%"` — refusing
12
12
  * `NaN`, infinities, negatives and any string that is not a percentage.
13
13
  *
14
14
  * This is the JS-thread validator, for `snapToPosition` and for surfacing a
15
15
  * bad `snapPoints` prop by name. The worklet below re-implements the same
16
16
  * parse inline, because a module-scope worklet may not call it.
17
17
  */
18
- export function parseDetent(spec: DetentSpec): Result<ParsedDetent, DetentError> {
18
+ export function parseSnapPoint(spec: SnapPointSpec): Result<ParsedSnapPoint, SnapPointError> {
19
19
  if (typeof spec === "number") {
20
20
  if (!Number.isFinite(spec) || spec < 0) return err({ code: "invalid-snap-point", value: spec });
21
21
  return ok({ kind: "px", value: spec });
@@ -36,10 +36,10 @@ export function parseDetent(spec: DetentSpec): Result<ParsedDetent, DetentError>
36
36
  * the resting bottom for a detached sheet. While it is unmeasured (or zero)
37
37
  * there is nothing to snap to, and the result is empty rather than a list of
38
38
  * zeros. An invalid entry is skipped, never written into the list: one `NaN`
39
- * in `detents` freezes a sheet.
39
+ * in `snapPoints` freezes a sheet.
40
40
  */
41
41
  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: the flat-worklet rule forbids extracting helpers; see the doc comment above.
42
- export function normalizeDetents(spec: readonly DetentSpec[], available: number): number[] {
42
+ export function normalizeSnapPoints(spec: readonly SnapPointSpec[], available: number): number[] {
43
43
  "worklet";
44
44
  if (!(available > 0)) return [];
45
45
 
@@ -1,22 +1,22 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { selectSnapHeight } from "./select-snap-height";
3
3
 
4
- const detents = [200, 400, 800];
4
+ const snapPoints = [200, 400, 800];
5
5
  const at = (height: number, velocity = 0, closedHeight: number | null = 0) =>
6
- selectSnapHeight({ height, velocity, detents, closedHeight, projection: 0.2 });
6
+ selectSnapHeight({ height, velocity, snapPoints, closedHeight, projection: 0.2 });
7
7
 
8
8
  describe("selectSnapHeight", () => {
9
- test("at rest, the nearest detent wins", () => {
9
+ test("at rest, the nearest snap point wins", () => {
10
10
  expect(at(250)).toBe(200);
11
11
  expect(at(350)).toBe(400);
12
12
  expect(at(700)).toBe(800);
13
13
  });
14
14
 
15
- test("velocity projects the release point — a flick upward reaches the next detent", () => {
15
+ test("velocity projects the release point — a flick upward reaches the next snap point", () => {
16
16
  expect(at(250, 1000)).toBe(400);
17
17
  });
18
18
 
19
- test("a flick downward from just above a detent falls through it", () => {
19
+ test("a flick downward from just above a snap point falls through it", () => {
20
20
  expect(at(420, -1500)).toBe(200);
21
21
  });
22
22
 
@@ -34,16 +34,18 @@ describe("selectSnapHeight", () => {
34
34
  expect(at(700, -4000, null)).toBe(200);
35
35
  });
36
36
 
37
- test("a projection past the highest detent still lands on it", () => {
37
+ test("a projection past the highest snap point still lands on it", () => {
38
38
  expect(at(700, 5000)).toBe(800);
39
39
  });
40
40
 
41
- test("exactly between two detents, the lower one wins", () => {
41
+ test("exactly between two snap points, the lower one wins", () => {
42
42
  expect(at(300)).toBe(200);
43
43
  });
44
44
 
45
45
  test("with nothing to snap to, the height is left where it is", () => {
46
- expect(selectSnapHeight({ height: 123, velocity: 0, detents: [], closedHeight: null, projection: 0.2 })).toBe(123);
46
+ expect(selectSnapHeight({ height: 123, velocity: 0, snapPoints: [], closedHeight: null, projection: 0.2 })).toBe(
47
+ 123
48
+ );
47
49
  });
48
50
 
49
51
  test("a NaN velocity is treated as rest rather than poisoning the target", () => {
@@ -3,8 +3,8 @@ export type SelectSnapHeightInput = {
3
3
  height: number;
4
4
  /** Release velocity in height space — upward positive, so pass `-velocityY`. */
5
5
  velocity: number;
6
- /** Ascending detents. */
7
- detents: readonly number[];
6
+ /** Ascending snap points. */
7
+ snapPoints: readonly number[];
8
8
  /** The closed height when closing is allowed from this gesture, else `null`. */
9
9
  closedHeight: number | null;
10
10
  /** Seconds of velocity to project — the `0.2` the previous engine used. */
@@ -12,7 +12,7 @@ export type SelectSnapHeightInput = {
12
12
  };
13
13
 
14
14
  /**
15
- * The detent a released drag settles at: the candidate nearest to where the
15
+ * The snap point a released drag settles at: the candidate nearest to where the
16
16
  * finger was heading, `height + projection · velocity`.
17
17
  *
18
18
  * Closing is a candidate only when `closedHeight` is given; the pan passes
@@ -31,8 +31,8 @@ export function selectSnapHeight(input: SelectSnapHeightInput): number {
31
31
  bestDistance = Math.abs(projected - input.closedHeight);
32
32
  }
33
33
 
34
- for (let index = 0; index < input.detents.length; index += 1) {
35
- const candidate = input.detents[index] as number;
34
+ for (let index = 0; index < input.snapPoints.length; index += 1) {
35
+ const candidate = input.snapPoints[index] as number;
36
36
  const distance = Math.abs(projected - candidate);
37
37
  if (distance < bestDistance) {
38
38
  best = candidate;
@@ -0,0 +1,42 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { SHEET_STATE } from "../sheet.types";
3
+ import { sheetState } from "./sheet-state";
4
+
5
+ const snapPoints = [200, 400];
6
+ const maxHeight = 800;
7
+
8
+ describe("sheetState", () => {
9
+ test("at or below the closed height the sheet is closed", () => {
10
+ expect(sheetState(0, 0, snapPoints, 0, maxHeight)).toBe(SHEET_STATE.CLOSED);
11
+ expect(sheetState(-10, -10, snapPoints, 0, maxHeight)).toBe(SHEET_STATE.CLOSED);
12
+ expect(sheetState(-32, -32, snapPoints, -32, maxHeight)).toBe(SHEET_STATE.CLOSED);
13
+ });
14
+
15
+ test("between closed and the highest snap point it is opened", () => {
16
+ expect(sheetState(200, 200, snapPoints, 0, maxHeight)).toBe(SHEET_STATE.OPENED);
17
+ expect(sheetState(300, 300, snapPoints, 0, maxHeight)).toBe(SHEET_STATE.OPENED);
18
+ });
19
+
20
+ test("at the highest snap point it is extended", () => {
21
+ expect(sheetState(400, 400, snapPoints, 0, maxHeight)).toBe(SHEET_STATE.EXTENDED);
22
+ });
23
+
24
+ test("a hair below the highest snap point still counts as extended, so a settled spring unlocks the list", () => {
25
+ expect(sheetState(399.7, 399.7, snapPoints, 0, maxHeight)).toBe(SHEET_STATE.EXTENDED);
26
+ });
27
+
28
+ test("above the highest snap point — over-drag or a keyboard lift — it is over-extended", () => {
29
+ expect(sheetState(450, 450, snapPoints, 0, maxHeight)).toBe(SHEET_STATE.OVER_EXTENDED);
30
+ expect(sheetState(400, 600, snapPoints, 0, maxHeight)).toBe(SHEET_STATE.OVER_EXTENDED);
31
+ });
32
+
33
+ test("filling the available height is fill, whatever the snap points say", () => {
34
+ expect(sheetState(800, 800, snapPoints, 0, maxHeight)).toBe(SHEET_STATE.FILL);
35
+ expect(sheetState(400, 800, snapPoints, 0, maxHeight)).toBe(SHEET_STATE.FILL);
36
+ expect(sheetState(800, 800, [200, 800], 0, maxHeight)).toBe(SHEET_STATE.FILL);
37
+ });
38
+
39
+ test("with no snap points anything open is extended, so a dynamic sheet still unlocks its list", () => {
40
+ expect(sheetState(300, 300, [], 0, maxHeight)).toBe(SHEET_STATE.EXTENDED);
41
+ });
42
+ });
@@ -4,19 +4,19 @@ import { SHEET_STATE, type SheetState } from "../sheet.types";
4
4
  const SETTLE_EPSILON = 0.5;
5
5
 
6
6
  /**
7
- * Where the sheet is relative to its detents.
7
+ * Where the sheet is relative to its snap points.
8
8
  *
9
9
  * `base` decides closed and extended — the keyboard-free height is what the
10
10
  * gesture and the scroll lock reason about. `height` decides fill and
11
11
  * over-extended, because a keyboard lift or an over-drag pushes the visible
12
- * sheet past the highest detent without moving `base`. With no detents (a
13
- * dynamic sheet whose content is the only detent, before it measures) any
12
+ * sheet past the highest snap point without moving `base`. With no snap points (a
13
+ * dynamic sheet whose content is the only snap point, before it measures) any
14
14
  * open height is extended, so the list under it is never locked.
15
15
  */
16
16
  export function sheetState(
17
17
  base: number,
18
18
  height: number,
19
- detents: readonly number[],
19
+ snapPoints: readonly number[],
20
20
  closedHeight: number,
21
21
  maxHeight: number
22
22
  ): SheetState {
@@ -24,8 +24,8 @@ export function sheetState(
24
24
  if (base <= closedHeight + SETTLE_EPSILON) return SHEET_STATE.CLOSED;
25
25
  if (height >= maxHeight - SETTLE_EPSILON) return SHEET_STATE.FILL;
26
26
 
27
- const count = detents.length;
28
- const highest = count > 0 ? (detents[count - 1] as number) : base;
27
+ const count = snapPoints.length;
28
+ const highest = count > 0 ? (snapPoints[count - 1] as number) : base;
29
29
  if (height > highest + SETTLE_EPSILON) return SHEET_STATE.OVER_EXTENDED;
30
30
  if (base >= highest - SETTLE_EPSILON) return SHEET_STATE.EXTENDED;
31
31
  return SHEET_STATE.OPENED;
@@ -12,31 +12,31 @@ The two pans that drive a sheet, and the haptic hooks they fire.
12
12
  ## The handlers
13
13
 
14
14
  `onStart` cancels the animation that owns `base`, records the source (handle
15
- or content), where the drag began and which detent was under it.
15
+ or content), where the drag began and which snap point was under it.
16
16
 
17
17
  `onChange` turns the finger's travel into a height — `startBase −
18
18
  translationY`, less the scrollable's offset for a content pan once BSHEET-4
19
19
  wires one — rubber-banded by `resistOverDrag` past the lowest and highest
20
- detent and never above the container. The lowest is the closed height when
21
- `enablePanDownToClose`, else the first detent. `crossedDetent` decides the
22
- detent haptic; entering the rubber band fires the over-drag haptic once.
20
+ snap point and never above the container. The lowest is the closed height when
21
+ `enablePanDownToClose`, else the first snap point. `crossedSnapPoint` decides the
22
+ snap point haptic; entering the rubber band fires the over-drag haptic once.
23
23
 
24
24
  `onFinalize`, not `onEnd`: a gesture that fails after activating never fires
25
- `onEnd`, and the sheet would be stranded between detents. `selectSnapHeight`
25
+ `onEnd`, and the sheet would be stranded between snap points. `selectSnapHeight`
26
26
  projects the release velocity a fifth of a second ahead and picks the nearest
27
27
  candidate, closing only when the pan may close, and `animateTo` gets half the
28
28
  velocity in height space — upward positive, so `−velocityY`.
29
29
 
30
30
  A content pan over a scrollable is shared with the list. When `listOwnsRelease`
31
- says the release is the list's — the sheet at its highest detent, the list
31
+ says the release is the list's — the sheet at its highest snap point, the list
32
32
  scrolled — the sheet does not snap, but it **still settles**: the finger moved
33
33
  `base` by hand, so no animation ran and nothing else would write
34
- `currentIndex`. `restingDetent` names the detent under `base` and `settleAt`
34
+ `currentIndex`. `restingSnapPoint` names the snap point under `base` and `settleAt`
35
35
  does the bookkeeping of a finished animation without the motion —
36
36
  `currentIndex`, then `onSettle`, so the handle's accessibility value and
37
37
  `onIndexChange` catch up. A `return` there once left the index at the previous
38
- detent (or at `-1`) under a sheet sitting at 90%, and the next overlay tap did
39
- nothing because `close` read the sheet as already closed. Between detents,
38
+ snap point (or at `-1`) under a sheet sitting at 90%, and the next overlay tap did
39
+ nothing because `close` read the sheet as already closed. Between snap points,
40
40
  which the ownership test should rule out, the sheet snaps to the nearest with
41
41
  no velocity and never closes: the release velocity is the list's momentum.
42
42
 
@@ -51,14 +51,14 @@ view. Scrollables (BSHEET-4) wrap their native gesture in
51
51
 
52
52
  ## Per-gesture memory
53
53
 
54
- Where the drag began, the detent last under it and whether it is over-dragging
54
+ Where the drag began, the snap point last under it and whether it is over-dragging
55
55
  live in shared values rather than closure variables. Each worklet gets its own
56
56
  copy of a captured `let`, so a write in `onStart` would never be seen by
57
57
  `onChange`.
58
58
 
59
59
  ## Haptics
60
60
 
61
- The package imports nothing from a haptics library. `onDetentHaptic`,
61
+ The package imports nothing from a haptics library. `onSnapPointHaptic`,
62
62
  `onCloseHaptic` and `onOverDragHaptic` are worklets the consumer passes —
63
63
  `Presets.System.selection` from pulsar is one already — and they run inside the
64
64
  gesture handler, in the frame the crossing happened.
@@ -8,11 +8,11 @@ import type { GestureType } from "react-native-gesture-handler";
8
8
  export type HapticWorklet = () => void;
9
9
 
10
10
  export type SheetHaptics = {
11
- /** A drag crossed a detent. */
12
- onDetentHaptic?: HapticWorklet;
11
+ /** A drag crossed a snap point. */
12
+ onSnapPointHaptic?: HapticWorklet;
13
13
  /** A drag let go on a close. */
14
14
  onCloseHaptic?: HapticWorklet;
15
- /** A drag left the detent range and the rubber band took over. */
15
+ /** A drag left the snap point range and the rubber band took over. */
16
16
  onOverDragHaptic?: HapticWorklet;
17
17
  };
18
18
 
@@ -44,7 +44,7 @@ type ScrollLockProps = Pick<
44
44
  >;
45
45
 
46
46
  export type ScrollLock = {
47
- /** `true` while the sheet is below its highest detent: the list is held at `scrollLockedAt`. */
47
+ /** `true` while the sheet is below its highest snap point: the list is held at `scrollLockedAt`. */
48
48
  locked: SharedValue<boolean>;
49
49
  /** For the list's `onScroll`. Tracks the offset into `scrollOffsetY` and enforces the lock. */
50
50
  scrollHandler: ReturnType<typeof useAnimatedScrollHandler>;
@@ -55,14 +55,14 @@ export type ScrollLock = {
55
55
  /**
56
56
  * The scroll lock: the UI-thread half of a scrollable body.
57
57
  *
58
- * Below the highest detent a drag on the list must move the sheet, not the
58
+ * Below the highest snap point a drag on the list must move the sheet, not the
59
59
  * rows. The content pan already does the moving — the native scroll and the
60
60
  * pan are simultaneous — so this hook's job is to make sure the list does not
61
61
  * *also* move: every scroll event while locked is answered with a `scrollTo`
62
62
  * back to where the lock engaged, and the offset the pan reads is pinned
63
63
  * there too, so the pan sees the list consume nothing.
64
64
  *
65
- * The lock engages when the sheet leaves its highest detent by any path — a
65
+ * The lock engages when the sheet leaves its highest snap point by any path — a
66
66
  * drag, a `snapToIndex`, a keyboard — and takes the offset of that moment as
67
67
  * its target. A list scrolled a hundred pixels, then collapsed by the handle,
68
68
  * holds those hundred pixels rather than jumping to the top; when the sheet