@lodev09/react-native-true-sheet 4.0.0-beta.14 → 4.0.0-beta.16

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 (74) hide show
  1. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContainerView.kt +28 -6
  2. package/android/src/main/java/com/lodev09/truesheet/TrueSheetContentView.kt +99 -7
  3. package/android/src/main/java/com/lodev09/truesheet/TrueSheetFooterView.kt +12 -0
  4. package/android/src/main/java/com/lodev09/truesheet/TrueSheetOverlayView.kt +11 -1
  5. package/android/src/main/java/com/lodev09/truesheet/TrueSheetView.kt +18 -7
  6. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewController.kt +40 -39
  7. package/android/src/main/java/com/lodev09/truesheet/TrueSheetViewManager.kt +18 -8
  8. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetBottomSheetView.kt +14 -8
  9. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetCoordinatorLayout.kt +8 -1
  10. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDetentCalculator.kt +32 -5
  11. package/android/src/main/java/com/lodev09/truesheet/core/TrueSheetDimView.kt +3 -0
  12. package/ios/TrueSheetContainerView.h +17 -4
  13. package/ios/TrueSheetContainerView.mm +23 -6
  14. package/ios/TrueSheetContentView.h +13 -1
  15. package/ios/TrueSheetContentView.mm +93 -35
  16. package/ios/TrueSheetFooterView.mm +46 -16
  17. package/ios/TrueSheetView.mm +30 -28
  18. package/ios/TrueSheetViewController.h +21 -1
  19. package/ios/TrueSheetViewController.mm +54 -16
  20. package/ios/core/TrueSheetDetentCalculator.h +6 -1
  21. package/ios/core/TrueSheetDetentCalculator.mm +9 -4
  22. package/ios/core/TrueSheetKeyboardObserver.mm +2 -0
  23. package/lib/module/TrueSheet.js +8 -5
  24. package/lib/module/TrueSheet.js.map +1 -1
  25. package/lib/module/TrueSheet.web.js +93 -218
  26. package/lib/module/TrueSheet.web.js.map +1 -1
  27. package/lib/module/TrueSheetPeek.web.js +20 -15
  28. package/lib/module/TrueSheetPeek.web.js.map +1 -1
  29. package/lib/module/fabric/TrueSheetViewNativeComponent.ts +5 -3
  30. package/lib/module/navigation/expo-router/index.js +5 -4
  31. package/lib/module/navigation/expo-router/index.js.map +1 -1
  32. package/lib/module/web/constants.js +1 -1
  33. package/lib/module/web/constants.js.map +1 -1
  34. package/lib/module/web/layout.js +168 -0
  35. package/lib/module/web/layout.js.map +1 -0
  36. package/lib/module/web/vaul/helpers.js +1 -0
  37. package/lib/module/web/vaul/helpers.js.map +1 -1
  38. package/lib/module/web/vaul/index.js +9 -4
  39. package/lib/module/web/vaul/index.js.map +1 -1
  40. package/lib/module/web/vaul/use-snap-points.js +6 -8
  41. package/lib/module/web/vaul/use-snap-points.js.map +1 -1
  42. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  43. package/lib/typescript/src/TrueSheet.types.d.ts +64 -25
  44. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  45. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  46. package/lib/typescript/src/TrueSheetPeek.web.d.ts +8 -4
  47. package/lib/typescript/src/TrueSheetPeek.web.d.ts.map +1 -1
  48. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts +5 -3
  49. package/lib/typescript/src/fabric/TrueSheetViewNativeComponent.d.ts.map +1 -1
  50. package/lib/typescript/src/navigation/expo-router/index.d.ts +5 -3
  51. package/lib/typescript/src/navigation/expo-router/index.d.ts.map +1 -1
  52. package/lib/typescript/src/navigation/types.d.ts +1 -1
  53. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  54. package/lib/typescript/src/web/constants.d.ts +1 -1
  55. package/lib/typescript/src/web/constants.d.ts.map +1 -1
  56. package/lib/typescript/src/web/layout.d.ts +19 -0
  57. package/lib/typescript/src/web/layout.d.ts.map +1 -0
  58. package/lib/typescript/src/web/vaul/helpers.d.ts +1 -0
  59. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -1
  60. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -1
  61. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -1
  62. package/package.json +7 -6
  63. package/src/TrueSheet.tsx +7 -4
  64. package/src/TrueSheet.types.ts +67 -25
  65. package/src/TrueSheet.web.tsx +132 -263
  66. package/src/TrueSheetPeek.web.tsx +25 -23
  67. package/src/fabric/TrueSheetViewNativeComponent.ts +5 -3
  68. package/src/navigation/expo-router/index.ts +8 -11
  69. package/src/navigation/types.ts +3 -2
  70. package/src/web/constants.ts +1 -1
  71. package/src/web/layout.ts +196 -0
  72. package/src/web/vaul/helpers.ts +5 -0
  73. package/src/web/vaul/index.tsx +21 -4
  74. package/src/web/vaul/use-snap-points.ts +6 -14
