@lodev09/react-native-true-sheet 3.11.6 → 3.11.7

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.
@@ -255,6 +255,42 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
255
255
  (header ? headerHeight : 0) + (footer ? footerHeight : 0) + peekContentHeight ||
256
256
  DEFAULT_PEEK_HEIGHT;
257
257
 
258
+ // Below the last detent a vertical touch pan moves the sheet, not the
259
+ // content — `[data-vaul-scroll-locked]` disables vertical touch panning on
260
+ // the scroll container and everything inside it (see vaul/style.css).
261
+ const isScrollLocked =
262
+ validDetents.length > 0 && activeSnapPoint !== validDetents[validDetents.length - 1];
263
+
264
+ // Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
265
+ // Track it here so the form sheet can size its card to fit content,
266
+ // clamped between a minimum ratio of the viewport and a maximum derived
267
+ // from `detachedOffset` (the breathing room left at top + bottom of the
268
+ // floating card).
269
+ const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
270
+
271
+ const effectiveMaxContentHeight = useMemo<number | undefined>(() => {
272
+ if (maxContentHeight !== undefined) return maxContentHeight;
273
+ if (!isFormSheet) return undefined;
274
+ const min = windowHeight * DEFAULT_FORM_SHEET_HEIGHT_RATIO;
275
+ const max = Math.max(min, windowHeight - 2 * detachedOffset);
276
+ if (measuredContentHeight <= 0) return min;
277
+ return Math.max(min, Math.min(measuredContentHeight, max));
278
+ }, [maxContentHeight, isFormSheet, windowHeight, detachedOffset, measuredContentHeight]);
279
+
280
+ // Center the form sheet using the actual visible drawer height. Vaul
281
+ // auto-sizes to content (capped by `maxContentHeight`), so when content is
282
+ // shorter than `effectiveMaxContentHeight`'s min-clamped floor, using that
283
+ // for the offset would push a small sheet below the viewport center.
284
+ const effectiveDetachedOffset = useMemo(() => {
285
+ if (!isFormSheet) return detachedOffset;
286
+ const max = Math.max(0, windowHeight - 2 * detachedOffset);
287
+ const visibleHeight =
288
+ measuredContentHeight > 0
289
+ ? Math.min(measuredContentHeight, max)
290
+ : (effectiveMaxContentHeight ?? 0);
291
+ return Math.max(0, (windowHeight - visibleHeight) / 2);
292
+ }, [isFormSheet, windowHeight, detachedOffset, measuredContentHeight, effectiveMaxContentHeight]);
293
+
258
294
  // Present/dismiss events. The sheet settles via a CSS `transform` transition
259
295
  // on either the drawer (snap-points on autopresent) or the wrapper (whole-
260
296
  // card slide on reopen/dismiss). `Animation.finished` from the Web Animations
@@ -285,54 +321,91 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
285
321
  activeSnapPointRef.current = activeSnapPoint;
286
322
  });
287
323
 
