@oxyhq/bloom 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/docs/layout.mdx +21 -35
  2. package/lib/commonjs/fab/Fab.js +4 -55
  3. package/lib/commonjs/fab/Fab.js.map +1 -1
  4. package/lib/commonjs/fab/Fab.web.js +1 -29
  5. package/lib/commonjs/fab/Fab.web.js.map +1 -1
  6. package/lib/commonjs/layout/bottom-edge.js +47 -92
  7. package/lib/commonjs/layout/bottom-edge.js.map +1 -1
  8. package/lib/commonjs/layout/index.js +0 -6
  9. package/lib/commonjs/layout/index.js.map +1 -1
  10. package/lib/commonjs/tab-bar/TabBarBase.js +6 -22
  11. package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
  12. package/lib/module/fab/Fab.js +6 -57
  13. package/lib/module/fab/Fab.js.map +1 -1
  14. package/lib/module/fab/Fab.web.js +2 -30
  15. package/lib/module/fab/Fab.web.js.map +1 -1
  16. package/lib/module/layout/bottom-edge.js +47 -91
  17. package/lib/module/layout/bottom-edge.js.map +1 -1
  18. package/lib/module/layout/index.js +1 -1
  19. package/lib/module/layout/index.js.map +1 -1
  20. package/lib/module/tab-bar/TabBarBase.js +8 -24
  21. package/lib/module/tab-bar/TabBarBase.js.map +1 -1
  22. package/lib/typescript/commonjs/fab/Fab.d.ts.map +1 -1
  23. package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/layout/bottom-edge.d.ts +31 -57
  25. package/lib/typescript/commonjs/layout/bottom-edge.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/layout/index.d.ts +1 -1
  27. package/lib/typescript/commonjs/layout/index.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
  29. package/lib/typescript/module/fab/Fab.d.ts.map +1 -1
  30. package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
  31. package/lib/typescript/module/layout/bottom-edge.d.ts +31 -57
  32. package/lib/typescript/module/layout/bottom-edge.d.ts.map +1 -1
  33. package/lib/typescript/module/layout/index.d.ts +1 -1
  34. package/lib/typescript/module/layout/index.d.ts.map +1 -1
  35. package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/fab/Fab.tsx +4 -57
  38. package/src/fab/Fab.web.tsx +2 -31
  39. package/src/layout/bottom-edge.tsx +54 -108
  40. package/src/layout/index.ts +1 -6
  41. package/src/tab-bar/TabBarBase.tsx +6 -28