@@ -1,16 +1,15 @@
1
- import { createContext, type RefObject, useContext, useEffect, useRef } from 'react';
1
+ import { createContext, type RefObject, useContext, useLayoutEffect, useRef } from 'react';
2
2
  import { type LayoutChangeEvent, View, type ViewProps } from 'react-native';
3
3
 
4
- import { getDOMElement } from './web/dom';
5
-
6
4
  /**
7
- * Reports the measured peek content height to the owning TrueSheet.
5
+ * Registers a peek with the owning TrueSheet so it can measure the peek live.
8
6
  * @internal
9
7
  */
10
8
  export interface TrueSheetPeekContextValue {
11
9
  contentRef: RefObject<View | null>;
12
- peekRef: RefObject<View | null>;
13
- setPeekContentHeight: (height: number) => void;
10
+ attachPeek: (view: View) => void;
11
+ detachPeek: (view: View) => void;
12
+ measurePeek: (view: View) => void;
14
13
  }
15
14
 
16
15
  export const TrueSheetPeekContext = createContext<TrueSheetPeekContextValue | null>(null);
@@ -19,14 +18,19 @@ export const TrueSheetPeekContext = createContext<TrueSheetPeekContextValue | nu
19
18
  * Distance from the top of the content view to the bottom of the peek view,
20
19
  * so the peek view's offset within the content (padding, views above it)
21
20
  * counts toward the peek detent. Both rects live in the drawer's transformed
22
- * subtree, so an in-flight translate cancels out of the delta.
21
+ * subtree, so an in-flight translate cancels out of the delta. Rounded so the
22
+ * sub-pixel noise of a mid-transition measurement doesn't register as a
23
+ * change — the content re-measures every frame while the snap animates, and
24
+ * a new value would restart the snap.
23
25
  * @internal
24
26
  */
25
27
  export const measurePeekContentHeight = (
26
28
  peekElement: HTMLElement,
27
29
  contentElement: HTMLElement
28
30
  ): number =>
29
- peekElement.getBoundingClientRect().bottom - contentElement.getBoundingClientRect().top;
31
+ Math.round(
32
+ peekElement.getBoundingClientRect().bottom - contentElement.getBoundingClientRect().top
33
+ );
30
34
 
31
35
  /**
32
36
  * Wrapper component that marks its children as the sheet's peek content.
@@ -36,25 +40,23 @@ export const measurePeekContentHeight = (
36
40
  */