288
- const computeDetentInfo = useCallback((): DetentInfoEventPayload => {
289
- const snap = activeSnapPointRef.current;
290
- const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
291
- const position = drawerContentRef.current?.getBoundingClientRect().top ?? 0;
292
- const detent = typeof snap === 'number' ? snap : 0;
293
- return { index, position, detent };
324
+ // Detent geometry — target top-Y (`positions`) and height ratio (`values`)
325
+ // per detent. Mirrors vaul's snap-offset math exactly (same effective
326
+ // height, ceiling, and 'auto'/'peek' resolution) so computed targets match
327
+ // where the drawer actually settles. Numeric detent d → top-Y =
328
+ // (1 - d) * effectiveH. 'auto' resolves to the auto-size wrapper's measured
329
+ // height (tracked via vaul's `onContentHeightChange`) — same signal vaul
330
+ // uses to compute its snap offset. Inputs live in a render-synced ref so
331
+ // the compute callbacks stay referentially stable for the event effects.
332
+ const geometryInputsRef = useRef({
333
+ effectiveDetached,
334
+ effectiveDetachedOffset,
335
+ effectiveMaxContentHeight,
336
+ peekHeight,
337
+ measuredContentHeight,
338
+ });
339
+ geometryInputsRef.current = {
340
+ effectiveDetached,
341
+ effectiveDetachedOffset,
342
+ effectiveMaxContentHeight,
343
+ peekHeight,
344
+ measuredContentHeight,
345
+ };
346
+
347
+ const computeDetentGeometry = useCallback(() => {
348
+ const inputs = geometryInputsRef.current;
349
+ const windowH = window.innerHeight;
350
+ const effectiveH = inputs.effectiveDetached
351
+ ? windowH - inputs.effectiveDetachedOffset
352
+ : windowH;
353
+ // Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
354
+ const ceiling =
355
+ inputs.effectiveMaxContentHeight !== undefined
356
+ ? Math.min(effectiveH, inputs.effectiveMaxContentHeight)
357
+ : effectiveH;
358
+ // Matches vaul's 'auto' fallback (effectiveHeight / 2) before content is
359
+ // measured — keeps targets consistent even before the drawer mounts.
360
+ const autoHeight = Math.min(
361
+ inputs.measuredContentHeight > 0 ? inputs.measuredContentHeight : effectiveH / 2,
362
+ ceiling
363
+ );
364
+
365
+ const positions: number[] = [];
366
+ const values: number[] = [];
367
+ for (const d of validDetentsRef.current) {
368
+ const h =
369
+ typeof d === 'number'
370
+ ? Math.min(d * effectiveH, ceiling)
371
+ : d === 'peek'
372
+ ? Math.min(inputs.peekHeight, ceiling)
373
+ : autoHeight;
374
+ positions.push(effectiveH - h);
375
+ values.push(effectiveH > 0 ? h / effectiveH : 0);
376
+ }
377
+ return { windowH, positions, values };
294
378
  }, []);
295
379
 
380
+ // Detent info for lifecycle events. Position/detent come from the active
381
+ // detent's target geometry — not the live DOM rect — so willPresent (drawer
382
+ // not mounted yet), detentChange (animation just started), and didPresent
383
+ // all emit the settled detent position, matching iOS/Android. Drag events
384
+ // pass `live: true` to report the in-flight rect position instead, also
385
+ // matching native.
386
+ const computeDetentInfo = useCallback(
387
+ (live = false): DetentInfoEventPayload => {
388
+ const snap = activeSnapPointRef.current;
389
+ const index = snap != null ? validDetentsRef.current.indexOf(snap) : -1;
390
+ const { windowH, positions, values } = computeDetentGeometry();
391
+ const target = index >= 0 ? positions[index] : undefined;
392
+ const position = live
393
+ ? (drawerContentRef.current?.getBoundingClientRect().top ?? target ?? windowH)
394
+ : (target ?? windowH);
395
+ return { index, position, detent: index >= 0 ? (values[index] ?? 0) : 0 };
396
+ },
397
+ [computeDetentGeometry]
398
+ );
399
+
296
400
  // Mirror Android: interpolate fractional index and detent from the drawer's
297
401
  // top-Y so continuous position updates (drag, animation) carry smooth values
298
- // between detent boundaries. Numeric detent d → top-Y = (1 - d) * effectiveH.
299
- // 'auto' resolves to the [data-vaul-auto-size-wrapper] element's measured
300
- // offsetHeight — same signal vaul uses to compute its snap offset.
402
+ // between detent boundaries.
301
403
  const interpolateFromPosition = useCallback(
302
404
  (position: number): { index: number; detent: number } => {
303
- const snaps = validDetentsRef.current;
304
- const count = snaps.length;
405
+ const { windowH, positions, values } = computeDetentGeometry();
406
+ const count = positions.length;
305
407
  if (count === 0) return { index: -1, detent: 0 };
306
408
 
307
- const windowH = window.innerHeight;
308
- const effectiveH = effectiveDetached ? windowH - detachedOffset : windowH;
309
- // Matches vaul's height ceiling: min(effectiveH, maxContentHeight).
310
- const ceiling =
311
- maxContentHeight !== undefined ? Math.min(effectiveH, maxContentHeight) : effectiveH;
312
-
313
- const autoWrapper = drawerContentRef.current?.querySelector<HTMLElement>(
314
- '[data-vaul-auto-size-wrapper]'
315
- );
316
- const autoHeight = Math.min(autoWrapper?.offsetHeight ?? ceiling / 2, ceiling);
317
-
318
- const positions: number[] = [];
319
- const values: number[] = [];
320
- for (let i = 0; i < count; i++) {
321
- const d = snaps[i];
322
- if (typeof d === 'number') {
323
- const h = Math.min(d * effectiveH, ceiling);
324
- positions.push(effectiveH - h);
325
- values.push(effectiveH > 0 ? h / effectiveH : 0);
326
- } else if (d === 'peek') {
327
- const h = Math.min(peekHeight, ceiling);
328
- positions.push(effectiveH - h);
329
- values.push(effectiveH > 0 ? h / effectiveH : 0);
330
- } else {
331
- positions.push(effectiveH - autoHeight);
332
- values.push(effectiveH > 0 ? autoHeight / effectiveH : 0);
333
- }
334
- }
335
-
336
409
  // Absorb subpixel drift from getBoundingClientRect so at-rest positions
337
410
  // don't sneak into the below-first branch and emit near-zero negatives
338
411
  // like `-1e-8` (which render as "-1" via JS scientific-notation toString).
@@ -378,7 +451,7 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
378
451
 
379
452
  return { index: count - 1, detent: values[count - 1]! };
380
453
  },
381
- [effectiveDetached, detachedOffset, maxContentHeight, peekHeight]
454
+ [computeDetentGeometry]
382
455
  );
383
456
 
384
457
  const handlePositionChange = useCallback(
@@ -504,14 +577,14 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
504
577
  const handleDrag = useCallback(() => {
505
578
  if (!isDraggingRef.current) {
506
579
  isDraggingRef.current = true;
507
- onDragBeginRef.current?.({ nativeEvent: computeDetentInfo() } as DragBeginEvent);
580
+ onDragBeginRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragBeginEvent);
508
581
  }
509
- onDragChangeRef.current?.({ nativeEvent: computeDetentInfo() } as DragChangeEvent);
582
+ onDragChangeRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragChangeEvent);
510
583
  }, [computeDetentInfo]);
511
584
  const handleRelease = useCallback(() => {
512
585
  if (!isDraggingRef.current) return;
513
586
  isDraggingRef.current = false;
514
- onDragEndRef.current?.({ nativeEvent: computeDetentInfo() } as DragEndEvent);
587
+ onDragEndRef.current?.({ nativeEvent: computeDetentInfo(true) } as DragEndEvent);
515
588
  }, [computeDetentInfo]);
516
589
 
517
590
  const { isNested, dismissAbove, descendants } = useSheetStack(
@@ -764,39 +837,9 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
764
837
  // Form-sheet style (presentation='form'): centered floating card with a
765
838
  // default width and a height fit to content. We reuse the existing detached
766
839
  // mechanic so drag/snap math stays correct — the wrapper is bottom-attached
767
- // with a computed offset that centers it vertically. `presentation` is
768
- // absolute: when 'form', `maxContentWidth` is ignored and the card uses
769
- // DEFAULT_FORM_SHEET_WIDTH.
770
-
771
- // Vaul measures the auto-size wrapper's offsetHeight (always, post fork).
772
- // Track it here so the form sheet can size its card to fit content,
773
- // clamped between a minimum ratio of the viewport and a maximum derived
774
- // from `detachedOffset` (the breathing room left at top + bottom of the
775
- // floating card).
776
- const [measuredContentHeight, setMeasuredContentHeight] = useState(0);
777
-
778
- const effectiveMaxContentHeight = useMemo<number | undefined>(() => {
779
- if (maxContentHeight !== undefined) return maxContentHeight;
780
- if (!isFormSheet) return undefined;
781
- const min = windowHeight * DEFAULT_FORM_SHEET_HEIGHT_RATIO;
782
- const max = Math.max(min, windowHeight - 2 * detachedOffset);
783
- if (measuredContentHeight <= 0) return min;
784
- return Math.max(min, Math.min(measuredContentHeight, max));
785
- }, [maxContentHeight, isFormSheet, windowHeight, detachedOffset, measuredContentHeight]);
786
-
787
- // Center the form sheet using the actual visible drawer height. Vaul
788
- // auto-sizes to content (capped by `maxContentHeight`), so when content is
789
- // shorter than `effectiveMaxContentHeight`'s min-clamped floor, using that
790
- // for the offset would push a small sheet below the viewport center.
791
- const effectiveDetachedOffset = useMemo(() => {
792
- if (!isFormSheet) return detachedOffset;
793
- const max = Math.max(0, windowHeight - 2 * detachedOffset);
794
- const visibleHeight =
795
- measuredContentHeight > 0
796
- ? Math.min(measuredContentHeight, max)
797
- : (effectiveMaxContentHeight ?? 0);
798
- return Math.max(0, (windowHeight - visibleHeight) / 2);
799
- }, [isFormSheet, windowHeight, detachedOffset, measuredContentHeight, effectiveMaxContentHeight]);
840
+ // with a computed offset (`effectiveDetachedOffset`, declared above) that
841
+ // centers it vertically. `presentation` is absolute: when 'form',
842
+ // `maxContentWidth` is ignored and the card uses DEFAULT_FORM_SHEET_WIDTH.
800
843
 
801
844
  // The wrapper holds all horizontal sizing/anchoring so its rounded-bottom
802
845
  // clip (when detached) aligns with the drawer's horizontal bounds on
@@ -930,7 +973,10 @@ const TrueSheetComponent = forwardRef<TrueSheetMethods, TrueSheetProps>((props,
930
973
  {isValidElement(header) ? header : createElement(header)}
931
974
  </View>
932
975
  )}
933
- <div style={scrollableContainerStyle}>
976
+ <div
977
+ style={scrollableContainerStyle}
978
+ data-vaul-scroll-locked={isScrollLocked ? '' : undefined}
979
+ >
934
980
  <View ref={contentRef} style={style}>
935
981
  {children}
936
982
  </View>
@@ -24,7 +24,7 @@ import type { DrawerDirection } from './types';
24
24
  import { useComposedRefs } from './use-composed-refs';
25
25
  import { useControllableState } from './use-controllable-state';
26
26
  import { usePositionFixed } from './use-position-fixed';
27
- import { isInput, usePreventScroll } from './use-prevent-scroll';
27
+ import { isInput, isScrollable, usePreventScroll } from './use-prevent-scroll';
28
28
  import { useScaleBackground } from './use-scale-background';
29
29
  import { useSnapPoints } from './use-snap-points';
30
30
 
@@ -337,6 +337,44 @@ export function Root({
337
337
  noBodyStyles,
338
338
  });
339
339
 
340
+ // While the sheet itself is being dragged, scrollables in the touched chain
341
+ // are frozen (overflow: hidden). Touch browsers latch the scroll gesture at
342
+ // touchstart and ignore preventDefault once scrolling has started, so making
343
+ // the scroller non-scrollable mid-gesture is the only reliable way to keep
344
+ // the content from panning along with the sheet.
345
+ const frozenScrollablesRef = React.useRef<
346
+ { element: HTMLElement; overflowX: string; overflowY: string }[] | null
347
+ >(null);
348
+
349
+ function freezeScrollables(target: EventTarget) {
350
+ if (frozenScrollablesRef.current) return;
351
+ const frozen: { element: HTMLElement; overflowX: string; overflowY: string }[] = [];
352
+ let element = target instanceof HTMLElement ? target : null;
353
+ while (element && element !== drawerRef.current) {
354
+ if (isScrollable(element)) {
355
+ frozen.push({
356
+ element,
357
+ overflowX: element.style.overflowX,
358
+ overflowY: element.style.overflowY,
359
+ });
360
+ element.style.overflowX = 'hidden';
361
+ element.style.overflowY = 'hidden';
362
+ }
363
+ element = element.parentElement;
364
+ }
365
+ frozenScrollablesRef.current = frozen;
366
+ }
367
+
368
+ function unfreezeScrollables() {
369
+ const frozen = frozenScrollablesRef.current;
370
+ if (!frozen) return;
371
+ frozenScrollablesRef.current = null;
372
+ for (const { element, overflowX, overflowY } of frozen) {
373
+ element.style.overflowX = overflowX;
374
+ element.style.overflowY = overflowY;
375
+ }
376
+ }
377
+
340
378
  function getScale() {
341
379
  return (window.innerWidth - WINDOW_TOP_OFFSET) / window.innerWidth;
342
380
  }
@@ -386,7 +424,16 @@ export function Root({
386
424
  }
387
425
 
388
426
  if (swipeAmount !== null) {
389
- if (direction === 'bottom' ? swipeAmount > 0 : swipeAmount < 0) {
427
+ // Translated past the drag threshold keep dragging. Below the last
428
+ // snap point the threshold is 0, so the sheet always wins over content
429
+ // scrolling. At the last snap point the threshold is its resting
430
+ // translate — which can still be > 0 when the max detent < 1 — so an
431
+ // at-rest sheet falls through to the scroll checks and content
432
+ // scrolling wins, matching native. Mid-drag/mid-animation the sheet is
433
+ // displaced past rest and still wins.
434
+ const atLastSnapPoint = snapPoints && activeSnapPointIndex === snapPoints.length - 1;
435
+ const restOffset = atLastSnapPoint ? (snapPointsOffset?.[activeSnapPointIndex!] ?? 0) : 0;
436
+ if (direction === 'bottom' ? swipeAmount > restOffset + 1 : swipeAmount < restOffset - 1) {
390
437
  return true;
391
438
  }
392
439
  }
@@ -396,13 +443,16 @@ export function Root({
396
443
  return false;
397
444
  }
398
445
 
399
- // Disallow dragging if drawer was scrolled within `scrollLockTimeout`
446
+ // Disallow dragging if drawer was scrolled within `scrollLockTimeout`.
447
+ // Don't re-arm the timestamp here — a prevented drag attempt is not a
448
+ // scroll, and re-arming would keep the lock alive for as long as the
449
+ // finger moves, deadening the whole gesture instead of just the first
450
+ // `scrollLockTimeout` ms after the last real scroll.
400
451
  if (
401
452
  lastTimeDragPrevented.current &&
402
453
  date.getTime() - lastTimeDragPrevented.current.getTime() < scrollLockTimeout &&
403
454
  swipeAmount === 0
404
455
  ) {
405
- lastTimeDragPrevented.current = date;
406
456
  return false;
407
457
  }
408
458
 
@@ -417,7 +467,11 @@ export function Root({
417
467
  while (element) {
418
468
  // Check if the element is scrollable
419
469
  if (element.scrollHeight > element.clientHeight) {
420
- if (element.scrollTop !== 0) {
470
+ // `> 0`, not `!== 0`: Safari reports a negative scrollTop during the
471
+ // rubber-band bounce at the top — that's "at the top" for drag
472
+ // purposes, and treating it as scrolled would arm the scroll lock and
473
+ // delay the sheet drag until the bounce fully settles.
474
+ if (element.scrollTop > 0) {
421
475
  lastTimeDragPrevented.current = new Date();
422
476
 
423
477
  // The element is scrollable and not scrolled to the top, so don't drag
@@ -480,10 +534,21 @@ export function Root({
480
534
  return;
481
535
  }
482
536
 
483
- if (!isAllowedToDrag.current && !shouldDrag(event.target, isDraggingInDirection)) return;
484
- drawerRef.current.classList.add(DRAG_CLASS);
485
- // If shouldDrag gave true once after pressing down on the drawer, we set isAllowedToDrag to true and it will remain true until we let go, there's no reason to disable dragging mid way, ever, and that's the solution to it
486
- isAllowedToDrag.current = true;
537
+ if (!isAllowedToDrag.current) {
538
+ if (!shouldDrag(event.target, isDraggingInDirection)) return;
539
+ drawerRef.current.classList.add(DRAG_CLASS);
540
+ // If shouldDrag gave true once after pressing down on the drawer, we set isAllowedToDrag to true and it will remain true until we let go, there's no reason to disable dragging mid way, ever, and that's the solution to it
541
+ isAllowedToDrag.current = true;
542
+ // Touch pans latched onto a scroller would keep scrolling the content
543
+ // along with the sheet drag — freeze them for the drag's duration.
544
+ if (event.pointerType !== 'mouse') freezeScrollables(event.target);
545
+ // Drag can engage mid-gesture (content scrolled back to its top under
546
+ // the same finger, or the scroll lock expiring). Re-anchor and start
547
+ // moving on the next tick so the sheet tracks the finger from here
548
+ // instead of jumping by the distance the gesture already consumed.
549
+ pointerStart.current = isVertical(direction) ? event.pageY : event.pageX;
550
+ return;
551
+ }
487
552
  set(drawerRef.current, {
488
553
  transition: 'none',
489
554
  });
@@ -691,6 +756,7 @@ export function Root({
691
756
 
692
757
  drawerRef.current.classList.remove(DRAG_CLASS);
693
758
  isAllowedToDrag.current = false;
759
+ unfreezeScrollables();
694
760
  setIsDragging(false);
695
761
  dragEndTime.current = new Date();
696
762
  }
@@ -700,6 +766,7 @@ export function Root({
700
766
 
701
767
  drawerRef.current.classList.remove(DRAG_CLASS);
702
768
  isAllowedToDrag.current = false;
769
+ unfreezeScrollables();
703
770
  setIsDragging(false);
704
771
  dragEndTime.current = new Date();
705
772
  const swipeAmount = getTranslate(drawerRef.current, direction);
@@ -1,7 +1,19 @@
1
+ /* Registered so it can transition in sync with `transform` during snaps.
2
+ Descendants derive their visible-height layout from it, so `inherits: true`
3
+ is required. Unsupported browsers ignore this and the value just jumps to
4
+ the snap target instead of animating. */
5
+ @property --snap-point-height {
6
+ syntax: '<length>';
7
+ inherits: true;
8
+ initial-value: 0px;
9
+ }
10
+
1
11
  [data-vaul-drawer] {
2
12
  touch-action: none;
3
13
  will-change: transform;
4
- transition: transform 0.5s cubic-bezier(0.32, 0.72, 0, 1);
14
+ transition:
15
+ transform 0.5s cubic-bezier(0.32, 0.72, 0, 1),
16
+ --snap-point-height 0.5s cubic-bezier(0.32, 0.72, 0, 1);
5
17
  animation-duration: 0.5s;
6
18
  animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
7
19
  }
@@ -84,6 +96,17 @@
84
96
  transform: translate3d(var(--snap-point-height, 0), 0, 0);
85
97
  }
86
98
 
99
+ /* Below the last snap point, vertical touch pans should move the sheet — not
100
+ scroll the content (matches native). touch-action is latched at gesture
101
+ start and ancestors above a scroller aren't consulted, so every descendant
102
+ (nested scrollers included) must opt out of vertical panning ahead of time.
103
+ pan-x keeps horizontal carousels inside the content working. Wheel/trackpad
104
+ scrolling is unaffected. */
105
+ [data-vaul-scroll-locked],
106
+ [data-vaul-scroll-locked] * {
107
+ touch-action: pan-x !important;
108
+ }
109
+
87
110
  [data-vaul-overlay][data-vaul-snap-points='false'] {
88
111
  animation-duration: 0.5s;
89
112
  animation-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
@@ -198,13 +198,17 @@ export function useSnapPoints({
198
198
 
199
199
  const animateThisSnap = hasSnappedRef.current || initialAnimated;
200
200
  hasSnappedRef.current = true;
201
+ // `--snap-point-height` transitions alongside `transform` (registered via
202
+ // @property) so layouts derived from it (e.g. the scrollable fill) resize
203
+ // in sync with the drawer's slide instead of jumping to the target.
201
204
  set(drawerRef.current, {
202
- transition: animateThisSnap
203
- ? `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
205
+ 'transition': animateThisSnap
206
+ ? `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
204
207
  : 'none',
205
- transform: isVertical(direction)
208
+ 'transform': isVertical(direction)
206
209
  ? `translate3d(0, ${dimension}px, 0)`
207
210
  : `translate3d(${dimension}px, 0, 0)`,
211
+ '--snap-point-height': `${dimension}px`,
208
212
  });
209
213
 
210
214
  // Snapping implies drag overshoot (if any) should be undone.
@@ -360,19 +364,24 @@ export function useSnapPoints({
360
364
  if ((direction === 'bottom' || direction === 'right') && newValue > snapPointsOffset[0]) {
361
365
  const excess = newValue - snapPointsOffset[0];
362
366
  set(drawerRef.current, {
363
- transform: isVertical(direction)
367
+ 'transform': isVertical(direction)
364
368
  ? `translate3d(0, ${snapPointsOffset[0]}px, 0)`
365
369
  : `translate3d(${snapPointsOffset[0]}px, 0, 0)`,
370
+ '--snap-point-height': `${snapPointsOffset[0]}px`,
366
371
  });
367
372
  setDetachedWrapperTransform(excess, false);
368
373
  return;
369
374
  }
370
375
 
371
376
  setDetachedWrapperTransform(0, false);
377
+ // Keep `--snap-point-height` tracking the live drag position so layouts
378
+ // derived from it (e.g. the scrollable fill) resize with the drawer
379
+ // instead of staying cut off at the last detent's visible height.
372
380
  set(drawerRef.current, {
373
- transform: isVertical(direction)
381
+ 'transform': isVertical(direction)
374
382
  ? `translate3d(0, ${newValue}px, 0)`
375
383
  : `translate3d(${newValue}px, 0, 0)`,
384
+ '--snap-point-height': `${newValue}px`,
376
385
  });
377
386
  }
378
387