package/src/fab/Fab.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { memo, useEffect, useMemo, useRef, type ComponentType } from 'react';
1
+ import React, { memo, useMemo, type ComponentType } from 'react';
2
2
  import {
3
3
  Animated,
4
4
  Pressable,
@@ -11,7 +11,7 @@ import {
11
11
  } from 'react-native';
12
12
  import { styled } from 'react-native-css';
13
13
 
14
- import { useBottomEdgeCollapsed, useBottomEdgeInset } from '../layout/bottom-edge';
14
+ import { useBottomEdgeInset } from '../layout/bottom-edge';
15
15
  import { useTheme } from '../theme/use-theme';
16
16
  import { animation, borderRadius } from '../styles/tokens';
17
17
  import { bloomShadowStyle } from '../design-tokens/shadows';
@@ -57,23 +57,6 @@ function resolveSize(size: FabSize | number): ResolvedSize {
57
57
  return SIZE_CONFIG[size];
58
58
  }
59
59
 
60
- /**
61
- * How long the FAB takes to leave when the bottom edge collapses.
62
- *
63
- * A DURATION, not a spring matched to the tab bar's — because the FAB is not
64
- * trying to stay level with the bar any more. It tried: an earlier version rode
65
- * the bar down by the 14px it shrank, and on a device the two visibly did not
66
- * move together. The bar animates on the UI thread while this component learns
67
- * about the collapse through `runOnJS` plus two React renders, so it starts one
68
- * to three frames late — worst exactly while scrolling, which is the only time
69
- * it happens. No spring config fixes a variable start delay.
70
- *
71
- * Fading is what makes that latency invisible: there is no spatial relationship
72
- * left to violate, so nothing betrays the few frames. Slightly quicker than the
73
- * bar's 380ms minimize, so the FAB is gone before the eye goes looking for it.
74
- */
75
- const COLLAPSE_FADE_MS = 200;
76
-
77
60
  const DEFAULT_OFFSET = 16;
78
61
  const DEFAULT_Z_INDEX = 50;
79
62
  const HIT_SLOP = { top: 8, bottom: 8, left: 8, right: 8 } as const;
@@ -90,10 +73,6 @@ const HIT_SLOP = { top: 8, bottom: 8, left: 8, right: 8 } as const;
90
73
  * lands above a floating tab bar instead of behind it. A z-index cannot fix that
91
74
  * pairing: the bar's host is the last sibling of the app shell and paints over
92
75
  * every descendant, so the FAB has to be somewhere else, not merely on top.
93
- *
94
- * It is the RESERVED inset, so this stays a static layout value — it does not
95
- * shrink when the bar collapses. The FAB fades out instead of chasing the bar
96
- * down; see `COLLAPSE_FADE_MS` for why chasing it does not work.
97
76
  */
98
77
  function placementStyle(
99
78
  placement: FabPlacement,
@@ -139,13 +118,10 @@ function placementStyle(
139
118
  */
140
119
  type FabPressableProps = Pick<
141
120
  PressableProps,
142
- | 'accessibilityElementsHidden'
143
121
  | 'accessibilityHint'
144
122
  | 'accessibilityLabel'
145
123
  | 'accessibilityRole'
146
124
  | 'accessibilityState'
147
- | 'importantForAccessibility'
148
- | 'pointerEvents'
149
125
  | 'children'
150
126
  | 'className'
151
127
  | 'disabled'
@@ -182,35 +158,11 @@ const FabComponent: React.FC<FabProps> = ({
182
158
  zIndex = DEFAULT_Z_INDEX,
183
159
  }) => {
184
160
  const bottomEdgeInset = useBottomEdgeInset();
185
- // The FAB belongs to the chrome: when the chrome retracts, so does it.
186
- const bottomEdgeCollapsed = useBottomEdgeCollapsed();
187
161
  const theme = useTheme();
188
162
  const sizeConfig = useMemo(() => resolveSize(size), [size]);
189
163
  const isExtended = label != null && label.length > 0;
190
164
  const content = icon ?? children;
191
165
 
192
- // Only a FAB anchored to the BOTTOM edge is affected by the bottom edge
193
- // collapsing; a top-placed or consumer-placed one is not.
194
- const hidesOnCollapse = placement === 'bottom-right' || placement === 'bottom-left';
195
- const hidden = hidesOnCollapse && bottomEdgeCollapsed;
196
- const fade = useRef(new Animated.Value(1)).current;
197
-
198
- // An effect because RN's Animated has no declarative form — starting an
199
- // animation is imperative by construction. `opacity` is native-drivable, so
200
- // once started this never touches the JS thread again, which matters: it runs
201
- // while the user is mid-scroll.
202
- // The disabled dim rides on the SAME value rather than a second static
203
- // `opacity` in the style array: two opacity entries would mean the later one
204
- // silently wins, so a disabled FAB would either not dim or not fade.
205
- const restingOpacity = disabled ? 0.5 : 1;
206
- useEffect(() => {
207
- Animated.timing(fade, {
208
- toValue: hidden ? 0 : restingOpacity,
209
- duration: COLLAPSE_FADE_MS,
210
- useNativeDriver: true,
211
- }).start();
212
- }, [hidden, restingOpacity, fade]);
213
-
214
166
  const { scaleAnim, onPressIn, onPressOut } = usePressAnimation(
215
167
  disabled ? undefined : animation.pressScale,
216
168
  );
@@ -277,7 +229,7 @@ const FabComponent: React.FC<FabProps> = ({
277
229
  placementStyle(placement, offset, bottomEdgeInset),
278
230
  { zIndex },
279
231
  containerStyle,
280
- { opacity: fade },
232
+ disabled && { opacity: 0.5 },
281
233
  pressed && !disabled && { backgroundColor: pressedBackground },
282
234
  // Before the caller's `style`, so `style` keeps winning the whole array
283
235
  // the way it always has (and the way the web fork spreads it last). The
@@ -287,12 +239,7 @@ const FabComponent: React.FC<FabProps> = ({
287
239
  { transform: [{ scale: scaleAnim }] },
288
240
  style,
289
241
  ]}
290
- // A FAB that has faded out must not be pressable or reachable — an
291
- // invisible target that still takes a tap is worse than a visible one.
292
- pointerEvents={hidden ? 'none' : 'auto'}
293
- accessibilityElementsHidden={hidden}
294
- importantForAccessibility={hidden ? 'no-hide-descendants' : 'auto'}
295
- onPress={disabled || hidden ? undefined : onPress}
242
+ onPress={disabled ? undefined : onPress}
296
243
  onPressIn={handlePressIn}
297
244
  onPressOut={handlePressOut}
298
245
  disabled={disabled}
@@ -7,7 +7,7 @@ import React, {
7
7
  type MouseEvent,
8
8
  } from 'react';
9
9
 
10
- import { useBottomEdgeCollapsed, useBottomEdgeInset } from '../layout/bottom-edge';
10
+ import { useBottomEdgeInset } from '../layout/bottom-edge';
11
11
  import { useTheme } from '../theme/use-theme';
12
12
  import { animation, borderRadius } from '../styles/tokens';
13
13
  import { pressedSurface } from '../theme/press-colors';
@@ -83,11 +83,8 @@ const BLOOM_FAB_CSS = interactiveWebCss({
83
83
  box-shadow: var(--bloom-fab-shadow);
84
84
  font-family: inherit;
85
85
  `,
86
- // `opacity` carries two things: the disabled dim and the collapse fade. 200ms
87
- // is the collapse's number — the dim is imperceptibly slower for it, and one
88
- // property cannot hold two durations.
89
86
  transition:
90
- 'opacity 200ms ease, transform 120ms ease, box-shadow 160ms ease, background-color 120ms ease',
87
+ 'opacity 120ms ease, transform 120ms ease, box-shadow 160ms ease, background-color 120ms ease',
91
88
  // A FAB lifts on hover rather than dimming: it floats over the content, so a
92
89
  // deeper shadow is the affordance an opacity dip cannot express.
93
90
  hover: { declarations: 'box-shadow: var(--bloom-fab-shadow-hover);' },
@@ -132,16 +129,6 @@ const BLOOM_FAB_CSS = interactiveWebCss({
132
129
  * behind it. A z-index cannot fix that pairing: the bar's host is the last
133
130
  * sibling of the app shell and paints over every descendant, so the FAB has to be
134
131
  * somewhere else, not merely on top.
135
- *
136
- * It is the LIVE inset here, and `bottom` is transitioned — where the native fork
137
- * follows a collapsing bar with an Animated TRANSFORM instead. The forks differ
138
- * because the constraint does. Native must stay off the JS thread mid-scroll, so
139
- * the follow has to be a native-driver transform. On web a transform cannot carry
140
- * it: `:active` already sets `transform: scale(...)` from the shared
141
- * `interactive-web-css` rule, and an inline transform would beat that stylesheet
142
- * rule and eat the press scale — and one property cannot hold the press's 120ms
143
- * and the collapse's 380ms at once. `bottom` is a separate property with its own
144
- * duration, and it moves twice per scroll gesture rather than per frame.
145
132
  */
146
133
  function placementStyle(
147
134
  placement: FabPlacement,
@@ -211,13 +198,6 @@ const FabWebComponent: React.FC<FabProps> = ({
211
198
  }) => {
212
199
  useInteractiveWebCss(STYLE_ID, BLOOM_FAB_CSS);
213
200
  const bottomEdgeInset = useBottomEdgeInset();
214
- // The FAB belongs to the chrome: when the chrome retracts, so does it. Only a
215
- // BOTTOM-anchored FAB is affected by the bottom edge collapsing.
216
- const bottomEdgeCollapsed = useBottomEdgeCollapsed();
217
- // A faded-out FAB must not be tabbable, clickable or announced — an invisible
218
- // target that still takes a click is worse than a visible one.
219
- const hidden =
220
- bottomEdgeCollapsed && (placement === 'bottom-right' || placement === 'bottom-left');
221
201
  const theme = useTheme();
222
202
  const reactId = useId();
223
203
  const resolvedId = id ?? `bloom-fab-${reactId}`;
@@ -254,13 +234,6 @@ const FabWebComponent: React.FC<FabProps> = ({
254
234
  ['--bloom-fab-press-scale' as string]: animation.pressScale,
255
235
  ...placementStyle(placement, offset, bottomEdgeInset),
256
236
  };
257
- // Only while HIDDEN, so the `:disabled { opacity }` rule still owns the
258
- // dim the rest of the time — an inline opacity would beat that stylesheet
259
- // rule unconditionally and a disabled FAB would stop dimming.
260
- if (hidden) {
261
- base.opacity = 0;
262
- base.pointerEvents = 'none';
263
- }
264
237
  if (isExtended) {
265
238
  const pad = sizeConfig.diameter <= 44 ? 14 : 20;
266
239
  base.paddingLeft = pad;
@@ -314,8 +287,6 @@ const FabWebComponent: React.FC<FabProps> = ({
314
287
  onClick={handleClick}
315
288
  disabled={disabled}
316
289
  aria-disabled={disabled || undefined}
317
- aria-hidden={hidden || undefined}
318
- tabIndex={hidden ? -1 : undefined}
319
290
  aria-label={ariaLabel}
320
291
  title={title ?? accessibilityHint}
321
292
  data-testid={testID}
@@ -27,32 +27,27 @@
27
27
  * edge, so two surfaces at that edge overlap rather than stack; the tallest is
28
28
  * what a new surface has to clear. Summing would strand it at twice the height.
29
29
  *
30
- * ## A size and a state, because they travel differently
31
- *
32
- * A surface that collapses — the tab bar minimizes 58 -> 44 on scroll publishes
33
- * two things, and only one of them is a measurement:
34
- *
35
- * - RESERVED (`useBottomEdgeInset`) never shrinks while the claimant is
36
- * mounted. It is what must be kept permanently free: a list's bottom
37
- * padding, a toast stack's offset. Shrinking it on collapse would jitter a
38
- * list's content size on every scroll and jump a toast 14px mid-display,
39
- * because the bar re-expands the instant the user scrolls back up.
40
- * - COLLAPSED (`useBottomEdgeCollapsed`) is a boolean, deliberately, and this
41
- * is the part that was learned the hard way. It first shipped as a live
42
- * PIXEL height so a FAB could ride down with the bar — and it felt broken on
43
- * a device. The bar animates on the UI thread; a React consumer learns about
44
- * the change through `runOnJS` plus two render passes, so its motion STARTS
45
- * one to three frames late, worst exactly while scrolling because that is
46
- * when the JS thread is busiest. No spring config fixes a variable start
47
- * delay.
48
- *
49
- * Two things moving together betray that lag; a state change does not. A reader
50
- * that FADES on this boolean has no spatial relationship to violate, so the same
51
- * few frames of latency are imperceptible. That is why the channel is a state and
52
- * not a geometry: it is the shape of signal that survives the trip to the JS
53
- * thread. Anything that genuinely must track the collapse per-frame has to read
54
- * the tab bar's own shared value on the UI thread instead — React state is the
55
- * wrong transport for it, at any width.
30
+ * ## What does NOT belong here: the collapse
31
+ *
32
+ * The tab bar minimizes 58 -> 44 on scroll, and this registry twice tried to
33
+ * publish that so a FAB could react first as a live pixel height (1.11.0), then
34
+ * as a boolean (1.12.0). Both felt wrong on a device, for the same reason, and
35
+ * the reason is structural rather than a matter of tuning:
36
+ *
37
+ * the bar animates on the UI thread, while anything reading it from here
38
+ * arrives via `runOnJS` plus two React render passes.
39
+ *
40
+ * So a consumer's reaction STARTS one to three frames after the bar's, and the
41
+ * delay is variable worst exactly while scrolling, because that is when the JS
42
+ * thread is busiest. Fading instead of moving does not hide it either: the eye
43
+ * still sees two pieces of chrome change at different moments.
44
+ *
45
+ * A collapse is MOTION, and motion that must stay in lock-step with a
46
+ * reanimated animation has to travel as a shared value on the UI thread. React
47
+ * state is the wrong transport at any width, so it is not offered here at all.
48
+ * The app owns that signal — Mention drives its header, its bar and its FAB from
49
+ * one `SharedValue` through `useAnimatedStyle` and this registry stays what it
50
+ * is good at: GEOMETRY. Where things are, not when they move.
56
51
  *
57
52
  * ## Why an external store
58
53
  *
@@ -72,50 +67,33 @@ import {
72
67
  type PropsWithChildren,
73
68
  } from 'react';
74
69
 
75
- interface Claim {
76
- /** What the surface keeps permanently free, collapsed or not. */
77
- reserved: number;
78
- /** Whether it is currently in its collapsed state. */
79
- collapsed: boolean;
80
- }
81
-
82
70
  interface BottomEdgeStore {
83
71
  subscribe: (onChange: () => void) => () => void;
84
72
  /**
85
- * The cached totals. Each returns the SAME number until a claim actually
86
- * changes it — `useSyncExternalStore` re-renders forever if the snapshot is
87
- * recomputed per call, and a reader of one channel must not re-render when
88
- * only the other moved.
73
+ * The cached total. Returns the SAME number until a claim actually changes it
74
+ * — `useSyncExternalStore` re-renders forever if the snapshot is recomputed
75
+ * per call.
89
76
  */
90
- getReserved: () => number;
91
- getCollapsed: () => boolean;
92
- claim: (id: string, reserved: number, collapsed: boolean) => void;
77
+ getInset: () => number;
78
+ claim: (id: string, height: number) => void;
93
79
  release: (id: string) => void;
94
80
  }
95
81
 
96
82
  function createBottomEdgeStore(): BottomEdgeStore {
97
- const claims = new Map<string, Claim>();
83
+ const claims = new Map<string, number>();
98
84
  const listeners = new Set<() => void>();
99
- let reserved = 0;
100
- let collapsed = false;
85
+ let inset = 0;
101
86
 
102
87
  const recompute = () => {
103
- let nextReserved = 0;
104
- // ANY claimant collapsing collapses the edge. With the one floating bar this
105
- // is written for the two readings coincide; where they would not, "something
106
- // at this edge just retracted" is still the signal a reader wants, and it is
107
- // the safe direction — a FAB that fades when it did not strictly have to
108
- // beats one that stays put over a bar that left.
109
- let nextCollapsed = false;
110
- for (const claim of claims.values()) {
111
- if (claim.reserved > nextReserved) nextReserved = claim.reserved;
112
- if (claim.collapsed) nextCollapsed = true;
88
+ let next = 0;
89
+ for (const height of claims.values()) {
90
+ if (height > next) next = height;
113
91
  }
114
- // Bail before notifying: a re-registration at an unchanged footprint (every
115
- // render of a claimant that did not move) must not re-render every reader.
116
- if (nextReserved === reserved && nextCollapsed === collapsed) return;
117
- reserved = nextReserved;
118
- collapsed = nextCollapsed;
92
+ // Bail before notifying: a re-registration at an unchanged height (every
93
+ // render of a claimant whose footprint did not move) must not re-render
94
+ // every reader.
95
+ if (next === inset) return;
96
+ inset = next;
119
97
  for (const listener of listeners) listener();
120
98
  };
121
99
 
@@ -126,12 +104,10 @@ function createBottomEdgeStore(): BottomEdgeStore {
126
104
  listeners.delete(onChange);
127
105
  };
128
106
  },
129
- getReserved: () => reserved,
130
- getCollapsed: () => collapsed,
131
- claim(id, nextReserved, nextCollapsed) {
132
- const existing = claims.get(id);
133
- if (existing?.reserved === nextReserved && existing.collapsed === nextCollapsed) return;
134
- claims.set(id, { reserved: nextReserved, collapsed: nextCollapsed });
107
+ getInset: () => inset,
108
+ claim(id, height) {
109
+ if (claims.get(id) === height) return;
110
+ claims.set(id, height);
135
111
  recompute();
136
112
  },
137
113
  release(id) {
@@ -160,18 +136,15 @@ BottomEdgeProvider.displayName = 'BottomEdgeProvider';
160
136
  // would resubscribe `useSyncExternalStore` on every render.
161
137
  const NO_SUBSCRIPTION = () => () => {};
162
138
  const NO_INSET = () => 0;
163
- const NO_COLLAPSE = () => false;
164
139
 
165
140
  /**
166
- * How much of the bottom edge is permanently RESERVED, in px.
141
+ * How much of the bottom edge is already occupied, in px.
167
142
  *
168
- * Never shrinks while its claimant is mounted, so it is the number for anything
169
- * that must not move as the user scrolls — a list's bottom padding, a toast
170
- * stack's offset:
143
+ * Add it to whatever offset the surface would otherwise use:
171
144
  *
172
145
  * ```tsx
173
- * const reserved = useBottomEdgeInset();
174
- * <FlatList contentContainerStyle={{ paddingBottom: reserved + 12 }} />
146
+ * const occupied = useBottomEdgeInset();
147
+ * <View style={{ position: 'absolute', bottom: windowEdgeGap(insets.bottom) + occupied }} />
175
148
  * ```
176
149
  *
177
150
  * `0` outside a provider, and `0` on the first commit even inside one — a claim
@@ -183,56 +156,29 @@ export function useBottomEdgeInset(): number {
183
156
  const store = useContext(BottomEdgeContext);
184
157
  return useSyncExternalStore(
185
158
  store?.subscribe ?? NO_SUBSCRIPTION,
186
- store?.getReserved ?? NO_INSET,
187
- store?.getReserved ?? NO_INSET,
188
- );
189
- }
190
-
191
- /**
192
- * Whether the surface owning the bottom edge is currently COLLAPSED — the tab
193
- * bar minimized on scroll.
194
- *
195
- * A boolean rather than a live height, on purpose: see the note at the top of
196
- * this file. A reader should FADE or otherwise change state on it, never try to
197
- * stay geometrically locked to the collapsing surface — that lock is what a
198
- * React-state channel cannot deliver, because the surface animates on the UI
199
- * thread and this arrives one to three frames later.
200
- *
201
- * ```tsx
202
- * const collapsed = useBottomEdgeCollapsed();
203
- * // fade out; do not chase the bar's new top edge
204
- * ```
205
- *
206
- * `false` outside a provider.
207
- */
208
- export function useBottomEdgeCollapsed(): boolean {
209
- const store = useContext(BottomEdgeContext);
210
- return useSyncExternalStore(
211
- store?.subscribe ?? NO_SUBSCRIPTION,
212
- store?.getCollapsed ?? NO_COLLAPSE,
213
- store?.getCollapsed ?? NO_COLLAPSE,
159
+ store?.getInset ?? NO_INSET,
160
+ store?.getInset ?? NO_INSET,
214
161
  );
215
162
  }
216
163
 
217
164
  /**
218
- * Claim the bottom edge for as long as the caller is mounted.
165
+ * Claim `height` px of the bottom edge for as long as the caller is mounted.
219
166
  *
220
167
  * The claimant owns its own placement — claiming does not move it. It declares
221
- * the space it occupies so that everything reading the edge stays off it. Pass
222
- * the FULL footprint (the surface's height plus the gap it holds off the window
223
- * edge), which is the same number the surface positions itself with, and keep it
224
- * at the surface's FULL size even while collapsed: `reserved` is what must stay
225
- * permanently free, and the collapse is reported separately by `collapsed`.
168
+ * the space it occupies so that everything reading `useBottomEdgeInset()` stays
169
+ * off it. Pass the FULL footprint (the surface's height plus the gap it holds
170
+ * off the window edge), which is the same number the surface positions itself
171
+ * with.
226
172
  *
227
173
  * A no-op outside a provider, so a surface stays usable standalone.
228
174
  */
229
- export function useClaimBottomEdge(reserved: number, collapsed = false): void {
175
+ export function useClaimBottomEdge(height: number): void {
230
176
  const store = useContext(BottomEdgeContext);
231
177
  const id = useId();
232
178
 
233
179
  useEffect(() => {
234
180
  if (!store) return;
235
- store.claim(id, reserved, collapsed);
181
+ store.claim(id, height);
236
182
  return () => store.release(id);
237
- }, [store, id, reserved, collapsed]);
183
+ }, [store, id, height]);
238
184
  }
@@ -5,9 +5,4 @@
5
5
  * parked there". A surface that floats at the bottom needs both.
6
6
  */
7
7
  export { EDGE_GAP, windowEdgeGap } from './edge';
8
- export {
9
- BottomEdgeProvider,
10
- useBottomEdgeInset,
11
- useBottomEdgeCollapsed,
12
- useClaimBottomEdge,
13
- } from './bottom-edge';
8
+ export { BottomEdgeProvider, useBottomEdgeInset, useClaimBottomEdge } from './bottom-edge';
@@ -20,7 +20,6 @@ import {
20
20
  useContext,
21
21
  useEffect,
22
22
  useMemo,
23
- useState,
24
23
  type ComponentType,
25
24
  } from 'react';
26
25
  import { Pressable, StyleSheet, useWindowDimensions, View, type ViewStyle } from 'react-native';
@@ -30,7 +29,6 @@ import Animated, {
30
29
  interpolate,
31
30
  interpolateColor,
32
31
  runOnJS,
33
- useAnimatedReaction,
34
32
  useAnimatedStyle,
35
33
  useSharedValue,
36
34
  withSpring,
@@ -424,33 +422,13 @@ function TabBarBody({
424
422
  // its own layout can never drift from where the bar actually sits.
425
423
  const bottomOffset = windowEdgeGap(insets.bottom);
426
424
 
427
- // Whether the bar has settled into its minimized size, as REACT state.
428
- //
429
- // Reacting on `target` rather than on `progress` is what keeps this cheap:
430
- // `target` is the binary 0/1 the minimize spring is heading for, so this fires
431
- // twice per scroll gesture instead of once per frame. Crossing `progress > 0.5`
432
- // would work too and would cost the same, but it would report the change
433
- // halfway through the animation rather than when it was decided.
434
- const [isMinimized, setIsMinimized] = useState(false);
435
- useAnimatedReaction(
436
- () => minimized.target.value,
437
- (target, previous) => {
438
- if (target !== previous) runOnJS(setIsMinimized)(target === 1);
439
- },
440
- [minimized],
441
- );
442
-
443
425
  // Publish what the bar occupies so anything else at this edge stacks above it
444
- // rather than behind it. Same numbers `useTabBarFootprint` reports, derived
445
- // from the same geometry, so a consumer reading either can never disagree with
446
- // where the bar actually sits.
447
- //
448
- // RESERVED stays the EXPANDED height even while minimized: the bar re-expands
449
- // the moment the user scrolls back up, so anything that permanently reserves
450
- // space (a list's bottom padding, a toast) must keep room for the full pill or
451
- // it would jitter on every scroll. The collapse travels as its own STATE — see
452
- // `layout/bottom-edge` for why it is a boolean and not the live height.
453
- useClaimBottomEdge(bottomOffset + EXPANDED_HEIGHT, isMinimized);
426
+ // rather than behind it. Same number `useTabBarFootprint` reports, derived from
427
+ // the same two values, so a consumer reading either can never disagree with
428
+ // where the bar actually sits. The EXPANDED height on purpose: the bar
429
+ // minimizes on scroll and re-expands, so claiming the minimized height would
430
+ // drop a FAB onto the pill the moment the user scrolled back up.
431
+ useClaimBottomEdge(bottomOffset + EXPANDED_HEIGHT);
454
432
 
455
433
  // How centring and the animated inset compose: centring is STATIC and belongs
456
434
  // to the wrap, the inset stays ANIMATED on the pill inside it. The wrap is