37
41
  export const TrueSheetPeek = ({ onLayout, ...rest }: ViewProps) => {
38
42
  const context = useContext(TrueSheetPeekContext);
39
- const localRef = useRef<View>(null);
43
+ const viewRef = useRef<View>(null);
40
44
 
41
- // Register into the sheet's peek ref so it can measure the peek live
42
- // during detent geometry computation (state lags the first willPresent).
43
- const viewRef = context?.peekRef ?? localRef;
45
+ // Attach by instance rather than sharing a ref — during a screen swap the
46
+ // incoming peek mounts before the outgoing one unmounts, and a shared ref
47
+ // would be nulled by the outgoing unmount.
48
+ useLayoutEffect(() => {
49
+ const view = viewRef.current;
50
+ if (!context || !view) return;
44
51
 
45
- useEffect(() => () => context?.setPeekContentHeight(0), [context]);
52
+ context.attachPeek(view);
53
+ return () => context.detachPeek(view);
54
+ }, [context]);
46
55
 
47
56
  const handleLayout = (event: LayoutChangeEvent) => {
48
- if (context) {
49
- const peekElement = getDOMElement(viewRef.current);
50
- const contentElement = getDOMElement(context.contentRef.current);
51
-
52
- const bottom =
53
- peekElement && contentElement
54
- ? measurePeekContentHeight(peekElement, contentElement)
55
- : event.nativeEvent.layout.height;
56
-
57
- context.setPeekContentHeight(bottom);
57
+ const view = viewRef.current;
58
+ if (context && view) {
59
+ context.measurePeek(view);
58
60
  }
59
61
 
60
62
  onLayout?.(event);
@@ -35,7 +35,7 @@ type BlurOptionsType = Readonly<{
35
35
  type ScrollEdgeEffect = 'automatic' | 'hard' | 'soft' | 'hidden';
36
36
 
37
37
  type ScrollableOptionsType = Readonly<{
38
- contentInsetAdjustmentBehavior?: WithDefault<boolean, true>;
38
+ contentInsetAdjustment?: WithDefault<'automatic' | 'safe-area' | 'footer' | 'never', 'automatic'>;
39
39
  keyboardScrollOffset?: WithDefault<Double, 0>;
40
40
  keyboardOffset?: WithDefault<Double, 0>;
41
41
  scrollingExpandsSheet?: WithDefault<boolean, true>;
@@ -48,6 +48,7 @@ type FooterOptionsType = Readonly<{
48
48
  // the field name, so a second `position` would redefine TrueSheetViewPosition
49
49
  footerPosition?: WithDefault<'relative' | 'absolute', 'relative'>;
50
50
  keyboardOffset?: WithDefault<Double, 0>;
51
+ avoidKeyboard?: WithDefault<boolean, true>;
51
52
  }>;
52
53
 
53
54
  type HeaderOptionsType = Readonly<{
@@ -109,14 +110,15 @@ export interface NativeProps extends ViewProps {
109
110
  'none'
110
111
  >;
111
112
 
112
- anchor?: WithDefault<'left' | 'center' | 'right', 'center'>;
113
- anchorOffset?: WithDefault<Double, 16>;
113
+ placement?: WithDefault<'automatic' | 'leading' | 'center' | 'trailing', 'automatic'>;
114
+ placementOffset?: WithDefault<Double, 16>;
114
115
  insetAdjustment?: WithDefault<'automatic' | 'never', 'automatic'>;
115
116
 
116
117
  // Blur options
117
118
  blurOptions?: BlurOptionsType;
118
119
 
119
120
  // Boolean properties - match defaults from TrueSheet.types.ts
121
+ glass?: WithDefault<boolean, true>;
120
122
  grabber?: WithDefault<boolean, true>;
121
123
  grabberOptions?: GrabberOptionsType;
122
124
  accessibilityOptions?: AccessibilityOptionsType;
@@ -22,12 +22,15 @@ const sheetRouter: TrueSheetRouterFactory = createTrueSheetRouter(
22
22
  StackRouter as StackRouterFactory
23
23
  );
24
24
 
25
+ // `routes`/`dispatch` are injected by `createProps`, so they are declared as
26
+ // CreateProps (last generic) and excluded from the public navigator props
25
27
  const SheetNavigator = unstable_integrateWithRouter<
26
28
  TrueSheetNavigationOptions,
27
29
  TrueSheetNavigationState<ParamListBase>,
28
30
  TrueSheetStandardEventMap,
29
- TrueSheetNavigatorContentExtraProps,
30
- TrueSheetRouterOptions
31
+ object,
32
+ TrueSheetRouterOptions,
33
+ TrueSheetNavigatorContentExtraProps
31
34
  >(trueSheetNavigator, sheetRouter, {
32
35
  createProps: ({ state, dispatch }) => ({
33
36
  routes: state.routes,
@@ -35,15 +38,9 @@ const SheetNavigator = unstable_integrateWithRouter<
35
38
  }),
36
39
  });
37
40
 
38
- // Public props: `routes`/`dispatch` are injected by `createProps` (optional here
39
- // so the navigator type stays comparable), and expo-router intersects the event
40
- // map with an index signature which breaks precisely typed `screenListeners`
41
- // callbacks - replace it with the exact map
42
- type SheetProps = Omit<
43
- React.ComponentProps<typeof SheetNavigator>,
44
- 'screenListeners' | 'routes' | 'dispatch'
45
- > &
46
- Partial<Pick<React.ComponentProps<typeof SheetNavigator>, 'routes' | 'dispatch'>> &
41
+ // expo-router intersects the event map with an index signature which breaks
42
+ // precisely typed `screenListeners` callbacks - replace it with the exact map
43
+ type SheetProps = Omit<React.ComponentProps<typeof SheetNavigator>, 'screenListeners'> &
47
44
  Pick<TrueSheetNavigatorProps, 'screenListeners'>;
48
45
 
49
46
  /**
@@ -216,6 +216,7 @@ export type TrueSheetNavigationSheetProps = Pick<
216
216
  | 'dimmedDetentIndex'
217
217
  | 'backgroundBlur'
218
218
  | 'blurOptions'
219
+ | 'glass'
219
220
  | 'maxContentHeight'
220
221
  | 'maxContentWidth'
221
222
  | 'presentation'
@@ -229,8 +230,8 @@ export type TrueSheetNavigationSheetProps = Pick<
229
230
  | 'scrollableRef'
230
231
  | 'scrollableOptions'
231
232
  | 'insetAdjustment'
232
- | 'anchor'
233
- | 'anchorOffset'
233
+ | 'placement'
234
+ | 'placementOffset'
234
235
  | 'elevation'
235
236
  | 'detached'
236
237
  | 'detachedOffset'
@@ -4,7 +4,7 @@ export const DEFAULT_FORM_SHEET_WIDTH = 580;
4
4
  export const DEFAULT_FORM_SHEET_HEIGHT_RATIO = 0.5;
5
5
  export const COLOR_SURFACE_CONTAINER_LOW_LIGHT = '#F7F2FA';
6
6
  export const COLOR_SURFACE_CONTAINER_LOW_DARK = '#1D1B20';
7
- export const DEFAULT_ANCHOR_OFFSET = 16;
7
+ export const DEFAULT_PLACEMENT_OFFSET = 16;
8
8
  export const DEFAULT_DETACHED_OFFSET = 16;
9
9
  export const DEFAULT_GRABBER_COLOR_LIGHT = 'rgba(0, 0, 0, 0.3)';
10
10
  export const DEFAULT_GRABBER_COLOR_DARK = 'rgba(255, 255, 255, 0.3)';
@@ -0,0 +1,196 @@
1
+ /// <reference lib="dom" />
2
+
3
+ const FOOTER_INSET = '--truesheet-footer-inset';
4
+
5
+ interface SheetLayoutOptions {
6
+ drawer: HTMLElement;
7
+ layout: HTMLElement;
8
+ getContent: () => HTMLElement | null;
9
+ getHeader: () => HTMLElement | null;
10
+ getFooter: () => HTMLElement | null;
11
+ getScrollable: () => HTMLElement | null;
12
+ absoluteHeader: boolean;
13
+ absoluteFooter: boolean;
14
+ footerInsetAdjustment: boolean;
15
+ measureContent: boolean;
16
+ onHeightChange: (height: number) => void;
17
+ }
18
+
19
+ export function observeSheetLayout({
20
+ drawer,
21
+ layout,
22
+ getContent,
23
+ getHeader,
24
+ getFooter,
25
+ getScrollable,
26
+ absoluteHeader,
27
+ absoluteFooter,
28
+ footerInsetAdjustment,
29
+ measureContent,
30
+ onHeightChange,
31
+ }: SheetLayoutOptions) {
32
+ let frame = 0;
33
+ let disposed = false;
34
+ let width = -1;
35
+ let height = -1;
36
+ let scroller: HTMLElement | null = null;
37
+ let originalPadding = '';
38
+ let insetPadding = '';
39
+ let appliedInset = 0;
40
+ const observed = new Set<Element>();
41
+
42
+ // This tree has no viewport constraint or injected footer inset. Its resize
43
+ // observer catches intrinsic changes without observing the live flex layout.
44
+ const measurement = measureContent ? document.createElement('div') : null;
45
+ if (measurement) {
46
+ Object.assign(measurement.style, {
47
+ position: 'absolute',
48
+ top: '0',
49
+ left: '0',
50
+ display: 'flex',
51
+ flexDirection: 'column',
52
+ visibility: 'hidden',
53
+ pointerEvents: 'none',
54
+ });
55
+ measurement.style.setProperty(FOOTER_INSET, '0px');
56
+ measurement.inert = true;
57
+ measurement.setAttribute('aria-hidden', 'true');
58
+ measurement.setAttribute('data-truesheet-measure', '');
59
+ drawer.appendChild(measurement);
60
+ }
61
+
62
+ const measure = () => {
63
+ if (!measurement?.isConnected) return;
64
+ const headerHeight = absoluteHeader ? 0 : (getHeader()?.offsetHeight ?? 0);
65
+ const footerHeight =
66
+ !absoluteFooter || (footerInsetAdjustment && scroller) ? (getFooter()?.offsetHeight ?? 0) : 0;
67
+ const next = measurement.offsetHeight + headerHeight + footerHeight;
68
+ if (height === next) return;
69
+ height = next;
70
+ onHeightChange(next);
71
+ };
72
+
73
+ const updateInset = () => {
74
+ let inset = 0;
75
+ const footer = getFooter();
76
+ if (footerInsetAdjustment && scroller && footer) {
77
+ let bottom = scroller.offsetTop + scroller.offsetHeight;
78
+ let parent = scroller.offsetParent;
79
+ while (parent instanceof HTMLElement && parent !== layout) {
80
+ bottom += parent.offsetTop;
81
+ parent = parent.offsetParent;
82
+ }
83
+ if (parent === layout) {
84
+ inset = Math.min(
85
+ footer.offsetHeight,
86
+ Math.max(0, bottom - (layout.clientHeight - footer.offsetHeight))
87
+ );
88
+ }
89
+ }
90
+ if (inset === appliedInset) return;
91
+ appliedInset = inset;
92
+ layout.style.setProperty(FOOTER_INSET, `${inset}px`);
93
+ };
94
+
95
+ const resizeObserver = new ResizeObserver(() => {
96
+ if (measurement && width !== layout.clientWidth) {
97
+ width = layout.clientWidth;
98
+ measurement.style.width = `${width}px`;
99
+ }
100
+ updateInset();
101
+ measure();
102
+ });
103
+ const measurementObserver = new ResizeObserver(measure);
104
+ if (measurement) measurementObserver.observe(measurement);
105
+
106
+ const restorePadding = () => {
107
+ if (scroller && scroller.style.paddingBottom === insetPadding) {
108
+ scroller.style.paddingBottom = originalPadding;
109
+ }
110
+ };
111
+
112
+ const sync = () => {
113
+ if (disposed) return;
114
+ const content = getContent();
115
+ const nextScroller = getScrollable();
116
+ if (nextScroller !== scroller) {
117
+ restorePadding();
118
+ scroller = nextScroller;
119
+ insetPadding = '';
120
+ }
121
+ if (
122
+ footerInsetAdjustment &&
123
+ scroller &&
124
+ (!insetPadding || scroller.style.paddingBottom !== insetPadding)
125
+ ) {
126
+ originalPadding = scroller.style.paddingBottom;
127
+ const base = getComputedStyle(scroller).paddingBottom;
128
+ insetPadding = `calc(${base} + var(${FOOTER_INSET}, 0px))`;
129
+ scroller.style.paddingBottom = insetPadding;
130
+ insetPadding = scroller.style.paddingBottom;
131
+ }
132
+
133
+ const nodes = new Set<Element>([layout]);
134
+ for (const node of [getHeader(), getFooter(), scroller]) {
135
+ if (node) nodes.add(node);
136
+ }
137
+ for (const node of observed) {
138
+ if (!nodes.has(node)) {
139
+ resizeObserver.unobserve(node);
140
+ observed.delete(node);
141
+ }
142
+ }
143
+ for (const node of nodes) {
144
+ if (!observed.has(node)) {
145
+ resizeObserver.observe(node);
146
+ observed.add(node);
147
+ }
148
+ }
149
+
150
+ if (measurement && content) {
151
+ width = layout.clientWidth;
152
+ measurement.style.width = `${width}px`;
153
+ measurement.replaceChildren(content.cloneNode(true));
154
+ }
155
+ updateInset();
156
+ measure();
157
+ };
158
+
159
+ const mutationObserver = new MutationObserver((mutations) => {
160
+ const changed = mutations.some(
161
+ (mutation) => mutation.target !== layout || mutation.type === 'childList'
162
+ );
163
+ if (!changed || frame) return;
164
+ frame = requestAnimationFrame(() => {
165
+ frame = 0;
166
+ sync();
167
+ });
168
+ });
169
+
170
+ sync();
171
+ mutationObserver.observe(layout, {
172
+ childList: true,
173
+ characterData: true,
174
+ attributes: true,
175
+ attributeFilter: ['style', 'class'],
176
+ subtree: true,
177
+ });
178
+ const footer = getFooter();
179
+ if (absoluteFooter && footer) {
180
+ mutationObserver.observe(footer, { childList: true, characterData: true, subtree: true });
181
+ }
182
+
183
+ return {
184
+ measure,
185
+ disconnect: () => {
186
+ disposed = true;
187
+ cancelAnimationFrame(frame);
188
+ mutationObserver.disconnect();
189
+ resizeObserver.disconnect();
190
+ measurementObserver.disconnect();
191
+ restorePadding();
192
+ layout.style.removeProperty(FOOTER_INSET);
193
+ measurement?.remove();
194
+ },
195
+ };
196
+ }
@@ -80,6 +80,11 @@ export const isVertical = (direction: DrawerDirection) => {
80
80
  }
81
81
  };
82
82
 
83
+ export const getSnapPointTransform = (direction: DrawerDirection) =>
84
+ isVertical(direction)
85
+ ? 'translate3d(0, var(--snap-point-height), 0)'
86
+ : 'translate3d(var(--snap-point-height), 0, 0)';
87
+
83
88
  export function getTranslate(element: HTMLElement, direction: DrawerDirection): number | null {
84
89
  if (!element) {
85
90
  return null;
@@ -20,7 +20,14 @@ import {
20
20
  VELOCITY_THRESHOLD,
21
21
  WINDOW_TOP_OFFSET,
22
22
  } from './constants';
23
- import { dampenValue, getTranslate, isVertical, reset, set } from './helpers';
23
+ import {
24
+ dampenValue,
25
+ getSnapPointTransform,
26
+ getTranslate,
27
+ isVertical,
28
+ reset,
29
+ set,
30
+ } from './helpers';
24
31
  import type { DrawerDirection } from './types';
25
32
  import { useComposedRefs } from './use-composed-refs';
26
33
  import { useControllableState } from './use-controllable-state';
@@ -1301,11 +1308,17 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1301
1308
  // same detent). Without this, position goes stale mid-animation because
1302
1309
  // the drawer's `transitionrun` never fires.
1303
1310
  const onTransitionRun = (e: TransitionEvent) => {
1304
- if ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform')
1311
+ if (
1312
+ (e.target === drawer || e.target === wrapper) &&
1313
+ (e.propertyName === 'transform' || e.propertyName === '--snap-point-height')
1314
+ )
1305
1315
  state.start();
1306
1316
  };
1307
1317
  const onTransitionDone = (e: TransitionEvent) => {
1308
- if ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform')
1318
+ if (
1319
+ (e.target === drawer || e.target === wrapper) &&
1320
+ (e.propertyName === 'transform' || e.propertyName === '--snap-point-height')
1321
+ )
1309
1322
  state.stop();
1310
1323
  };
1311
1324
  const onAnimationStart = (e: AnimationEvent) => {
@@ -1412,6 +1425,9 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1412
1425
  wasOpenRef.current = isOpen;
1413
1426
  }, [isOpen, detached, drawerRef]);
1414
1427
 
1428
+ // Subsequent snap writes belong to useSnapPoints. A React style update on
1429
+ // release would overwrite the live drag value before its transition starts.
1430
+ const initialSnapPointOffset = React.useRef(snapPointsOffset?.[activeSnapPointIndex ?? 0] ?? 0);
1415
1431
  const contentNode = (
1416
1432
  <DialogPrimitive.Content
1417
1433
  data-vaul-drawer-direction={direction}
@@ -1426,7 +1442,8 @@ export const Content = React.forwardRef<HTMLDivElement, ContentProps>(
1426
1442
  style={
1427
1443
  snapPointsOffset && snapPointsOffset.length > 0
1428
1444
  ? ({
1429
- '--snap-point-height': `${snapPointsOffset[activeSnapPointIndex ?? 0]!}px`,
1445
+ '--snap-point-height': `${initialSnapPointOffset.current}px`,
1446
+ 'transform': getSnapPointTransform(direction),
1430
1447
  ...style,
1431
1448
  'pointerEvents': 'auto',
1432
1449
  } as React.CSSProperties)
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck — vendored upstream, incompatible with noUncheckedIndexedAccess
2
2
  import React from 'react';
3
- import { set, isVertical } from './helpers';
3
+ import { set, isVertical, getSnapPointTransform } from './helpers';
4
4
  import { DEFAULT_PEEK_HEIGHT, TRANSITIONS, VELOCITY_THRESHOLD } from './constants';
5
5
  import { useControllableState } from './use-controllable-state';
6
6
  import type { DrawerDirection } from './types';
@@ -198,16 +198,12 @@ 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
+ // One animated value drives both the translation and the visible height.
204
202
  set(drawerRef.current, {
205
203
  'transition': animateThisSnap
206
- ? `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')}), --snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
204
+ ? `--snap-point-height ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
207
205
  : 'none',
208
- 'transform': isVertical(direction)
209
- ? `translate3d(0, ${dimension}px, 0)`
210
- : `translate3d(${dimension}px, 0, 0)`,
206
+ 'transform': getSnapPointTransform(direction),
211
207
  '--snap-point-height': `${dimension}px`,
212
208
  });
213
209
 
@@ -364,9 +360,7 @@ export function useSnapPoints({
364
360
  if ((direction === 'bottom' || direction === 'right') && newValue > snapPointsOffset[0]) {
365
361
  const excess = newValue - snapPointsOffset[0];
366
362
  set(drawerRef.current, {
367
- 'transform': isVertical(direction)
368
- ? `translate3d(0, ${snapPointsOffset[0]}px, 0)`
369
- : `translate3d(${snapPointsOffset[0]}px, 0, 0)`,
363
+ 'transform': getSnapPointTransform(direction),
370
364
  '--snap-point-height': `${snapPointsOffset[0]}px`,
371
365
  });
372
366
  setDetachedWrapperTransform(excess, false);
@@ -378,9 +372,7 @@ export function useSnapPoints({
378
372
  // derived from it (e.g. the scrollable fill) resize with the drawer
379
373
  // instead of staying cut off at the last detent's visible height.
380
374
  set(drawerRef.current, {
381
- 'transform': isVertical(direction)
382
- ? `translate3d(0, ${newValue}px, 0)`
383
- : `translate3d(${newValue}px, 0, 0)`,
375
+ 'transform': getSnapPointTransform(direction),
384
376
  '--snap-point-height': `${newValue}px`,
385
377
  });
386
378
  }