@jobber/components 9.6.0 → 9.6.2

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 (187) hide show
  1. package/dist/ActivityIndicator-cjs.js +1 -0
  2. package/dist/ActivityIndicator-es.js +1 -0
  3. package/dist/Autocomplete/index.cjs +2 -0
  4. package/dist/Autocomplete/index.mjs +2 -0
  5. package/dist/Autocomplete-cjs.js +6 -4
  6. package/dist/Autocomplete-es.js +3 -1
  7. package/dist/Banner-cjs.js +1 -0
  8. package/dist/Banner-es.js +1 -0
  9. package/dist/Card/index.cjs +2 -1
  10. package/dist/Card/index.mjs +2 -1
  11. package/dist/Chip/index.cjs +2 -0
  12. package/dist/Chip/index.mjs +2 -0
  13. package/dist/Chip-cjs.js +1 -0
  14. package/dist/Chip-es.js +1 -0
  15. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +2 -0
  16. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +2 -0
  17. package/dist/Chips/InternalChipDismissible/index.cjs +2 -0
  18. package/dist/Chips/InternalChipDismissible/index.mjs +2 -0
  19. package/dist/Chips/index.cjs +2 -0
  20. package/dist/Chips/index.mjs +2 -0
  21. package/dist/Combobox/index.cjs +4 -2
  22. package/dist/Combobox/index.mjs +4 -2
  23. package/dist/{ComboboxSeparator-cjs.js → ComboboxChipRemove-cjs.js} +455 -501
  24. package/dist/{ComboboxSeparator-es.js → ComboboxChipRemove-es.js} +456 -500
  25. package/dist/ComboboxPrimitive-cjs.js +30 -29
  26. package/dist/ComboboxPrimitive-es.js +3 -2
  27. package/dist/ConfirmationModal/index.cjs +2 -0
  28. package/dist/ConfirmationModal/index.mjs +2 -0
  29. package/dist/ConfirmationModal-cjs.js +1 -0
  30. package/dist/ConfirmationModal-es.js +1 -0
  31. package/dist/DataDump/index.cjs +2 -1
  32. package/dist/DataDump/index.mjs +2 -1
  33. package/dist/DataDump-cjs.js +1 -0
  34. package/dist/DataDump-es.js +1 -0
  35. package/dist/DataList/components/DataListActions/index.cjs +2 -0
  36. package/dist/DataList/components/DataListActions/index.mjs +2 -0
  37. package/dist/DataList/components/DataListBulkActions/index.cjs +2 -0
  38. package/dist/DataList/components/DataListBulkActions/index.mjs +2 -0
  39. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +2 -0
  40. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +2 -0
  41. package/dist/DataList/components/DataListFilters/index.cjs +2 -0
  42. package/dist/DataList/components/DataListFilters/index.mjs +2 -0
  43. package/dist/DataList/components/DataListHeader/index.cjs +2 -0
  44. package/dist/DataList/components/DataListHeader/index.mjs +2 -0
  45. package/dist/DataList/components/DataListItem/index.cjs +2 -0
  46. package/dist/DataList/components/DataListItem/index.mjs +2 -0
  47. package/dist/DataList/components/DataListItemActions/index.cjs +2 -0
  48. package/dist/DataList/components/DataListItemActions/index.mjs +2 -0
  49. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +2 -0
  50. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +2 -0
  51. package/dist/DataList/components/DataListItems/index.cjs +2 -0
  52. package/dist/DataList/components/DataListItems/index.mjs +2 -0
  53. package/dist/DataList/components/DataListLayout/index.cjs +2 -0
  54. package/dist/DataList/components/DataListLayout/index.mjs +2 -0
  55. package/dist/DataList/components/DataListLayoutActions/index.cjs +2 -0
  56. package/dist/DataList/components/DataListLayoutActions/index.mjs +2 -0
  57. package/dist/DataList/index.cjs +2 -0
  58. package/dist/DataList/index.mjs +2 -0
  59. package/dist/DataTable-cjs.js +1 -0
  60. package/dist/DataTable-es.js +1 -0
  61. package/dist/DatePicker/index.cjs +2 -0
  62. package/dist/DatePicker/index.mjs +2 -0
  63. package/dist/DatePicker-cjs.js +8 -7
  64. package/dist/DatePicker-es.js +6 -5
  65. package/dist/Dialog/index.cjs +2 -1
  66. package/dist/Dialog/index.mjs +2 -1
  67. package/dist/DrawerDescription-cjs.js +99 -268
  68. package/dist/DrawerDescription-es.js +103 -270
  69. package/dist/DrawerGrid-cjs.js +1 -0
  70. package/dist/DrawerGrid-es.js +1 -0
  71. package/dist/DrawerTitle-cjs.js +6 -2
  72. package/dist/DrawerTitle-es.js +7 -3
  73. package/dist/FeatureSwitch-cjs.js +1 -0
  74. package/dist/FeatureSwitch-es.js +1 -0
  75. package/dist/FieldDescription-cjs.js +181 -184
  76. package/dist/FieldDescription-es.js +184 -187
  77. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +2 -0
  78. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +2 -0
  79. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +2 -0
  80. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +2 -0
  81. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +2 -0
  82. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +2 -0
  83. package/dist/FilterPicker/index.cjs +2 -0
  84. package/dist/FilterPicker/index.mjs +2 -0
  85. package/dist/FilterPickerContent-cjs.js +5 -4
  86. package/dist/FilterPickerContent-es.js +2 -1
  87. package/dist/FormFieldWrapper-cjs.js +1 -0
  88. package/dist/FormFieldWrapper-es.js +1 -0
  89. package/dist/FormatFile/index.cjs +2 -0
  90. package/dist/FormatFile/index.mjs +2 -0
  91. package/dist/FormatFile-cjs.js +1 -0
  92. package/dist/FormatFile-es.js +1 -0
  93. package/dist/Gallery/index.cjs +2 -0
  94. package/dist/Gallery/index.mjs +2 -0
  95. package/dist/InputAvatar-cjs.js +1 -0
  96. package/dist/InputAvatar-es.js +1 -0
  97. package/dist/InputDate/index.cjs +2 -0
  98. package/dist/InputDate/index.mjs +2 -0
  99. package/dist/InputDate-cjs.js +1 -0
  100. package/dist/InputDate-es.js +1 -0
  101. package/dist/InputNumber/index.cjs +2 -1
  102. package/dist/InputNumber/index.mjs +2 -1
  103. package/dist/InternalChipDismissible-cjs.js +1 -0
  104. package/dist/InternalChipDismissible-es.js +1 -0
  105. package/dist/LightBox/index.cjs +2 -0
  106. package/dist/LightBox/index.mjs +2 -0
  107. package/dist/LightBox-cjs.js +1 -0
  108. package/dist/LightBox-es.js +1 -0
  109. package/dist/Menu/index.cjs +2 -1
  110. package/dist/Menu/index.mjs +2 -1
  111. package/dist/Menu-cjs.js +6 -4
  112. package/dist/Menu-es.js +3 -1
  113. package/dist/MenuSubmenuTrigger-cjs.js +164 -206
  114. package/dist/MenuSubmenuTrigger-es.js +152 -194
  115. package/dist/Modal/index.cjs +3 -1
  116. package/dist/Modal/index.mjs +4 -2
  117. package/dist/NumberFieldInput-cjs.js +308 -187
  118. package/dist/NumberFieldInput-es.js +308 -187
  119. package/dist/Page/index.cjs +2 -1
  120. package/dist/Page/index.mjs +2 -1
  121. package/dist/Page-cjs.js +1 -0
  122. package/dist/Page-es.js +1 -0
  123. package/dist/Popover/index.cjs +2 -0
  124. package/dist/Popover/index.mjs +2 -0
  125. package/dist/Popover-cjs.js +9 -7
  126. package/dist/Popover-es.js +3 -1
  127. package/dist/ProgressBar-cjs.js +1 -0
  128. package/dist/ProgressBar-es.js +1 -0
  129. package/dist/ScrollAreaViewport-cjs.js +1093 -747
  130. package/dist/ScrollAreaViewport-es.js +1081 -734
  131. package/dist/Select/index.cjs +2 -1
  132. package/dist/Select/index.mjs +2 -1
  133. package/dist/SelectGroupLabel-cjs.js +306 -207
  134. package/dist/SelectGroupLabel-es.js +302 -203
  135. package/dist/SideDrawer-cjs.js +1 -0
  136. package/dist/SideDrawer-es.js +1 -0
  137. package/dist/Spinner-cjs.js +1 -0
  138. package/dist/Spinner-es.js +1 -0
  139. package/dist/Tooltip/index.cjs +2 -0
  140. package/dist/Tooltip/index.mjs +2 -0
  141. package/dist/Tooltip-cjs.js +7 -6
  142. package/dist/Tooltip-es.js +2 -1
  143. package/dist/dialogReturnFocus-cjs.js +1 -1
  144. package/dist/dialogReturnFocus-es.js +2 -2
  145. package/dist/floating-ui.react-cjs.js +192 -2564
  146. package/dist/floating-ui.react-dom-cjs.js +2103 -0
  147. package/dist/floating-ui.react-dom-es.js +2072 -0
  148. package/dist/floating-ui.react-es.js +156 -2519
  149. package/dist/floating-ui.utils-cjs.js +342 -0
  150. package/dist/floating-ui.utils-es.js +300 -0
  151. package/dist/index.cjs +4 -3
  152. package/dist/index.mjs +4 -3
  153. package/dist/primitives/BottomSheet/index.cjs +1 -1
  154. package/dist/primitives/BottomSheet/index.mjs +1 -1
  155. package/dist/primitives/ComboboxPrimitive/index.cjs +4 -2
  156. package/dist/primitives/ComboboxPrimitive/index.mjs +4 -2
  157. package/dist/primitives/SelectPrimitive/index.cjs +2 -0
  158. package/dist/primitives/SelectPrimitive/index.mjs +2 -0
  159. package/dist/primitives/index.cjs +3 -2
  160. package/dist/primitives/index.mjs +3 -2
  161. package/dist/scrollEdges-cjs.js +11 -1
  162. package/dist/scrollEdges-es.js +11 -1
  163. package/dist/sharedHelpers/i18n/index.cjs +1 -0
  164. package/dist/sharedHelpers/i18n/index.d.ts +1 -0
  165. package/dist/sharedHelpers/i18n/index.mjs +1 -1
  166. package/dist/sharedHelpers/i18n/releasedLanguages.d.ts +12 -0
  167. package/dist/sharedHelpers/i18n/useReleasedLocale.d.ts +6 -0
  168. package/dist/showToast-cjs.js +1 -1
  169. package/dist/showToast-es.js +1 -1
  170. package/dist/testUtils/releasedLanguages.d.ts +16 -0
  171. package/dist/unstyledPrimitives/index.cjs +478 -720
  172. package/dist/unstyledPrimitives/index.mjs +421 -663
  173. package/dist/useAtlantisI18n-cjs.js +44 -11
  174. package/dist/useAtlantisI18n-es.js +44 -12
  175. package/dist/useCompositeListItem-cjs.js +361 -2108
  176. package/dist/useCompositeListItem-es.js +331 -2076
  177. package/dist/useLabel-cjs.js +3 -2
  178. package/dist/useLabel-es.js +3 -2
  179. package/dist/useRenderElement-cjs.js +7 -9
  180. package/dist/useRenderElement-es.js +7 -9
  181. package/dist/useScrollLock-cjs.js +344 -819
  182. package/dist/useScrollLock-es.js +296 -732
  183. package/dist/useScrollToActive-cjs.js +5 -4
  184. package/dist/useScrollToActive-es.js +2 -1
  185. package/package.json +3 -3
  186. package/dist/scrollable-cjs.js +0 -58
  187. package/dist/scrollable-es.js +0 -54
@@ -1,10 +1,10 @@
1
1
  import * as React from 'react';
2
- import { E as EMPTY_OBJECT, N as NOOP, f as formatErrorMessage, u as useRenderElement, m as mergeProps } from './useRenderElement-es.js';
3
- import { aK as FloatingRootStore, aL as PopupTriggerMap, b3 as shimExports, a6 as popupStateMapping, t as transitionStatusMapping, a2 as FloatingPortal, a3 as InternalBackdrop, a4 as inertValue, W as useDismiss, e as getTarget, a0 as contains, b2 as useScrollLock, c as useIsoLayoutEffect, b4 as usePopupInteractionProps, X as ReactStore, b5 as setPopupOpenState, b6 as usePopupRootStore, b7 as usePopupRootSync, b8 as useImplicitActiveTrigger, b9 as useOpenStateTransitions, h as createChangeEventDetails, k as imperativeAction, ba as PopupHandleAttachment, u as useBaseUiId, bb as useTriggerDataForwarding, f as useButton, V as useClick, bc as useOpenMethodTriggerProps, bd as CLICK_TRIGGER_IDENTIFIER, be as triggerOpenStateMapping, p as popupTransitionStateMapping, bf as CommonPopupDataAttributes, d as useStableCallback, o as ownerDocument, S as SafeReact, r as error, l as useOpenChangeComplete, F as FOCUSABLE_POPUP_PROPS, n as FloatingFocusManager, v as useControlled, z as none, bg as android, I as getWindow, y as addEventListener, bh as closeWatcher, a8 as useAnimationFrame, au as isElement, D as activeElement, bi as TransitionStatusDataAttributes, bj as swipe, bk as BASE_UI_SWIPE_IGNORE_SELECTOR, q as useTimeout, a7 as useDirection, a9 as webkit } from './useScrollLock-es.js';
2
+ import { E as EMPTY_OBJECT, f as formatErrorMessage, u as useRenderElement, m as mergeProps } from './useRenderElement-es.js';
3
+ import { ac as FloatingRootStore, ad as PopupTriggerMap, N as createSelector, a0 as FloatingPortal, a1 as InternalBackdrop, a2 as inertValue, b9 as usePopupRootSync, aS as useImplicitActiveTrigger, aT as useOpenStateTransitions, d as createChangeEventDetails, j as imperativeAction, R as useDismiss, g as getTarget, _ as contains, av as useScrollLock, aY as usePopupInteractionProps, aN as ReactStore, aV as setPopupOpenState, ba as usePopupStore, U as useOnFirstRender, C as CommonPopupDataAttributes, p as popupStateMapping, t as transitionStatusMapping, u as useBaseUiId, b4 as useTriggerDataForwarding, c as useButton, Q as useClick, bb as useOpenMethodTriggerProps, bc as CLICK_TRIGGER_IDENTIFIER, b8 as triggerOpenStateMapping, b as useStableCallback, o as ownerDocument, a as useIsoLayoutEffect, S as SafeReact, l as error, f as useOpenChangeComplete, F as FOCUSABLE_POPUP_PROPS, i as FloatingFocusManager, n as useControlled, v as none, aa as useId, bd as android, s as addEventListener, be as closeWatcher, a5 as useAnimationFrame, bf as BASE_UI_SWIPE_IGNORE_SELECTOR, D as activeElement, bg as TransitionStatusDataAttributes, bh as swipe, k as useTimeout, a4 as useDirection, a6 as webkit } from './useScrollLock-es.js';
4
4
  import { c as clamp, C as COMPOSITE_KEYS, u as useCSPContext, s as styleDisableScrollbar, n as normalizeScrollOffset } from './scrollEdges-es.js';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
6
+ import { K as getWindow, i as isHTMLElement, d as isLastTraversableNode, e as getParentNode, h as getComputedStyle$1, b as isElement } from './floating-ui.utils-es.js';
6
7
  import * as ReactDOM from 'react-dom';
7
- import { f as findScrollableTouchTarget, h as hasScrollableAncestor } from './scrollable-es.js';
8
8
 
9
9
  function getEmptyRootContext() {
10
10
  return new FloatingRootStore({
@@ -58,12 +58,12 @@ function createPopupFloatingRootContext(triggerElements, floatingId, nested = fa
58
58
  onOpenChange: undefined
59
59
  });
60
60
  }
61
- const activeTriggerIdSelector = state => state.triggerIdProp ?? state.activeTriggerId;
62
- const openSelector = state => state.openProp ?? state.open;
63
- const popupIdSelector = state => {
61
+ const activeTriggerIdSelector = createSelector(state => state.triggerIdProp ?? state.activeTriggerId);
62
+ const openSelector = createSelector(state => state.openProp ?? state.open);
63
+ const popupIdSelector = createSelector(state => {
64
64
  const popupId = state.popupElement?.id ?? state.floatingId;
65
65
  return popupId || undefined;
66
- };
66
+ });
67
67
  function triggerOwnsOpenPopup(state, triggerId) {
68
68
  return triggerId !== undefined && openSelector(state) && activeTriggerIdSelector(state) === triggerId;
69
69
  }
@@ -75,81 +75,47 @@ function triggerOwnsOpenPopupOrIsOnlyTrigger(state, triggerId) {
75
75
  }
76
76
  const popupStoreSelectors = {
77
77
  open: openSelector,
78
- mounted: state => state.mounted,
79
- transitionStatus: state => state.transitionStatus,
80
- floatingRootContext: state => state.floatingRootContext,
81
- triggerCount: state => state.triggerCount,
82
- preventUnmountingOnClose: state => state.preventUnmountingOnClose,
83
- payload: state => state.payload,
78
+ mounted: createSelector(state => state.mounted),
79
+ transitionStatus: createSelector(state => state.transitionStatus),
80
+ floatingRootContext: createSelector(state => state.floatingRootContext),
81
+ triggerCount: createSelector(state => state.triggerCount),
82
+ preventUnmountingOnClose: createSelector(state => state.preventUnmountingOnClose),
83
+ payload: createSelector(state => state.payload),
84
84
  activeTriggerId: activeTriggerIdSelector,
85
- activeTriggerElement: state => state.mounted ? state.activeTriggerElement : null,
85
+ activeTriggerElement: createSelector(state => state.mounted ? state.activeTriggerElement : null),
86
86
  popupId: popupIdSelector,
87
87
  /**
88
88
  * Whether the trigger with the given ID was used to open the popup.
89
89
  */
90
- isTriggerActive: (state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId,
90
+ isTriggerActive: createSelector((state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId),
91
91
  /**
92
92
  * Whether the popup is open and was activated by a trigger with the given ID.
93
93
  */
94
- isOpenedByTrigger: (state, triggerId) => triggerOwnsOpenPopup(state, triggerId),
94
+ isOpenedByTrigger: createSelector((state, triggerId) => triggerOwnsOpenPopup(state, triggerId)),
95
95
  /**
96
96
  * Whether the popup is mounted and was activated by a trigger with the given ID.
97
97
  */
98
- isMountedByTrigger: (state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId && state.mounted,
99
- triggerProps: (state, isActive) => isActive ? state.activeTriggerProps : state.inactiveTriggerProps,
98
+ isMountedByTrigger: createSelector((state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId && state.mounted),
99
+ triggerProps: createSelector((state, isActive) => isActive ? state.activeTriggerProps : state.inactiveTriggerProps),
100
100
  /**
101
101
  * Popup id for the trigger that currently owns the open popup.
102
102
  */
103
- triggerPopupId: (state, triggerId) => triggerOwnsOpenPopupOrIsOnlyTrigger(state, triggerId) ? popupIdSelector(state) : undefined,
104
- popupProps: state => state.popupProps,
105
- popupElement: state => state.popupElement,
106
- positionerElement: state => state.positionerElement
103
+ triggerPopupId: createSelector((state, triggerId) => triggerOwnsOpenPopupOrIsOnlyTrigger(state, triggerId) ? popupIdSelector(state) : undefined),
104
+ popupProps: createSelector(state => state.popupProps),
105
+ popupElement: createSelector(state => state.popupElement),
106
+ positionerElement: createSelector(state => state.positionerElement)
107
107
  };
108
108
 
109
- /**
110
- * Store members a detached handle-backed trigger reads or invokes for trigger registration and data
111
- * forwarding. `set`/`update` are included only for trigger-count and trigger-data bookkeeping; on a
112
- * detached (inert) store they are intentionally no-ops, so a write through them is not guaranteed to
113
- * be durable. Component handle-store views Pick these from their concrete store (preserving its
114
- * context and selectors) and add any component-specific trigger-invoked members such as `setOpen`.
115
- */
116
-
117
- /**
118
- * The subset of a popup store that trigger registration and data forwarding rely on. Narrow enough
119
- * that an inert store can be passed while detached.
120
- */
121
-
122
- /**
123
- * Reads the store currently exposed by a popup handle and subscribes to store-pointer changes.
124
- * Detached triggers use this to follow a handle as a root attaches or detaches: while no root is
125
- * attached, the handle exposes its fallback store; once a root attaches, subscribers re-render and
126
- * read from the live root store.
127
- *
128
- * Returns `undefined` when no handle is provided so callers can fall back to their root context.
129
- *
130
- * @param handle The popup handle to read from, or `undefined` when the trigger is not handle-bound.
131
- */
132
- function usePopupHandleStore(handle) {
133
- const subscribe = React.useCallback(listener => {
134
- if (handle === undefined) {
135
- return NOOP;
136
- }
137
- return handle.subscribeStore(listener);
138
- }, [handle]);
139
- const getSnapshot = React.useCallback(() => {
140
- return handle === undefined ? undefined : handle.store;
141
- }, [handle]);
142
- return shimExports.useSyncExternalStore(subscribe, getSnapshot, () => handle?.serverStore);
143
- }
144
-
109
+ const IsDrawerContext = /*#__PURE__*/React.createContext(false);
110
+ if (process.env.NODE_ENV !== "production") IsDrawerContext.displayName = "IsDrawerContext";
145
111
  const DialogRootContext = /*#__PURE__*/React.createContext(undefined);
146
112
  if (process.env.NODE_ENV !== "production") DialogRootContext.displayName = "DialogRootContext";
147
113
  function useDialogRootContext(optional) {
148
- const store = React.useContext(DialogRootContext);
149
- if (!optional && store === undefined) {
114
+ const dialogRootContext = React.useContext(DialogRootContext);
115
+ if (optional === false && dialogRootContext === undefined) {
150
116
  throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: DialogRootContext is missing. Dialog parts must be placed within <Dialog.Root>.' : formatErrorMessage(27));
151
117
  }
152
- return store;
118
+ return dialogRootContext;
153
119
  }
154
120
 
155
121
  const DialogPortalContext = /*#__PURE__*/React.createContext(undefined);
@@ -162,26 +128,14 @@ function useDialogPortalContext() {
162
128
  return value;
163
129
  }
164
130
 
165
- /**
166
- * Shared by `Dialog.Popup` and `Dialog.Viewport`, whose states have the same shape.
167
- * `nested` is not mapped: unmapped `true` booleans already render as `data-nested`.
168
- */
169
- const dialogStateAttributesMapping = {
170
- ...popupStateMapping,
171
- ...transitionStatusMapping,
172
- nestedDialogOpen(value) {
173
- return value ? {
174
- 'data-nested-dialog-open': ''
175
- } : null;
176
- }
177
- };
178
-
179
131
  const DialogPortal = /*#__PURE__*/React.forwardRef(function DialogPortal(props, forwardedRef) {
180
132
  const {
181
133
  keepMounted = false,
182
134
  ...portalProps
183
135
  } = props;
184
- const store = useDialogRootContext();
136
+ const {
137
+ store
138
+ } = useDialogRootContext();
185
139
  const mounted = store.useState('mounted');
186
140
  const modal = store.useState('modal');
187
141
  const open = store.useState('open');
@@ -203,6 +157,25 @@ const DialogPortal = /*#__PURE__*/React.forwardRef(function DialogPortal(props,
203
157
  });
204
158
  if (process.env.NODE_ENV !== "production") DialogPortal.displayName = "DialogPortal";
205
159
 
160
+ function useDialogRoot(params) {
161
+ const {
162
+ store,
163
+ actionsRef
164
+ } = params;
165
+ const open = store.useState('open');
166
+ usePopupRootSync(store, open);
167
+ useImplicitActiveTrigger(store);
168
+ const {
169
+ forceUnmount
170
+ } = useOpenStateTransitions(open, store);
171
+ const handleImperativeClose = React.useCallback(() => {
172
+ store.setOpen(false, createChangeEventDetails(imperativeAction));
173
+ }, [store]);
174
+ React.useImperativeHandle(actionsRef, () => ({
175
+ unmount: forceUnmount,
176
+ close: handleImperativeClose
177
+ }), [forceUnmount, handleImperativeClose]);
178
+ }
206
179
  function DialogInteractions({
207
180
  store,
208
181
  parentContext,
@@ -238,18 +211,8 @@ function DialogInteractions({
238
211
  if ('button' in event && event.button !== 0) {
239
212
  return false;
240
213
  }
241
- if ('touches' in event) {
242
- // Outside press can be handled on `touchend`, where the lifted point is
243
- // reported in `changedTouches` and `touches` contains any remaining
244
- // active points. Treat it as a single-finger tap only when exactly one
245
- // touch ended and no other fingers are still down.
246
- if (event.type === 'touchend') {
247
- if (event.changedTouches.length !== 1 || event.touches.length !== 0) {
248
- return false;
249
- }
250
- } else if (event.touches.length !== 1) {
251
- return false;
252
- }
214
+ if ('touches' in event && event.touches.length !== 1) {
215
+ return false;
253
216
  }
254
217
  const target = getTarget(event);
255
218
  if (isTopmost && !disablePointerDismissal) {
@@ -257,9 +220,7 @@ function DialogInteractions({
257
220
  // This supports multiple modal dialogs that aren't nested in the React tree:
258
221
  // https://github.com/mui/base-ui/issues/1320
259
222
  if (modal) {
260
- const internalBackdrop = store.context.internalBackdropRef.current;
261
- const backdrop = store.context.backdropRef.current;
262
- return internalBackdrop || backdrop ? internalBackdrop === target || backdrop === target || contains(target, popupElement) && !target?.hasAttribute('data-base-ui-portal') : true;
223
+ return store.context.internalBackdropRef.current || store.context.backdropRef.current ? store.context.internalBackdropRef.current === target || store.context.backdropRef.current === target || contains(target, popupElement) && !target?.hasAttribute('data-base-ui-portal') : true;
263
224
  }
264
225
  return true;
265
226
  }
@@ -270,82 +231,73 @@ function DialogInteractions({
270
231
  useScrollLock(open && modal === true, popupElement);
271
232
 
272
233
  // Listen for nested open/close events on this store to maintain the counts.
273
- // A close notification is an open notification with zeroed counts.
274
234
  store.useContextCallback('onNestedDialogOpen', (dialogCount, drawerCount) => {
275
235
  setOwnNestedOpenDialogs(dialogCount);
276
236
  setOwnNestedOpenDrawers(drawerCount);
277
237
  });
238
+ store.useContextCallback('onNestedDialogClose', () => {
239
+ setOwnNestedOpenDialogs(0);
240
+ setOwnNestedOpenDrawers(0);
241
+ });
278
242
 
279
243
  // Notify parent of our open/close state using parent callbacks, if any
280
- useIsoLayoutEffect(() => {
281
- if (parentContext?.onNestedDialogOpen) {
282
- if (open) {
283
- parentContext.onNestedDialogOpen(ownNestedOpenDialogs + 1, ownNestedOpenDrawers + (isDrawer ? 1 : 0));
284
- } else {
285
- parentContext.onNestedDialogOpen(0, 0);
286
- }
244
+ React.useEffect(() => {
245
+ if (parentContext?.onNestedDialogOpen && open) {
246
+ parentContext.onNestedDialogOpen(ownNestedOpenDialogs + 1, ownNestedOpenDrawers + (isDrawer ? 1 : 0));
247
+ }
248
+ if (parentContext?.onNestedDialogClose && !open) {
249
+ parentContext.onNestedDialogClose();
287
250
  }
288
251
  return () => {
289
- if (parentContext?.onNestedDialogOpen && open) {
290
- parentContext.onNestedDialogOpen(0, 0);
252
+ if (parentContext?.onNestedDialogClose && open) {
253
+ parentContext.onNestedDialogClose();
291
254
  }
292
255
  };
293
256
  }, [isDrawer, open, ownNestedOpenDialogs, ownNestedOpenDrawers, parentContext]);
257
+ const activeTriggerProps = dismiss.reference ?? EMPTY_OBJECT;
258
+ const inactiveTriggerProps = dismiss.trigger ?? EMPTY_OBJECT;
259
+ // Consumers (DialogPopup/DrawerPopup) already spread `FOCUSABLE_POPUP_PROPS`
260
+ // directly, so the popup props only need to carry the dismiss handlers.
261
+ const popupProps = dismiss.floating ?? EMPTY_OBJECT;
294
262
  usePopupInteractionProps(store, {
295
- // `enabled` is not passed to `useDismiss`, so its props are always defined,
296
- // and `trigger` is the same object as `reference`.
297
- activeTriggerProps: dismiss.reference,
298
- inactiveTriggerProps: dismiss.trigger,
299
- // DialogPopup and DrawerPopup spread `FOCUSABLE_POPUP_PROPS` directly, so
300
- // this only needs to carry the dismiss handlers.
301
- popupProps: dismiss.floating,
263
+ activeTriggerProps,
264
+ inactiveTriggerProps,
265
+ popupProps,
302
266
  nestedOpenDialogCount: ownNestedOpenDialogs,
303
267
  nestedOpenDrawerCount: ownNestedOpenDrawers
304
268
  });
305
269
  return null;
306
270
  }
307
271
 
308
- /**
309
- * A `ReactStore` whose state never changes.
310
- *
311
- * Useful for fallback stores that need to support normal store reads while detached from the
312
- * component that owns real state. Context values may still contain mutable refs or maps.
313
- */
314
- class NullStore extends ReactStore {
315
- // `update`/`set`/`notifyAll` funnel through `setState` in the base `Store`, so overriding
316
- // `setState` alone would neutralize them today. They are overridden explicitly so the store stays
317
- // inert even if a future base-class change stops routing a mutator through `setState`.
318
- setState(_newState) {}
319
- update(_changes) {}
320
- set(_key, _value) {}
321
- notifyAll() {}
322
- }
323
-
324
272
  const selectors = {
325
273
  ...popupStoreSelectors,
326
- modal: state => state.modal,
327
- nested: state => state.nested,
328
- nestedOpenDialogCount: state => state.nestedOpenDialogCount,
329
- nestedOpenDrawerCount: state => state.nestedOpenDrawerCount,
330
- disablePointerDismissal: state => state.disablePointerDismissal,
331
- openMethod: state => state.openMethod,
332
- descriptionElementId: state => state.descriptionElementId,
333
- titleElementId: state => state.titleElementId,
334
- viewportElement: state => state.viewportElement,
335
- role: state => state.role
274
+ modal: createSelector(state => state.modal),
275
+ nested: createSelector(state => state.nested),
276
+ nestedOpenDialogCount: createSelector(state => state.nestedOpenDialogCount),
277
+ nestedOpenDrawerCount: createSelector(state => state.nestedOpenDrawerCount),
278
+ disablePointerDismissal: createSelector(state => state.disablePointerDismissal),
279
+ openMethod: createSelector(state => state.openMethod),
280
+ descriptionElementId: createSelector(state => state.descriptionElementId),
281
+ titleElementId: createSelector(state => state.titleElementId),
282
+ viewportElement: createSelector(state => state.viewportElement),
283
+ role: createSelector(state => state.role)
336
284
  };
337
-
338
- /**
339
- * The subset of `DialogStore` that detached handle-backed triggers rely on. Both the real
340
- * `DialogStore` and the inert fallback store satisfy it, so a trigger can read from whichever
341
- * store the handle currently exposes.
342
- */
343
-
344
285
  class DialogStore extends ReactStore {
345
- constructor(initialState, floatingId, nested) {
286
+ constructor(initialState, floatingId, nested = false) {
346
287
  const triggerElements = new PopupTriggerMap();
347
- const state = createInitialState(initialState, triggerElements, floatingId, nested);
348
- super(state, createInitialContext(triggerElements), selectors);
288
+ const state = createInitialState(initialState);
289
+ state.floatingRootContext = createPopupFloatingRootContext(triggerElements, floatingId, nested);
290
+ super(state, {
291
+ popupRef: /*#__PURE__*/React.createRef(),
292
+ backdropRef: /*#__PURE__*/React.createRef(),
293
+ internalBackdropRef: /*#__PURE__*/React.createRef(),
294
+ outsidePressEnabledRef: {
295
+ current: true
296
+ },
297
+ triggerElements,
298
+ onOpenChange: undefined,
299
+ onOpenChangeComplete: undefined
300
+ }, selectors);
349
301
  }
350
302
  setOpen = (nextOpen, eventDetails) => {
351
303
  eventDetails.preventUnmountOnClose = () => {
@@ -367,22 +319,20 @@ class DialogStore extends ReactStore {
367
319
  setPopupOpenState(updatedState, nextOpen, eventDetails.trigger);
368
320
  this.update(updatedState);
369
321
  };
370
- }
322
+ static useStore(externalStore, initialState) {
323
+ /* eslint-disable react-hooks/rules-of-hooks */
324
+ const store = usePopupStore(externalStore, (floatingId, nested) => new DialogStore(initialState, floatingId, nested), true).store;
325
+ /* eslint-enable react-hooks/rules-of-hooks */
371
326
 
372
- /**
373
- * Creates the inert fallback store used by detached handle-backed triggers while no
374
- * `Dialog.Root` is attached. It preserves a dialog-specific trigger registry in context so
375
- * detached triggers can register before migrating to the live root store.
376
- */
377
- function createNullDialogStore() {
378
- const triggerElements = new PopupTriggerMap();
379
- return new NullStore(Object.freeze(createInitialState(undefined, triggerElements)), Object.freeze(createInitialContext(triggerElements)), selectors);
327
+ return store;
328
+ }
380
329
  }
381
- function createInitialState(initialState, triggerElements, floatingId, nested = false) {
382
- const state = {
330
+ function createInitialState(initialState = {}) {
331
+ return {
383
332
  ...createInitialPopupStoreState(),
384
333
  modal: true,
385
334
  disablePointerDismissal: false,
335
+ popupElement: null,
386
336
  viewportElement: null,
387
337
  descriptionElementId: undefined,
388
338
  titleElementId: undefined,
@@ -393,24 +343,9 @@ function createInitialState(initialState, triggerElements, floatingId, nested =
393
343
  role: 'dialog',
394
344
  ...initialState
395
345
  };
396
- state.floatingRootContext = createPopupFloatingRootContext(triggerElements, floatingId, nested);
397
- return state;
398
- }
399
- function createInitialContext(triggerElements) {
400
- return {
401
- popupRef: /*#__PURE__*/React.createRef(),
402
- backdropRef: /*#__PURE__*/React.createRef(),
403
- internalBackdropRef: /*#__PURE__*/React.createRef(),
404
- outsidePressEnabledRef: {
405
- current: true
406
- },
407
- triggerElements,
408
- onOpenChange: undefined,
409
- onOpenChangeComplete: undefined
410
- };
411
346
  }
412
347
 
413
- function useRenderDialogRoot(mode, props) {
348
+ function useRenderDialogRoot(props, mode = 'dialog') {
414
349
  const {
415
350
  children,
416
351
  open: openProp,
@@ -429,26 +364,38 @@ function useRenderDialogRoot(mode, props) {
429
364
  const modal = isAlertDialog ? true : modalProp;
430
365
  const disablePointerDismissal = isAlertDialog || disablePointerDismissalProp;
431
366
  const role = isAlertDialog ? 'alertdialog' : 'dialog';
432
- const parentStore = useDialogRootContext(true);
433
- const nested = parentStore != null;
367
+ const parentDialogRootContext = useDialogRootContext(true);
368
+ const nested = Boolean(parentDialogRootContext);
434
369
  const rootState = {
435
370
  modal,
436
371
  disablePointerDismissal,
437
372
  nested,
438
373
  role
439
374
  };
440
-
441
- // The store is owned by this Root instance and created exactly once. It is not tied to the handle:
442
- // the handle attaches to it, so swapping the handle re-attaches rather than recreating state.
443
- // Default values are only initial values; controlled values and root state are synced after creation.
444
- // Dialogs pass the popup element to Floating UI as the floating element (`treatPopupAsFloatingElement`).
445
- const store = usePopupRootStore((floatingId, floatingNested) => new DialogStore({
375
+ const store = DialogStore.useStore(handle?.store, {
446
376
  open: defaultOpen,
447
377
  openProp,
448
378
  activeTriggerId: defaultTriggerIdProp,
449
379
  triggerIdProp,
450
380
  ...rootState
451
- }, floatingId, floatingNested), true);
381
+ });
382
+
383
+ // Support initially open state when uncontrolled
384
+ useOnFirstRender(() => {
385
+ const nextState = openProp === undefined && store.state.open === false && defaultOpen === true ? {
386
+ open: true,
387
+ activeTriggerId: defaultTriggerIdProp
388
+ } : null;
389
+ if (isAlertDialog) {
390
+ // Handles can reuse plain Dialog stores; alert dialog invariants must exist immediately.
391
+ store.update(nextState ? {
392
+ ...rootState,
393
+ ...nextState
394
+ } : rootState);
395
+ } else if (nextState) {
396
+ store.update(nextState);
397
+ }
398
+ });
452
399
  store.useControlledProp('openProp', openProp);
453
400
  store.useControlledProp('triggerIdProp', triggerIdProp);
454
401
  store.useSyncedValues(rootState);
@@ -457,31 +404,83 @@ function useRenderDialogRoot(mode, props) {
457
404
  const open = store.useState('open');
458
405
  const mounted = store.useState('mounted');
459
406
  const payload = store.useState('payload');
460
- usePopupRootSync(store, open);
461
- useImplicitActiveTrigger(store);
462
- const {
463
- forceUnmount
464
- } = useOpenStateTransitions(open, store);
465
- React.useImperativeHandle(actionsRef, () => ({
466
- unmount: forceUnmount,
467
- close: () => store.setOpen(false, createChangeEventDetails(imperativeAction))
468
- }), [forceUnmount, store]);
407
+ useDialogRoot({
408
+ store,
409
+ actionsRef
410
+ });
469
411
  const shouldRenderInteractions = open || mounted;
470
- return /*#__PURE__*/jsxs(DialogRootContext.Provider, {
471
- value: store,
472
- children: [handle && /*#__PURE__*/jsx(PopupHandleAttachment, {
473
- handle: handle,
474
- store: store
475
- }), shouldRenderInteractions && /*#__PURE__*/jsx(DialogInteractions, {
476
- store: store,
477
- parentContext: parentStore?.context,
478
- isDrawer: isDrawer
479
- }), typeof children === 'function' ? children({
480
- payload
481
- }) : children]
412
+ const contextValue = React.useMemo(() => ({
413
+ store
414
+ }), [store]);
415
+ return /*#__PURE__*/jsx(IsDrawerContext.Provider, {
416
+ value: false,
417
+ children: /*#__PURE__*/jsxs(DialogRootContext.Provider, {
418
+ value: contextValue,
419
+ children: [shouldRenderInteractions && /*#__PURE__*/jsx(DialogInteractions, {
420
+ store: store,
421
+ parentContext: parentDialogRootContext?.store.context,
422
+ isDrawer: isDrawer
423
+ }), typeof children === 'function' ? children({
424
+ payload
425
+ }) : children]
426
+ })
482
427
  });
483
428
  }
484
429
 
430
+ /**
431
+ * Groups all parts of the dialog.
432
+ * Doesn't render its own HTML element.
433
+ *
434
+ * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog)
435
+ */
436
+ function DialogRoot(props) {
437
+ const mode = React.useContext(IsDrawerContext) ? 'drawer' : 'dialog';
438
+ return useRenderDialogRoot(props, mode);
439
+ }
440
+
441
+ let DialogViewportDataAttributes = function (DialogViewportDataAttributes) {
442
+ /**
443
+ * Present when the dialog is open.
444
+ */
445
+ DialogViewportDataAttributes[DialogViewportDataAttributes["open"] = CommonPopupDataAttributes.open] = "open";
446
+ /**
447
+ * Present when the dialog is closed.
448
+ */
449
+ DialogViewportDataAttributes[DialogViewportDataAttributes["closed"] = CommonPopupDataAttributes.closed] = "closed";
450
+ /**
451
+ * Present when the dialog is animating in.
452
+ */
453
+ DialogViewportDataAttributes[DialogViewportDataAttributes["startingStyle"] = CommonPopupDataAttributes.startingStyle] = "startingStyle";
454
+ /**
455
+ * Present when the dialog is animating out.
456
+ */
457
+ DialogViewportDataAttributes[DialogViewportDataAttributes["endingStyle"] = CommonPopupDataAttributes.endingStyle] = "endingStyle";
458
+ /**
459
+ * Present when the dialog is nested within another dialog.
460
+ */
461
+ DialogViewportDataAttributes["nested"] = "data-nested";
462
+ /**
463
+ * Present when the dialog has other open dialogs nested within it.
464
+ */
465
+ DialogViewportDataAttributes["nestedDialogOpen"] = "data-nested-dialog-open";
466
+ return DialogViewportDataAttributes;
467
+ }({});
468
+
469
+ const stateAttributesMapping$2 = {
470
+ ...popupStateMapping,
471
+ ...transitionStatusMapping,
472
+ nested(value) {
473
+ return value ? {
474
+ [DialogViewportDataAttributes.nested]: ''
475
+ } : null;
476
+ },
477
+ nestedDialogOpen(value) {
478
+ return value ? {
479
+ [DialogViewportDataAttributes.nestedDialogOpen]: ''
480
+ } : null;
481
+ }
482
+ };
483
+
485
484
  /**
486
485
  * A positioning container for the dialog popup that can be made scrollable.
487
486
  * Renders a `<div>` element.
@@ -497,7 +496,9 @@ const DialogViewport = /*#__PURE__*/React.forwardRef(function DialogViewport(com
497
496
  ...elementProps
498
497
  } = componentProps;
499
498
  const keepMounted = useDialogPortalContext();
500
- const store = useDialogRootContext();
499
+ const {
500
+ store
501
+ } = useDialogRootContext();
501
502
  const open = store.useState('open');
502
503
  const nested = store.useState('nested');
503
504
  const transitionStatus = store.useState('transitionStatus');
@@ -516,7 +517,7 @@ const DialogViewport = /*#__PURE__*/React.forwardRef(function DialogViewport(com
516
517
  enabled: shouldRender,
517
518
  state,
518
519
  ref: [forwardedRef, setViewportElement],
519
- stateAttributesMapping: dialogStateAttributesMapping,
520
+ stateAttributesMapping: stateAttributesMapping$2,
520
521
  props: [{
521
522
  role: 'presentation',
522
523
  hidden: !mounted,
@@ -547,9 +548,8 @@ const DialogTrigger = /*#__PURE__*/React.forwardRef(function DialogTrigger(compo
547
548
  handle,
548
549
  ...elementProps
549
550
  } = componentProps;
550
- const dialogRootStore = useDialogRootContext(true);
551
- const handleStore = usePopupHandleStore(handle);
552
- const store = handleStore ?? dialogRootStore;
551
+ const dialogRootContext = useDialogRootContext(true);
552
+ const store = handle?.store ?? dialogRootContext?.store;
553
553
  if (!store) {
554
554
  throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: <Dialog.Trigger> must be used within <Dialog.Root> or provided with a handle.' : formatErrorMessage(79));
555
555
  }
@@ -571,7 +571,9 @@ const DialogTrigger = /*#__PURE__*/React.forwardRef(function DialogTrigger(compo
571
571
  disabled,
572
572
  native: nativeButton
573
573
  });
574
- const click = useClick(floatingContext);
574
+ const click = useClick(floatingContext, {
575
+ enabled: floatingContext != null
576
+ });
575
577
  const interactionTypeProps = useOpenMethodTriggerProps(() => store.select('open'), interactionType => {
576
578
  store.set('openMethod', interactionType);
577
579
  });
@@ -643,6 +645,11 @@ let DrawerBackdropCssVars = /*#__PURE__*/function (DrawerBackdropCssVars) {
643
645
  return DrawerBackdropCssVars;
644
646
  }({});
645
647
 
648
+ const stateAttributesMapping$1 = {
649
+ ...popupStateMapping,
650
+ ...transitionStatusMapping
651
+ };
652
+
646
653
  /**
647
654
  * An overlay displayed beneath the popup.
648
655
  * Renders a `<div>` element.
@@ -657,7 +664,9 @@ const DrawerBackdrop = /*#__PURE__*/React.forwardRef(function DrawerBackdrop(com
657
664
  forceRender = false,
658
665
  ...elementProps
659
666
  } = componentProps;
660
- const store = useDialogRootContext();
667
+ const {
668
+ store
669
+ } = useDialogRootContext();
661
670
  const open = store.useState('open');
662
671
  const nested = store.useState('nested');
663
672
  const mounted = store.useState('mounted');
@@ -669,7 +678,7 @@ const DrawerBackdrop = /*#__PURE__*/React.forwardRef(function DrawerBackdrop(com
669
678
  return useRenderElement('div', componentProps, {
670
679
  state,
671
680
  ref: [store.context.backdropRef, forwardedRef],
672
- stateAttributesMapping: popupTransitionStateMapping,
681
+ stateAttributesMapping: stateAttributesMapping$1,
673
682
  props: [{
674
683
  role: 'presentation',
675
684
  hidden: !mounted,
@@ -713,8 +722,9 @@ if (process.env.NODE_ENV !== "production") DrawerContent.displayName = "DrawerCo
713
722
 
714
723
  const DrawerProviderContext = /*#__PURE__*/React.createContext(undefined);
715
724
  if (process.env.NODE_ENV !== "production") DrawerProviderContext.displayName = "DrawerProviderContext";
716
- function useDrawerProviderContext() {
717
- return React.useContext(DrawerProviderContext);
725
+ function useDrawerProviderContext(optional) {
726
+ const context = React.useContext(DrawerProviderContext);
727
+ return context;
718
728
  }
719
729
 
720
730
  let DrawerPopupDataAttributes = function (DrawerPopupDataAttributes) {
@@ -727,7 +737,7 @@ let DrawerPopupDataAttributes = function (DrawerPopupDataAttributes) {
727
737
  */
728
738
  DrawerPopupDataAttributes[DrawerPopupDataAttributes["closed"] = CommonPopupDataAttributes.closed] = "closed";
729
739
  /**
730
- * Present when the drawer begins animating in.
740
+ * Present when the drawer is animating in.
731
741
  */
732
742
  DrawerPopupDataAttributes[DrawerPopupDataAttributes["startingStyle"] = CommonPopupDataAttributes.startingStyle] = "startingStyle";
733
743
  /**
@@ -766,7 +776,7 @@ const DrawerRootContext = /*#__PURE__*/React.createContext(undefined);
766
776
  if (process.env.NODE_ENV !== "production") DrawerRootContext.displayName = "DrawerRootContext";
767
777
  function useDrawerRootContext(optional) {
768
778
  const drawerRootContext = React.useContext(DrawerRootContext);
769
- if (optional !== true && drawerRootContext === undefined) {
779
+ if (optional === false && drawerRootContext === undefined) {
770
780
  throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: DrawerRootContext is missing. Drawer parts must be placed within <Drawer.Root>.' : formatErrorMessage(90));
771
781
  }
772
782
  return drawerRootContext;
@@ -807,24 +817,22 @@ function resolveSnapPointValue(snapPoint, viewportHeight, rootFontSize) {
807
817
  }
808
818
  return null;
809
819
  }
810
-
811
- /**
812
- * Returns the index of the value closest to `target`, or `-1` if `values` is empty.
813
- */
814
- function closestSnapPointIndex(values, target) {
815
- let closestIndex = -1;
820
+ function findClosestSnapPoint(height, points) {
821
+ let closest = null;
816
822
  let closestDistance = Infinity;
817
- for (let index = 0; index < values.length; index += 1) {
818
- const distance = Math.abs(values[index] - target);
823
+ for (const point of points) {
824
+ const distance = Math.abs(point.height - height);
819
825
  if (distance < closestDistance) {
820
826
  closestDistance = distance;
821
- closestIndex = index;
827
+ closest = point;
822
828
  }
823
829
  }
824
- return closestIndex;
830
+ return closest;
825
831
  }
826
832
  function useDrawerSnapPoints() {
827
- const store = useDialogRootContext();
833
+ const {
834
+ store
835
+ } = useDialogRootContext();
828
836
  const {
829
837
  snapPoints,
830
838
  activeSnapPoint,
@@ -837,7 +845,12 @@ function useDrawerSnapPoints() {
837
845
  const measureViewportHeight = useStableCallback(() => {
838
846
  const doc = ownerDocument(viewportElement);
839
847
  const html = doc.documentElement;
840
- setViewportHeight(viewportElement ? viewportElement.offsetHeight : html.clientHeight);
848
+ if (viewportElement) {
849
+ setViewportHeight(viewportElement.offsetHeight);
850
+ }
851
+ if (!viewportElement) {
852
+ setViewportHeight(html.clientHeight);
853
+ }
841
854
  const fontSize = parseFloat(getComputedStyle(html).fontSize);
842
855
  if (Number.isFinite(fontSize)) {
843
856
  setRootFontSize(fontSize);
@@ -859,9 +872,12 @@ function useDrawerSnapPoints() {
859
872
  return [];
860
873
  }
861
874
  const maxHeight = Math.min(popupHeight, viewportHeight);
875
+ if (!Number.isFinite(maxHeight) || maxHeight <= 0) {
876
+ return [];
877
+ }
862
878
  const resolved = snapPoints.map(value => {
863
879
  const resolvedHeight = resolveSnapPointValue(value, viewportHeight, rootFontSize);
864
- if (resolvedHeight === null) {
880
+ if (resolvedHeight === null || !Number.isFinite(resolvedHeight)) {
865
881
  return null;
866
882
  }
867
883
  const clampedHeight = clamp(resolvedHeight, 0, maxHeight);
@@ -889,6 +905,9 @@ function useDrawerSnapPoints() {
889
905
  return deduped;
890
906
  }, [popupHeight, rootFontSize, snapPoints, viewportHeight]);
891
907
  const resolvedActiveSnapPoint = React.useMemo(() => {
908
+ if (activeSnapPoint === undefined) {
909
+ return resolvedSnapPoints[0];
910
+ }
892
911
  if (activeSnapPoint === null) {
893
912
  return undefined;
894
913
  }
@@ -898,11 +917,11 @@ function useDrawerSnapPoints() {
898
917
  }
899
918
  const maxHeight = Math.min(popupHeight, viewportHeight);
900
919
  const resolvedHeight = resolveSnapPointValue(activeSnapPoint, viewportHeight, rootFontSize);
901
- if (resolvedHeight === null) {
920
+ if (resolvedHeight === null || !Number.isFinite(resolvedHeight)) {
902
921
  return undefined;
903
922
  }
904
923
  const clampedHeight = clamp(resolvedHeight, 0, maxHeight);
905
- return resolvedSnapPoints[closestSnapPointIndex(resolvedSnapPoints.map(point => point.height), clampedHeight)];
924
+ return findClosestSnapPoint(clampedHeight, resolvedSnapPoints) ?? undefined;
906
925
  }, [activeSnapPoint, popupHeight, resolvedSnapPoints, rootFontSize, viewportHeight]);
907
926
  return {
908
927
  snapPoints,
@@ -917,8 +936,9 @@ function useDrawerSnapPoints() {
917
936
 
918
937
  const DrawerViewportContext = /*#__PURE__*/React.createContext(null);
919
938
  if (process.env.NODE_ENV !== "production") DrawerViewportContext.displayName = "DrawerViewportContext";
920
- function useDrawerViewportContext() {
921
- return React.useContext(DrawerViewportContext);
939
+ function useDrawerViewportContext(optional) {
940
+ const context = React.useContext(DrawerViewportContext);
941
+ return context;
922
942
  }
923
943
 
924
944
  let drawerSwipeVarsRegistered = false;
@@ -976,7 +996,8 @@ function removeCSSVariableInheritance$1() {
976
996
  drawerSwipeVarsRegistered = true;
977
997
  }
978
998
  const stateAttributesMapping = {
979
- ...popupTransitionStateMapping,
999
+ ...popupStateMapping,
1000
+ ...transitionStatusMapping,
980
1001
  expanded(value) {
981
1002
  return value ? {
982
1003
  [DrawerPopupDataAttributes.expanded]: ''
@@ -993,9 +1014,9 @@ const stateAttributesMapping = {
993
1014
  } : null;
994
1015
  },
995
1016
  swipeDirection(value) {
996
- return {
1017
+ return value ? {
997
1018
  [DrawerPopupDataAttributes.swipeDirection]: value
998
- };
1019
+ } : null;
999
1020
  },
1000
1021
  swiping(value) {
1001
1022
  return value ? {
@@ -1019,7 +1040,9 @@ const DrawerPopup = /*#__PURE__*/React.forwardRef(function DrawerPopup(component
1019
1040
  initialFocus,
1020
1041
  ...elementProps
1021
1042
  } = componentProps;
1022
- const store = useDialogRootContext();
1043
+ const {
1044
+ store
1045
+ } = useDialogRootContext();
1023
1046
  const popupRef = store.context.popupRef;
1024
1047
  const {
1025
1048
  swipeDirection,
@@ -1058,8 +1081,6 @@ const DrawerPopup = /*#__PURE__*/React.forwardRef(function DrawerPopup(component
1058
1081
  const swipeStrength = swipe?.swipeStrength ?? null;
1059
1082
  const [popupHeight, setPopupHeight] = React.useState(0);
1060
1083
  const popupHeightRef = React.useRef(0);
1061
-
1062
- /* istanbul ignore else -- process.env.NODE_ENV is a build-time constant. */
1063
1084
  if (process.env.NODE_ENV !== 'production') {
1064
1085
  // eslint-disable-next-line react-hooks/rules-of-hooks
1065
1086
  React.useEffect(() => {
@@ -1142,7 +1163,7 @@ const DrawerPopup = /*#__PURE__*/React.forwardRef(function DrawerPopup(component
1142
1163
  }
1143
1164
  };
1144
1165
  }, [nestedSwipeProgressStore, popupRef]);
1145
- useIsoLayoutEffect(() => {
1166
+ React.useEffect(() => {
1146
1167
  if (!open) {
1147
1168
  return undefined;
1148
1169
  }
@@ -1151,7 +1172,7 @@ const DrawerPopup = /*#__PURE__*/React.forwardRef(function DrawerPopup(component
1151
1172
  notifyParentFrontmostHeight?.(0);
1152
1173
  };
1153
1174
  }, [frontmostHeight, open, notifyParentFrontmostHeight]);
1154
- useIsoLayoutEffect(() => {
1175
+ React.useEffect(() => {
1155
1176
  if (!notifyParentHasNestedDrawer) {
1156
1177
  return undefined;
1157
1178
  }
@@ -1195,7 +1216,7 @@ const DrawerPopup = /*#__PURE__*/React.forwardRef(function DrawerPopup(component
1195
1216
  let dragStyles = swipe ? swipe.getDragStyles() : EMPTY_OBJECT;
1196
1217
  if (shouldApplySnapPoints && swipeDirection === 'down') {
1197
1218
  const baseOffset = activeSnapPointOffset ?? 0;
1198
- const movementValue = Number.parseFloat(String(dragStyles[DrawerPopupCssVars.swipeMovementY]));
1219
+ const movementValue = Number.parseFloat(String(dragStyles[DrawerPopupCssVars.swipeMovementY] ?? 0));
1199
1220
  if (swiping && Number.isFinite(movementValue)) {
1200
1221
  dragStyles = {
1201
1222
  ...dragStyles,
@@ -1305,7 +1326,6 @@ function DrawerRoot(props) {
1305
1326
  state: 'snapPoint'
1306
1327
  });
1307
1328
  const isNestedDrawerOpenRef = React.useRef(false);
1308
- const swipeAreaActiveRef = React.useRef(false);
1309
1329
  const setActiveSnapPoint = useStableCallback((nextSnapPoint, eventDetails) => {
1310
1330
  const resolvedEventDetails = eventDetails ?? createChangeEventDetails(none);
1311
1331
  onSnapPointChange?.(nextSnapPoint, resolvedEventDetails);
@@ -1365,7 +1385,6 @@ function DrawerRoot(props) {
1365
1385
  });
1366
1386
  const contextValue = React.useMemo(() => ({
1367
1387
  swipeDirection,
1368
- swipeAreaActiveRef,
1369
1388
  snapToSequentialPoints,
1370
1389
  snapPoints,
1371
1390
  activeSnapPoint: resolvedActiveSnapPoint,
@@ -1384,28 +1403,30 @@ function DrawerRoot(props) {
1384
1403
  notifyParentSwipingChange,
1385
1404
  notifyParentSwipeProgressChange,
1386
1405
  notifyParentHasNestedDrawer
1387
- }), [resolvedActiveSnapPoint, frontmostHeight, hasNestedDrawer, nestedSwiping, nestedSwipeProgressStore, notifyParentHasNestedDrawer, notifyParentSwipeProgressChange, notifyParentSwipingChange, notifyParentFrontmostHeight, onNestedDrawerPresenceChange, onNestedFrontmostHeightChange, onNestedSwipeProgressChange, onNestedSwipingChange, onPopupHeightChange, popupHeight, setActiveSnapPoint, snapPoints, snapToSequentialPoints, swipeAreaActiveRef, swipeDirection]);
1406
+ }), [resolvedActiveSnapPoint, frontmostHeight, hasNestedDrawer, nestedSwiping, nestedSwipeProgressStore, notifyParentHasNestedDrawer, notifyParentSwipeProgressChange, notifyParentSwipingChange, notifyParentFrontmostHeight, onNestedDrawerPresenceChange, onNestedFrontmostHeightChange, onNestedSwipeProgressChange, onNestedSwipingChange, onPopupHeightChange, popupHeight, setActiveSnapPoint, snapPoints, snapToSequentialPoints, swipeDirection]);
1388
1407
  const resolvedChildren = typeof children === 'function' ? payload => /*#__PURE__*/jsxs(React.Fragment, {
1389
1408
  children: [_DrawerProviderReport || (_DrawerProviderReport = /*#__PURE__*/jsx(DrawerProviderReporter, {})), children(payload)]
1390
1409
  }) : /*#__PURE__*/jsxs(React.Fragment, {
1391
1410
  children: [_DrawerProviderReport2 || (_DrawerProviderReport2 = /*#__PURE__*/jsx(DrawerProviderReporter, {})), children]
1392
1411
  });
1393
- const dialog = useRenderDialogRoot('drawer', {
1394
- open: openProp,
1395
- defaultOpen,
1396
- onOpenChange: handleOpenChange,
1397
- onOpenChangeComplete,
1398
- disablePointerDismissal,
1399
- modal,
1400
- actionsRef,
1401
- handle,
1402
- triggerId: triggerIdProp,
1403
- defaultTriggerId: defaultTriggerIdProp,
1404
- children: resolvedChildren
1405
- });
1406
1412
  return /*#__PURE__*/jsx(DrawerRootContext.Provider, {
1407
1413
  value: contextValue,
1408
- children: dialog
1414
+ children: /*#__PURE__*/jsx(IsDrawerContext.Provider, {
1415
+ value: true,
1416
+ children: /*#__PURE__*/jsx(DialogRoot, {
1417
+ open: openProp,
1418
+ defaultOpen: defaultOpen,
1419
+ onOpenChange: handleOpenChange,
1420
+ onOpenChangeComplete: onOpenChangeComplete,
1421
+ disablePointerDismissal: disablePointerDismissal,
1422
+ modal: modal,
1423
+ actionsRef: actionsRef,
1424
+ handle: handle,
1425
+ triggerId: triggerIdProp,
1426
+ defaultTriggerId: defaultTriggerIdProp,
1427
+ children: resolvedChildren
1428
+ })
1429
+ })
1409
1430
  });
1410
1431
  }
1411
1432
  function createNestedSwipeProgressStore() {
@@ -1432,25 +1453,29 @@ function createNestedSwipeProgressStore() {
1432
1453
  };
1433
1454
  }
1434
1455
  function DrawerProviderReporter() {
1456
+ const drawerId = useId();
1435
1457
  const providerContext = useDrawerProviderContext();
1436
- const store = useDialogRootContext(false);
1437
- const setDrawerOpen = providerContext?.setDrawerOpen;
1438
- const removeDrawer = providerContext?.removeDrawer;
1458
+ const {
1459
+ store
1460
+ } = useDialogRootContext(false);
1439
1461
  const open = store.useState('open');
1440
1462
  const nestedOpenDialogCount = store.useState('nestedOpenDialogCount');
1441
1463
  const popupElement = store.useState('popupElement');
1442
1464
  const isTopmost = nestedOpenDialogCount === 0;
1443
- useIsoLayoutEffect(() => {
1444
- if (!removeDrawer) {
1465
+ React.useEffect(() => {
1466
+ if (!providerContext || drawerId == null) {
1445
1467
  return undefined;
1446
1468
  }
1447
1469
  return () => {
1448
- removeDrawer(store);
1470
+ providerContext.removeDrawer(drawerId);
1449
1471
  };
1450
- }, [removeDrawer, store]);
1451
- useIsoLayoutEffect(() => {
1452
- setDrawerOpen?.(store, open);
1453
- }, [open, setDrawerOpen, store]);
1472
+ }, [drawerId, providerContext]);
1473
+ React.useEffect(() => {
1474
+ if (drawerId == null) {
1475
+ return;
1476
+ }
1477
+ providerContext?.setDrawerOpen(drawerId, open);
1478
+ }, [drawerId, open, providerContext]);
1454
1479
  React.useEffect(() => {
1455
1480
  // CloseWatcher enables the Android back gesture (Chromium-only).
1456
1481
  // Keep this Android-only for now to avoid interfering with Escape/nesting semantics on desktop due to `useDismiss`.
@@ -1478,11 +1503,53 @@ function DrawerProviderReporter() {
1478
1503
  return null;
1479
1504
  }
1480
1505
 
1481
- // `Document.elementFromPoint` retargets shadow content to the shadow host, which then fails
1482
- // `contains()` checks against a popup inside that shadow root, so callers pass `getRootNode()`
1483
- // (a document or a shadow root) rather than `ownerDocument()`.
1484
- function getElementAtPoint(root, x, y) {
1485
- return typeof root?.elementFromPoint === 'function' ? root.elementFromPoint(x, y) : null;
1506
+ function isScrollable(element, axis,
1507
+ // When true, a container that overflows only once extra space is added (e.g. drawer
1508
+ // keyboard scroll slack) still counts, as long as it has layout size on the axis.
1509
+ allowOverflowIntent = false) {
1510
+ const style = getComputedStyle$1(element);
1511
+ if (axis === 'vertical') {
1512
+ const overflowY = style.overflowY;
1513
+ if (overflowY !== 'auto' && overflowY !== 'scroll') {
1514
+ return false;
1515
+ }
1516
+ return allowOverflowIntent ? element.clientHeight > 0 : element.scrollHeight > element.clientHeight;
1517
+ }
1518
+ const overflowX = style.overflowX;
1519
+ if (overflowX !== 'auto' && overflowX !== 'scroll') {
1520
+ return false;
1521
+ }
1522
+ return allowOverflowIntent ? element.clientWidth > 0 : element.scrollWidth > element.clientWidth;
1523
+ }
1524
+ function hasScrollableAncestor(target, root, axes) {
1525
+ // `getParentNode` crosses shadow boundaries (and slots), so a target inside a shadow root
1526
+ // still walks up to scrollable ancestors in the light DOM.
1527
+ let node = target;
1528
+ while (isHTMLElement(node) && node !== root && !isLastTraversableNode(node)) {
1529
+ for (const axis of axes) {
1530
+ if (isScrollable(node, axis)) {
1531
+ return true;
1532
+ }
1533
+ }
1534
+ node = getParentNode(node);
1535
+ }
1536
+ return false;
1537
+ }
1538
+ function findScrollableTouchTarget(target, root, axis = 'vertical', allowOverflowIntent = false) {
1539
+ // `getParentNode` crosses shadow boundaries (and slots), so a target inside a shadow root
1540
+ // still reaches a scrollable ancestor in the light DOM.
1541
+ let node = isHTMLElement(target) ? target : null;
1542
+ while (isHTMLElement(node) && node !== root && !isLastTraversableNode(node)) {
1543
+ if (isScrollable(node, axis, allowOverflowIntent)) {
1544
+ return node;
1545
+ }
1546
+ node = getParentNode(node);
1547
+ }
1548
+ return isScrollable(root, axis, allowOverflowIntent) ? root : null;
1549
+ }
1550
+
1551
+ function getElementAtPoint(doc, x, y) {
1552
+ return typeof doc?.elementFromPoint === 'function' ? doc.elementFromPoint(x, y) : null;
1486
1553
  }
1487
1554
 
1488
1555
  const DEFAULT_SWIPE_THRESHOLD = 40;
@@ -1629,7 +1696,6 @@ function useSwipeDismiss(options) {
1629
1696
  });
1630
1697
  const swipeProgressRef = React.useRef(0);
1631
1698
  const swipeThresholdRef = React.useRef(swipeThresholdDefault);
1632
- const swipeThresholdFunctionRef = React.useRef(null);
1633
1699
  const swipeStartTimeRef = React.useRef(null);
1634
1700
  const lastDragSampleRef = React.useRef(null);
1635
1701
  const lastDragVelocityRef = React.useRef({
@@ -1651,12 +1717,15 @@ function useSwipeDismiss(options) {
1651
1717
  if (!direction) {
1652
1718
  return;
1653
1719
  }
1720
+ if (typeof swipeThresholdProp !== 'function') {
1721
+ swipeThresholdRef.current = swipeThresholdDefault;
1722
+ return;
1723
+ }
1654
1724
  const element = elementRef.current;
1655
- const thresholdFunction = swipeThresholdFunctionRef.current;
1656
- if (!element || !thresholdFunction) {
1725
+ if (!element) {
1657
1726
  return;
1658
1727
  }
1659
- const value = thresholdFunction({
1728
+ const value = swipeThresholdProp({
1660
1729
  element,
1661
1730
  direction
1662
1731
  });
@@ -1734,7 +1803,6 @@ function useSwipeDismiss(options) {
1734
1803
  setDragDismissed(false);
1735
1804
  updateSwipeProgress(0);
1736
1805
  swipeThresholdRef.current = swipeThresholdDefault;
1737
- swipeThresholdFunctionRef.current = null;
1738
1806
  dragStartPosRef.current = {
1739
1807
  x: 0,
1740
1808
  y: 0
@@ -1775,6 +1843,11 @@ function useSwipeDismiss(options) {
1775
1843
  lastProgressDetailsRef.current = null;
1776
1844
  syncDragStyles(false);
1777
1845
  }, [setSwiping, swipeThresholdDefault, syncDragStyles, updateSwipeProgress]);
1846
+ React.useEffect(() => {
1847
+ if (typeof swipeThresholdProp !== 'function') {
1848
+ swipeThresholdRef.current = swipeThresholdDefault;
1849
+ }
1850
+ }, [swipeThresholdDefault, swipeThresholdProp]);
1778
1851
  function getPrimaryPointerPosition(event) {
1779
1852
  if ('touches' in event) {
1780
1853
  const touch = event.touches[0];
@@ -1795,8 +1868,8 @@ function useSwipeDismiss(options) {
1795
1868
  return event.pointerType === 'touch';
1796
1869
  }
1797
1870
  function getTargetAtPoint(position, nativeEvent) {
1798
- const root = elementRef.current?.getRootNode();
1799
- const elementAtPoint = getElementAtPoint(root, position.x, position.y);
1871
+ const doc = ownerDocument(elementRef.current);
1872
+ const elementAtPoint = getElementAtPoint(doc, position.x, position.y);
1800
1873
  const target = elementAtPoint ?? getTarget(nativeEvent);
1801
1874
  return target;
1802
1875
  }
@@ -1839,8 +1912,6 @@ function useSwipeDismiss(options) {
1839
1912
  swipeStartTimeRef.current = getValidTimeStamp(event.timeStamp);
1840
1913
  swipeCancelBaselineRef.current = position;
1841
1914
  lastMovePosRef.current = position;
1842
- swipeThresholdRef.current = swipeThresholdDefault;
1843
- swipeThresholdFunctionRef.current = typeof swipeThresholdProp === 'function' ? swipeThresholdProp : null;
1844
1915
  if (element) {
1845
1916
  elementSizeRef.current = {
1846
1917
  width: element.offsetWidth,
@@ -1905,9 +1976,12 @@ function useSwipeDismiss(options) {
1905
1976
  onCancel?.(event.nativeEvent);
1906
1977
  }
1907
1978
  function applyDirectionalDamping(deltaX, deltaY) {
1908
- const exponent = value => Math.sign(value) * Math.abs(value) ** 0.5;
1979
+ const exponent = value => value >= 0 ? value ** 0.5 : -(Math.abs(value) ** 0.5);
1909
1980
  const dampAxis = (delta, allowNegative, allowPositive) => {
1910
- if (!allowNegative && delta < 0 || !allowPositive && delta > 0) {
1981
+ if (!allowNegative && delta < 0) {
1982
+ return exponent(delta);
1983
+ }
1984
+ if (!allowPositive && delta > 0) {
1911
1985
  return exponent(delta);
1912
1986
  }
1913
1987
  return delta;
@@ -1920,16 +1994,29 @@ function useSwipeDismiss(options) {
1920
1994
  };
1921
1995
  }
1922
1996
  function canSwipeFromScrollEdgeOnPendingMove(scrollTarget, deltaX, deltaY) {
1923
- // Swiping toward the axis start edge (down/right) is allowed when scrolled to the start;
1924
- // toward the end edge (up/left) when scrolled to the end.
1925
- const canSwipeOnAxis = (delta, scrollOffset, maxScrollOffset, allowTowardStart, allowTowardEnd) => delta > 0 && scrollOffset <= 0 && allowTowardStart || delta < 0 && scrollOffset >= Math.max(0, maxScrollOffset) && allowTowardEnd;
1926
1997
  const absDeltaX = Math.abs(deltaX);
1927
1998
  const absDeltaY = Math.abs(deltaY);
1928
- if (hasVertical && deltaY !== 0 && (!hasHorizontal || absDeltaY >= absDeltaX)) {
1929
- return canSwipeOnAxis(deltaY, scrollTarget.scrollTop, scrollTarget.scrollHeight - scrollTarget.clientHeight, allowDown, allowUp);
1930
- }
1931
- if (hasHorizontal && deltaX !== 0 && (!hasVertical || absDeltaX > absDeltaY)) {
1932
- return canSwipeOnAxis(deltaX, scrollTarget.scrollLeft, scrollTarget.scrollWidth - scrollTarget.clientWidth, allowRight, allowLeft);
1999
+ const useVerticalAxis = hasVertical && deltaY !== 0 && (!hasHorizontal || absDeltaY >= absDeltaX);
2000
+ if (useVerticalAxis) {
2001
+ const maxScrollTop = Math.max(0, scrollTarget.scrollHeight - scrollTarget.clientHeight);
2002
+ const atTop = scrollTarget.scrollTop <= 0;
2003
+ const atBottom = scrollTarget.scrollTop >= maxScrollTop;
2004
+ const movingDown = deltaY > 0;
2005
+ const movingUp = deltaY < 0;
2006
+ const canSwipeDown = movingDown && atTop && allowDown;
2007
+ const canSwipeUp = movingUp && atBottom && allowUp;
2008
+ return canSwipeDown || canSwipeUp;
2009
+ }
2010
+ const useHorizontalAxis = hasHorizontal && deltaX !== 0 && (!hasVertical || absDeltaX > absDeltaY);
2011
+ if (useHorizontalAxis) {
2012
+ const maxScrollLeft = Math.max(0, scrollTarget.scrollWidth - scrollTarget.clientWidth);
2013
+ const atLeft = scrollTarget.scrollLeft <= 0;
2014
+ const atRight = scrollTarget.scrollLeft >= maxScrollLeft;
2015
+ const movingRight = deltaX > 0;
2016
+ const movingLeft = deltaX < 0;
2017
+ const canSwipeRight = movingRight && atLeft && allowRight;
2018
+ const canSwipeLeft = movingLeft && atRight && allowLeft;
2019
+ return canSwipeRight || canSwipeLeft;
1933
2020
  }
1934
2021
  return null;
1935
2022
  }
@@ -1950,7 +2037,7 @@ function useSwipeDismiss(options) {
1950
2037
  pendingSwipeRef.current = true;
1951
2038
  pendingSwipeStartPosRef.current = startPos;
1952
2039
  swipeFromScrollableRef.current = false;
1953
- sawPrimaryButtonsOnMoveRef.current = !('touches' in event);
2040
+ sawPrimaryButtonsOnMoveRef.current = false;
1954
2041
  const allowedToStart = canStart ? canStart(startPos, {
1955
2042
  nativeEvent: event.nativeEvent,
1956
2043
  direction: primaryDirection
@@ -1978,21 +2065,14 @@ function useSwipeDismiss(options) {
1978
2065
  event.preventDefault();
1979
2066
  }
1980
2067
  if (isFirstPointerMoveRef.current) {
1981
- isFirstPointerMoveRef.current = false;
1982
- // Reset the drag origin to the first move's position to absorb the gap between the press and
1983
- // the first move event — notably on iOS touch, where the first `touchmove` arrives already
1984
- // offset from the `touchstart` — which would otherwise make the dragged element jump. This
1985
- // only matters when an element follows the pointer; when `trackDrag` is false (e.g. the
1986
- // swipe-area, which only opens the drawer) keep the original press position so a quick flick
1987
- // still registers: on a low-refresh-rate display the whole travel can land in this single
1988
- // first move, and discarding it would drop the gesture.
1989
- if (trackDrag) {
1990
- dragStartPosRef.current = position;
1991
- const moveTime = getValidTimeStamp(event.timeStamp);
1992
- if (moveTime !== null) {
1993
- swipeStartTimeRef.current = moveTime;
1994
- }
2068
+ // Adjust the starting position to the current position on the first move
2069
+ // to account for the delay between pointerdown and the first pointermove on iOS.
2070
+ dragStartPosRef.current = position;
2071
+ const moveTime = getValidTimeStamp(event.timeStamp);
2072
+ if (moveTime !== null) {
2073
+ swipeStartTimeRef.current = moveTime;
1995
2074
  }
2075
+ isFirstPointerMoveRef.current = false;
1996
2076
  }
1997
2077
  const clientX = position.x;
1998
2078
  const clientY = position.y;
@@ -2080,45 +2160,51 @@ function useSwipeDismiss(options) {
2080
2160
  newOffsetY += dampedDelta.y;
2081
2161
  }
2082
2162
  }
2083
-
2084
- // Only rewrite drag styles when the drag offset actually changed. `syncDragStyles` writes the
2085
- // raw (undamped) frozen transform and movement vars, relying on the consumer's `onProgress`
2086
- // to overwrite them with damped styles — but `updateSwipeProgress` dedupes unchanged
2087
- // deltas and skips `onProgress`. A move that doesn't change the offset (e.g. the cursor
2088
- // pinned at a screen edge during an off-screen drag, jittering only on the ignored axis)
2089
- // would otherwise reinstate the raw styles with no correction, jumping the element to the
2090
- // undamped position.
2091
- const previousOffset = dragOffsetRef.current;
2092
- const offsetChanged = newOffsetX !== previousOffset.x || newOffsetY !== previousOffset.y;
2093
2163
  dragOffsetRef.current = {
2094
2164
  x: newOffsetX,
2095
2165
  y: newOffsetY
2096
2166
  };
2097
- if (offsetChanged) {
2098
- syncDragStyles(true);
2099
- }
2167
+ syncDragStyles(true);
2100
2168
  recordDragSample({
2101
2169
  x: newOffsetX,
2102
2170
  y: newOffsetY
2103
2171
  }, getValidTimeStamp(event.timeStamp));
2104
2172
  const dragDeltaX = newOffsetX - initialTransformRef.current.x;
2105
2173
  const dragDeltaY = newOffsetY - initialTransformRef.current.y;
2106
- const progressDetails = {
2107
- deltaX: dragDeltaX,
2108
- deltaY: dragDeltaY,
2109
- direction: intendedSwipeDirectionRef.current
2110
- };
2111
- let progress = 0;
2174
+ const swipeDirectionDetails = intendedSwipeDirectionRef.current;
2112
2175
  const progressDirection = primaryDirection ?? intendedSwipeDirectionRef.current;
2113
- if (progressDirection) {
2114
- const size = progressDirection === 'left' || progressDirection === 'right' ? elementSizeRef.current.width : elementSizeRef.current.height;
2115
- const scale = initialTransformRef.current.scale || 1;
2116
- const progressDisplacement = getDisplacement(progressDirection, dragDeltaX, dragDeltaY);
2117
- if (size > 0 && scale > 0 && progressDisplacement > 0) {
2118
- progress = progressDisplacement / (size * scale);
2119
- }
2176
+ if (!progressDirection) {
2177
+ updateSwipeProgress(0, {
2178
+ deltaX: dragDeltaX,
2179
+ deltaY: dragDeltaY,
2180
+ direction: swipeDirectionDetails
2181
+ });
2182
+ return;
2120
2183
  }
2121
- updateSwipeProgress(progress, progressDetails);
2184
+ const size = progressDirection === 'left' || progressDirection === 'right' ? elementSizeRef.current.width : elementSizeRef.current.height;
2185
+ const scale = initialTransformRef.current.scale || 1;
2186
+ if (size <= 0 || scale <= 0) {
2187
+ updateSwipeProgress(0, {
2188
+ deltaX: dragDeltaX,
2189
+ deltaY: dragDeltaY,
2190
+ direction: swipeDirectionDetails
2191
+ });
2192
+ return;
2193
+ }
2194
+ const progressDisplacement = getDisplacement(progressDirection, newOffsetX - initialTransformRef.current.x, newOffsetY - initialTransformRef.current.y);
2195
+ if (progressDisplacement <= 0) {
2196
+ updateSwipeProgress(0, {
2197
+ deltaX: dragDeltaX,
2198
+ deltaY: dragDeltaY,
2199
+ direction: swipeDirectionDetails
2200
+ });
2201
+ return;
2202
+ }
2203
+ updateSwipeProgress(progressDisplacement / (size * scale), {
2204
+ deltaX: dragDeltaX,
2205
+ deltaY: dragDeltaY,
2206
+ direction: swipeDirectionDetails
2207
+ });
2122
2208
  }
2123
2209
  const handleEnd = useStableCallback(event => {
2124
2210
  if (!enabled) {
@@ -2206,9 +2292,33 @@ function useSwipeDismiss(options) {
2206
2292
  dismissDirection = intendedSwipeDirectionRef.current ?? primaryDirection;
2207
2293
  } else {
2208
2294
  for (const direction of directions) {
2209
- if (getDisplacement(direction, deltaX, deltaY) > swipeThresholdRef.current) {
2210
- shouldClose = true;
2211
- dismissDirection = direction;
2295
+ switch (direction) {
2296
+ case 'right':
2297
+ if (deltaX > swipeThresholdRef.current) {
2298
+ shouldClose = true;
2299
+ dismissDirection = 'right';
2300
+ }
2301
+ break;
2302
+ case 'left':
2303
+ if (deltaX < -swipeThresholdRef.current) {
2304
+ shouldClose = true;
2305
+ dismissDirection = 'left';
2306
+ }
2307
+ break;
2308
+ case 'down':
2309
+ if (deltaY > swipeThresholdRef.current) {
2310
+ shouldClose = true;
2311
+ dismissDirection = 'down';
2312
+ }
2313
+ break;
2314
+ case 'up':
2315
+ if (deltaY < -swipeThresholdRef.current) {
2316
+ shouldClose = true;
2317
+ dismissDirection = 'up';
2318
+ }
2319
+ break;
2320
+ }
2321
+ if (shouldClose) {
2212
2322
  break;
2213
2323
  }
2214
2324
  }
@@ -2350,29 +2460,25 @@ function useSwipeDismiss(options) {
2350
2460
  });
2351
2461
  });
2352
2462
  const getDragStyles = React.useCallback(() => {
2353
- // Read `isSwipingRef`, not the lagging `isSwiping` state, to match the imperative writer
2354
- // `syncDragStyles`. Otherwise a render that commits before `setSwiping(true)` flushes strips the
2355
- // transform it just wrote, flashing the popup to its resting position for a frame.
2356
- const swiping = isSwipingRef.current;
2357
2463
  const dragOffset = dragOffsetRef.current;
2358
2464
  const initialTransform = initialTransformRef.current;
2359
2465
  const deltaX = dragOffset.x - initialTransform.x;
2360
2466
  const deltaY = dragOffset.y - initialTransform.y;
2361
- if (!swiping && deltaX === 0 && deltaY === 0 && !dragDismissed) {
2467
+ if (!isSwiping && deltaX === 0 && deltaY === 0 && !dragDismissed) {
2362
2468
  return {
2363
2469
  [movementCssVars.x]: '0px',
2364
2470
  [movementCssVars.y]: '0px'
2365
2471
  };
2366
2472
  }
2367
2473
  return {
2368
- transition: swiping ? 'none' : undefined,
2474
+ transition: isSwiping ? 'none' : undefined,
2369
2475
  // While swiping, freeze the element at its current visual transform so it doesn't snap to the
2370
2476
  // end position.
2371
- transform: swiping ? getDragTransform(dragOffset, initialTransform.scale) : undefined,
2477
+ transform: isSwiping ? getDragTransform(dragOffset, initialTransform.scale) : undefined,
2372
2478
  [movementCssVars.x]: `${deltaX}px`,
2373
2479
  [movementCssVars.y]: `${deltaY}px`
2374
2480
  };
2375
- }, [dragDismissed, movementCssVars]);
2481
+ }, [dragDismissed, isSwiping, movementCssVars]);
2376
2482
  const getPointerProps = React.useCallback(() => {
2377
2483
  if (!enabled) {
2378
2484
  return {};
@@ -2432,8 +2538,6 @@ const MIN_SWIPE_RELEASE_DURATION_MS = 80;
2432
2538
  const MAX_SWIPE_RELEASE_DURATION_MS = 360;
2433
2539
  const MIN_SWIPE_RELEASE_SCALAR = 0.1;
2434
2540
  const MAX_SWIPE_RELEASE_SCALAR = 1;
2435
- const AXIS_LOCK_SLOP = 6;
2436
- const AXIS_LOCK_BIAS = 2;
2437
2541
  const DRAWER_CONTENT_SELECTOR = `[${DRAWER_CONTENT_ATTRIBUTE}]`;
2438
2542
  /**
2439
2543
  * A positioning container for the drawer popup that can be made scrollable.
@@ -2449,7 +2553,9 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2449
2553
  children,
2450
2554
  ...elementProps
2451
2555
  } = props;
2452
- const store = useDialogRootContext();
2556
+ const {
2557
+ store
2558
+ } = useDialogRootContext();
2453
2559
  const popupRef = store.context.popupRef;
2454
2560
  const backdropRef = store.context.backdropRef;
2455
2561
  const {
@@ -2457,8 +2563,7 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2457
2563
  notifyParentSwipingChange,
2458
2564
  notifyParentSwipeProgressChange,
2459
2565
  frontmostHeight,
2460
- snapToSequentialPoints,
2461
- swipeAreaActiveRef
2566
+ snapToSequentialPoints
2462
2567
  } = useDrawerRootContext();
2463
2568
  const providerContext = useDrawerProviderContext();
2464
2569
  const {
@@ -2492,13 +2597,30 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2492
2597
  const touchScrollStateRef = React.useRef(null);
2493
2598
  const virtualKeyboard = useDrawerVirtualKeyboardContext();
2494
2599
  const snapPointRange = React.useMemo(() => {
2495
- if (!snapPoints || snapPoints.length < 2 || resolvedSnapPoints.length < 2 || swipeDirection !== 'down' && swipeDirection !== 'up') {
2600
+ if (!snapPoints || snapPoints.length < 2) {
2601
+ return null;
2602
+ }
2603
+ if (swipeDirection !== 'down' && swipeDirection !== 'up') {
2604
+ return null;
2605
+ }
2606
+ if (resolvedSnapPoints.length < 2) {
2607
+ return null;
2608
+ }
2609
+ const offsets = resolvedSnapPoints.map(point => point.offset).filter(offset => Number.isFinite(offset)).sort((a, b) => a - b);
2610
+ if (offsets.length < 2) {
2496
2611
  return null;
2497
2612
  }
2498
- const offsets = resolvedSnapPoints.map(point => point.offset).sort((a, b) => a - b);
2499
2613
  const minOffset = offsets[0];
2500
2614
  const nextOffset = offsets[1];
2501
- const range = nextOffset - minOffset;
2615
+ const maxOffset = offsets[offsets.length - 1];
2616
+ let range = nextOffset - minOffset;
2617
+ if (!Number.isFinite(range) || range <= 0) {
2618
+ const fallbackRange = maxOffset - minOffset;
2619
+ if (!Number.isFinite(fallbackRange) || fallbackRange <= 0) {
2620
+ return null;
2621
+ }
2622
+ range = fallbackRange;
2623
+ }
2502
2624
  return {
2503
2625
  minOffset,
2504
2626
  range
@@ -2517,8 +2639,7 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2517
2639
  return [swipeDirection];
2518
2640
  }, [snapPoints, swipeDirection]);
2519
2641
  const setSwipeDismissed = useStableCallback(dismissed => {
2520
- popupRef.current?.toggleAttribute(DrawerPopupDataAttributes.swipeDismiss, dismissed);
2521
- backdropRef.current?.toggleAttribute(DrawerPopupDataAttributes.swipeDismiss, dismissed);
2642
+ setSwipeDismissedElements(popupRef.current, backdropRef.current, dismissed);
2522
2643
  });
2523
2644
  const clearSwipeRelease = useStableCallback(() => {
2524
2645
  setSwipeDismissed(false);
@@ -2532,7 +2653,11 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2532
2653
  nestedSwipeActiveRef.current = false;
2533
2654
  notifyParentSwipingChange?.(false);
2534
2655
  });
2535
- const applySwipeProgress = useStableCallback((resolvedProgress, shouldTrackProgress, notifyParent) => {
2656
+ const applySwipeProgress = useStableCallback(({
2657
+ resolvedProgress,
2658
+ shouldTrackProgress,
2659
+ notifyParent
2660
+ }) => {
2536
2661
  const isActive = open && !nested && shouldTrackProgress;
2537
2662
  const swipeProgress = isActive ? resolvedProgress : 0;
2538
2663
  const nestedSwipeProgress = open && shouldTrackProgress ? resolvedProgress : 0;
@@ -2550,48 +2675,78 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2550
2675
  if (!backdropElement) {
2551
2676
  return;
2552
2677
  }
2553
- const showProgress = isActive && swipeProgress > 0;
2554
- backdropElement.style.setProperty(DrawerBackdropCssVars.swipeProgress, showProgress ? `${swipeProgress}` : '0');
2555
- if (showProgress && frontmostHeight > 0) {
2678
+ if (!isActive || swipeProgress <= 0) {
2679
+ backdropElement.style.setProperty(DrawerBackdropCssVars.swipeProgress, '0');
2680
+ backdropElement.style.removeProperty(DrawerPopupCssVars.height);
2681
+ return;
2682
+ }
2683
+ backdropElement.style.setProperty(DrawerBackdropCssVars.swipeProgress, `${swipeProgress}`);
2684
+ if (frontmostHeight > 0) {
2556
2685
  backdropElement.style.setProperty(DrawerPopupCssVars.height, `${frontmostHeight}px`);
2557
2686
  } else {
2558
2687
  backdropElement.style.removeProperty(DrawerPopupCssVars.height);
2559
2688
  }
2560
2689
  });
2561
- function resolveSwipeRelease(popupElement, direction, deltaX, deltaY, velocityX, velocityY, releaseVelocityX, releaseVelocityY) {
2562
- const size = getBaseSwipeSize(popupElement, direction);
2563
- if (size <= 0) {
2690
+ function resolveSwipeRelease({
2691
+ direction,
2692
+ deltaX,
2693
+ deltaY,
2694
+ velocityX,
2695
+ velocityY,
2696
+ releaseVelocityX,
2697
+ releaseVelocityY
2698
+ }) {
2699
+ if (!direction) {
2564
2700
  return null;
2565
2701
  }
2566
-
2567
- // The snap point base offset shifts the popup along the dismiss direction for both
2568
- // `down` (+offset) and `up` (-offset), so it always adds to the directional translation.
2569
- const snapPointBaseOffset = (direction === 'down' || direction === 'up') && snapPoints && snapPoints.length > 0 ? activeSnapPointOffset ?? 0 : 0;
2570
- const translationAlongDirection = snapPointBaseOffset + getDisplacement(direction, deltaX, deltaY);
2702
+ const popupElement = store.context.popupRef.current;
2703
+ if (!popupElement) {
2704
+ return null;
2705
+ }
2706
+ const size = direction === 'left' || direction === 'right' ? popupElement.offsetWidth : popupElement.offsetHeight;
2707
+ if (!Number.isFinite(size) || size <= 0) {
2708
+ return null;
2709
+ }
2710
+ const axisDelta = direction === 'left' || direction === 'right' ? deltaX : deltaY;
2711
+ const snapPointBaseOffset = snapPoints && snapPoints.length > 0 ? activeSnapPointOffset ?? 0 : 0;
2712
+ let baseOffset = 0;
2713
+ if (direction === 'down') {
2714
+ baseOffset = snapPointBaseOffset;
2715
+ } else if (direction === 'up') {
2716
+ baseOffset = -snapPointBaseOffset;
2717
+ }
2718
+ const translation = baseOffset + axisDelta;
2719
+ const translationAlongDirection = direction === 'left' || direction === 'up' ? -translation : translation;
2571
2720
  const remainingDistance = Math.max(0, size - translationAlongDirection);
2572
- if (remainingDistance <= 0) {
2721
+ if (!Number.isFinite(remainingDistance) || remainingDistance <= 0) {
2573
2722
  return null;
2574
2723
  }
2575
- const releaseVelocity = getDisplacement(direction, releaseVelocityX, releaseVelocityY);
2576
- const directionalVelocity = Math.abs(releaseVelocity) > 0 ? releaseVelocity : getDisplacement(direction, velocityX, velocityY);
2577
- if (directionalVelocity <= MIN_SWIPE_RELEASE_VELOCITY) {
2724
+ const axisVelocity = direction === 'left' || direction === 'right' ? releaseVelocityX : releaseVelocityY;
2725
+ const fallbackVelocity = direction === 'left' || direction === 'right' ? velocityX : velocityY;
2726
+ const resolvedVelocity = Math.abs(axisVelocity) > 0 && Number.isFinite(axisVelocity) ? axisVelocity : fallbackVelocity;
2727
+ const directionalVelocity = direction === 'left' || direction === 'up' ? -resolvedVelocity : resolvedVelocity;
2728
+ if (!Number.isFinite(directionalVelocity) || directionalVelocity <= MIN_SWIPE_RELEASE_VELOCITY) {
2578
2729
  return null;
2579
2730
  }
2580
2731
  const clampedVelocity = clamp(directionalVelocity, MIN_SWIPE_RELEASE_VELOCITY, MAX_SWIPE_RELEASE_VELOCITY);
2581
- // The gesture hook supplies finite deltas and velocities. The guards above keep the remaining
2582
- // distance and divisor positive, so the duration stays within [MIN, MAX] and the resulting
2583
- // scalar within (0, 1].
2584
2732
  const durationMs = clamp(remainingDistance / clampedVelocity, MIN_SWIPE_RELEASE_DURATION_MS, MAX_SWIPE_RELEASE_DURATION_MS);
2733
+ if (!Number.isFinite(durationMs)) {
2734
+ return null;
2735
+ }
2585
2736
  const normalizedDuration = (durationMs - MIN_SWIPE_RELEASE_DURATION_MS) / (MAX_SWIPE_RELEASE_DURATION_MS - MIN_SWIPE_RELEASE_DURATION_MS);
2586
- return MIN_SWIPE_RELEASE_SCALAR + normalizedDuration * (MAX_SWIPE_RELEASE_SCALAR - MIN_SWIPE_RELEASE_SCALAR);
2737
+ const durationScalar = clamp(MIN_SWIPE_RELEASE_SCALAR + normalizedDuration * (MAX_SWIPE_RELEASE_SCALAR - MIN_SWIPE_RELEASE_SCALAR), MIN_SWIPE_RELEASE_SCALAR, MAX_SWIPE_RELEASE_SCALAR);
2738
+ if (!Number.isFinite(durationScalar) || durationScalar <= 0) {
2739
+ return null;
2740
+ }
2741
+ return durationScalar;
2587
2742
  }
2588
2743
  function updateNestedSwipeActive(details) {
2589
2744
  if (nestedSwipeActiveRef.current || !details) {
2590
2745
  return;
2591
2746
  }
2592
2747
  const direction = details.direction ?? swipeDirection;
2593
- const delta = getDisplacement(direction, details.deltaX, details.deltaY);
2594
- if (Math.abs(delta) < MIN_SWIPE_THRESHOLD) {
2748
+ const delta = direction === 'left' || direction === 'right' ? details.deltaX : details.deltaY;
2749
+ if (!Number.isFinite(delta) || Math.abs(delta) < MIN_SWIPE_THRESHOLD) {
2595
2750
  return;
2596
2751
  }
2597
2752
  nestedSwipeActiveRef.current = true;
@@ -2608,10 +2763,13 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2608
2763
  y: DrawerPopupCssVars.swipeMovementY
2609
2764
  },
2610
2765
  onSwipeStart(event) {
2611
- if ('touches' in event || event.pointerType === 'touch') {
2766
+ if ('touches' in event || 'pointerType' in event && event.pointerType === 'touch') {
2612
2767
  return;
2613
2768
  }
2614
2769
  const popupElement = popupRef.current;
2770
+ if (!popupElement) {
2771
+ return;
2772
+ }
2615
2773
  const doc = ownerDocument(popupElement);
2616
2774
  const selection = doc.getSelection?.();
2617
2775
  if (!selection || selection.isCollapsed) {
@@ -2643,38 +2801,54 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2643
2801
  return false;
2644
2802
  }
2645
2803
  const doc = popupElement.ownerDocument;
2646
- const elementAtPoint = getElementAtPoint(popupElement.getRootNode(), position.x, position.y);
2804
+ const elementAtPoint = getElementAtPoint(doc, position.x, position.y);
2647
2805
  if (!elementAtPoint || !contains(popupElement, elementAtPoint)) {
2648
2806
  return false;
2649
2807
  }
2650
2808
  const nativeEvent = details.nativeEvent;
2651
- const touchLike = 'touches' in nativeEvent || nativeEvent.pointerType === 'touch';
2809
+ const touchLike = 'touches' in nativeEvent || 'pointerType' in nativeEvent && nativeEvent.pointerType === 'touch';
2652
2810
  if (touchLike && shouldIgnoreSwipeForTextSelection(doc, popupElement)) {
2653
2811
  return false;
2654
2812
  }
2813
+ if (nativeEvent.type === 'touchstart' && isSwipeIgnoredTarget(elementAtPoint)) {
2814
+ return false;
2815
+ }
2655
2816
  return true;
2656
2817
  },
2657
2818
  onProgress(progress, details) {
2658
2819
  updateNestedSwipeActive(details);
2659
2820
  const hasSnapPoints = Boolean(snapPoints && snapPoints.length > 0);
2660
- if (swipingRef.current && swipeDirection === 'down' && hasSnapPoints && details) {
2821
+ if (swipingRef.current && swipeDirection === 'down' && hasSnapPoints && details && Number.isFinite(details.deltaY)) {
2661
2822
  const popupElement = store.context.popupRef.current;
2662
2823
  if (popupElement) {
2663
2824
  popupElement.style.removeProperty('transform');
2664
2825
  popupElement.style.setProperty(DrawerPopupCssVars.swipeMovementY, `${getSnapPointSwipeMovement(activeSnapPointOffset ?? 0, details.deltaY)}px`);
2665
2826
  }
2666
2827
  }
2828
+ const currentDirection = details?.direction ?? swipe$1.swipeDirection;
2829
+ const isDismissSwipe = currentDirection === undefined || currentDirection === swipeDirection;
2830
+ const isVerticalSwipe = swipeDirection === 'down' || swipeDirection === 'up';
2831
+ const shouldTrackProgress = hasSnapPoints && isVerticalSwipe || !hasSnapPoints || swipeDirection === 'left' || swipeDirection === 'right' || isDismissSwipe;
2667
2832
  let resolvedProgress = progress;
2668
2833
  if (snapPointRange && popupHeight > 0) {
2669
- const baseOffset = activeSnapPointOffset ?? snapPointRange.minOffset;
2670
- const offsetToProgress = nextOffset => clamp((nextOffset - snapPointRange.minOffset) / snapPointRange.range, 0, 1);
2671
2834
  if (details && Number.isFinite(details.deltaY)) {
2672
- resolvedProgress = offsetToProgress(clamp(baseOffset + details.deltaY, 0, popupHeight));
2835
+ const baseOffset = activeSnapPointOffset ?? snapPointRange.minOffset;
2836
+ const nextOffset = clamp(baseOffset + details.deltaY, 0, popupHeight);
2837
+ resolvedProgress = clamp((nextOffset - snapPointRange.minOffset) / snapPointRange.range, 0, 1);
2673
2838
  } else if (snapPointProgress !== null) {
2674
2839
  resolvedProgress = snapPointProgress;
2840
+ } else if (currentDirection === 'down' || currentDirection === 'up') {
2841
+ const displacement = progress * popupHeight;
2842
+ const baseOffset = activeSnapPointOffset ?? snapPointRange.minOffset;
2843
+ const nextOffset = currentDirection === 'down' ? baseOffset + displacement : baseOffset - displacement;
2844
+ resolvedProgress = clamp((nextOffset - snapPointRange.minOffset) / snapPointRange.range, 0, 1);
2675
2845
  }
2676
2846
  }
2677
- applySwipeProgress(resolvedProgress, true, true);
2847
+ applySwipeProgress({
2848
+ resolvedProgress,
2849
+ shouldTrackProgress,
2850
+ notifyParent: true
2851
+ });
2678
2852
  },
2679
2853
  onRelease({
2680
2854
  event,
@@ -2686,21 +2860,30 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2686
2860
  releaseVelocityX,
2687
2861
  releaseVelocityY
2688
2862
  }) {
2689
- const popupElement = store.context.popupRef.current;
2690
- if (!popupElement) {
2691
- clearSwipeRelease();
2692
- return undefined;
2693
- }
2694
- const releasePopupElement = popupElement;
2863
+ const swipeReleasePayload = {
2864
+ deltaX,
2865
+ deltaY,
2866
+ velocityX,
2867
+ velocityY,
2868
+ releaseVelocityX,
2869
+ releaseVelocityY
2870
+ };
2695
2871
  function startSwipeRelease(resolvedDirection) {
2696
2872
  // Start ending transition styles earlier and synchronously to prevent a period where
2697
2873
  // the popup appears stuck on release before the actual closing animation starts.
2874
+ const popupElement = store.context.popupRef.current;
2875
+ if (!popupElement) {
2876
+ return;
2877
+ }
2698
2878
  finishNestedSwipe();
2699
2879
  setSwipeDismissed(true);
2700
- releasePopupElement.style.removeProperty('transition');
2701
- releasePopupElement.setAttribute(TransitionStatusDataAttributes.endingStyle, '');
2880
+ popupElement.style.removeProperty('transition');
2881
+ popupElement.setAttribute(TransitionStatusDataAttributes.endingStyle, '');
2702
2882
  ReactDOM.flushSync(() => {
2703
- setSwipeRelease(resolveSwipeRelease(releasePopupElement, resolvedDirection, deltaX, deltaY, velocityX, velocityY, releaseVelocityX, releaseVelocityY));
2883
+ setSwipeRelease(resolveSwipeRelease({
2884
+ direction: resolvedDirection,
2885
+ ...swipeReleasePayload
2886
+ }));
2704
2887
  });
2705
2888
  }
2706
2889
  if (!snapPoints || snapPoints.length === 0) {
@@ -2708,16 +2891,29 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2708
2891
  clearSwipeRelease();
2709
2892
  return undefined;
2710
2893
  }
2711
- const directionalDelta = getDisplacement(direction, deltaX, deltaY);
2894
+ const element = store.context.popupRef.current;
2895
+ if (!element) {
2896
+ clearSwipeRelease();
2897
+ return undefined;
2898
+ }
2899
+ const baseThreshold = getBaseSwipeThreshold(element, direction);
2900
+ const delta = direction === 'left' || direction === 'right' ? deltaX : deltaY;
2901
+ if (!Number.isFinite(delta)) {
2902
+ clearSwipeRelease();
2903
+ return undefined;
2904
+ }
2905
+ const directionalDelta = direction === 'left' || direction === 'up' ? -delta : delta;
2712
2906
  if (directionalDelta <= 0) {
2713
2907
  clearSwipeRelease();
2714
2908
  return false;
2715
2909
  }
2716
- if (getDisplacement(direction, velocityX, velocityY) >= FAST_SWIPE_VELOCITY) {
2910
+ const velocity = direction === 'left' || direction === 'right' ? velocityX : velocityY;
2911
+ const directionalVelocity = direction === 'left' || direction === 'up' ? -velocity : velocity;
2912
+ if (directionalVelocity >= FAST_SWIPE_VELOCITY && directionalDelta > 0) {
2717
2913
  startSwipeRelease(direction);
2718
2914
  return true;
2719
2915
  }
2720
- const shouldClose = directionalDelta > getBaseSwipeThreshold(releasePopupElement, direction);
2916
+ const shouldClose = directionalDelta > baseThreshold;
2721
2917
  if (shouldClose) {
2722
2918
  startSwipeRelease(direction);
2723
2919
  } else {
@@ -2729,20 +2925,20 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2729
2925
  clearSwipeRelease();
2730
2926
  return undefined;
2731
2927
  }
2732
- if (!popupHeight) {
2928
+ if (!popupHeight || resolvedSnapPoints.length === 0) {
2733
2929
  clearSwipeRelease();
2734
- return false;
2930
+ return undefined;
2735
2931
  }
2736
- if (resolvedSnapPoints.length === 0) {
2932
+ const dragDelta = swipeDirection === 'down' ? deltaY : -deltaY;
2933
+ if (!Number.isFinite(dragDelta)) {
2737
2934
  clearSwipeRelease();
2738
2935
  return undefined;
2739
2936
  }
2740
- const dragDelta = swipeDirection === 'down' ? deltaY : -deltaY;
2741
2937
  const dragDirection = Math.sign(dragDelta);
2742
2938
  const releaseDirectionalVelocity = swipeDirection === 'down' ? releaseVelocityY : -releaseVelocityY;
2743
2939
  const fallbackDirectionalVelocity = swipeDirection === 'down' ? velocityY : -velocityY;
2744
- let resolvedDirectionalVelocity = releaseDirectionalVelocity;
2745
- if (dragDirection !== 0 && Math.abs(dragDelta) >= MIN_SWIPE_THRESHOLD) {
2940
+ let resolvedDirectionalVelocity = Number.isFinite(releaseDirectionalVelocity) ? releaseDirectionalVelocity : fallbackDirectionalVelocity;
2941
+ if (dragDirection !== 0 && Math.abs(dragDelta) >= MIN_SWIPE_THRESHOLD && Number.isFinite(resolvedDirectionalVelocity)) {
2746
2942
  const velocityDirection = Math.sign(resolvedDirectionalVelocity);
2747
2943
  if (velocityDirection !== 0 && velocityDirection !== dragDirection) {
2748
2944
  // Ignore touch reversals that would otherwise flip the snap decision.
@@ -2751,20 +2947,39 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2751
2947
  }
2752
2948
  const currentOffset = activeSnapPointOffset ?? 0;
2753
2949
  const dragTargetOffset = clamp(currentOffset + dragDelta, 0, popupHeight);
2754
- const velocityOffset = Math.abs(resolvedDirectionalVelocity) >= SNAP_VELOCITY_THRESHOLD ? clamp(resolvedDirectionalVelocity, -MAX_SNAP_VELOCITY, MAX_SNAP_VELOCITY) * SNAP_VELOCITY_MULTIPLIER : 0;
2950
+ const velocityOffset = Number.isFinite(resolvedDirectionalVelocity) && Math.abs(resolvedDirectionalVelocity) >= SNAP_VELOCITY_THRESHOLD ? clamp(resolvedDirectionalVelocity, -MAX_SNAP_VELOCITY, MAX_SNAP_VELOCITY) * SNAP_VELOCITY_MULTIPLIER : 0;
2755
2951
  const targetOffset = snapToSequentialPoints ? dragTargetOffset : clamp(dragTargetOffset + velocityOffset, 0, popupHeight);
2756
2952
  const snapPointEventDetails = createChangeEventDetails(swipe, event);
2757
2953
  const closeFromSnapPoints = () => {
2758
2954
  pendingSwipeCloseSnapPointRef.current = activeSnapPoint;
2759
- setActiveSnapPoint(null, snapPointEventDetails);
2955
+ setActiveSnapPoint?.(null, snapPointEventDetails);
2760
2956
  startSwipeRelease(swipeDirection);
2761
2957
  return true;
2762
2958
  };
2763
2959
  if (snapToSequentialPoints) {
2764
2960
  const orderedSnapPoints = [...resolvedSnapPoints].sort((first, second) => first.offset - second.offset);
2765
- const orderedOffsets = orderedSnapPoints.map(point => point.offset);
2766
- const currentIndex = closestSnapPointIndex(orderedOffsets, currentOffset);
2767
- let targetSnapPoint = orderedSnapPoints[closestSnapPointIndex(orderedOffsets, targetOffset)];
2961
+ if (orderedSnapPoints.length === 0) {
2962
+ clearSwipeRelease();
2963
+ return false;
2964
+ }
2965
+ let currentIndex = 0;
2966
+ let closestDistance = Math.abs(currentOffset - orderedSnapPoints[0].offset);
2967
+ for (let index = 1; index < orderedSnapPoints.length; index += 1) {
2968
+ const distance = Math.abs(currentOffset - orderedSnapPoints[index].offset);
2969
+ if (distance < closestDistance) {
2970
+ closestDistance = distance;
2971
+ currentIndex = index;
2972
+ }
2973
+ }
2974
+ let targetSnapPoint = orderedSnapPoints[0];
2975
+ closestDistance = Math.abs(targetOffset - targetSnapPoint.offset);
2976
+ for (const snapPoint of orderedSnapPoints) {
2977
+ const distance = Math.abs(targetOffset - snapPoint.offset);
2978
+ if (distance < closestDistance) {
2979
+ closestDistance = distance;
2980
+ targetSnapPoint = snapPoint;
2981
+ }
2982
+ }
2768
2983
  const velocityDirection = Math.sign(resolvedDirectionalVelocity);
2769
2984
  const shouldAdvance = dragDirection !== 0 && velocityDirection !== 0 && velocityDirection === dragDirection && Math.abs(resolvedDirectionalVelocity) >= SNAP_VELOCITY_THRESHOLD;
2770
2985
  let effectiveTargetOffset = targetOffset;
@@ -2781,24 +2996,34 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2781
2996
  return closeFromSnapPoints();
2782
2997
  }
2783
2998
  }
2784
- const closeDistance = Math.abs(effectiveTargetOffset - popupHeight);
2999
+ const closeOffset = popupHeight;
3000
+ const closeDistance = Math.abs(effectiveTargetOffset - closeOffset);
2785
3001
  const snapDistance = Math.abs(effectiveTargetOffset - targetSnapPoint.offset);
2786
3002
  if (closeDistance < snapDistance) {
2787
3003
  return closeFromSnapPoints();
2788
3004
  }
2789
- setActiveSnapPoint(targetSnapPoint.value, snapPointEventDetails);
3005
+ setActiveSnapPoint?.(targetSnapPoint.value, snapPointEventDetails);
2790
3006
  clearSwipeRelease();
2791
3007
  return false;
2792
3008
  }
2793
3009
  if (resolvedDirectionalVelocity >= FAST_SWIPE_VELOCITY && dragDelta > 0) {
2794
3010
  return closeFromSnapPoints();
2795
3011
  }
2796
- const closestSnapPoint = resolvedSnapPoints[closestSnapPointIndex(resolvedSnapPoints.map(point => point.offset), targetOffset)];
2797
- const closeDistance = Math.abs(targetOffset - popupHeight);
2798
- if (closeDistance < Math.abs(targetOffset - closestSnapPoint.offset)) {
3012
+ let closestSnapPoint = resolvedSnapPoints[0];
3013
+ let closestDistance = Math.abs(targetOffset - closestSnapPoint.offset);
3014
+ for (const snapPoint of resolvedSnapPoints) {
3015
+ const distance = Math.abs(targetOffset - snapPoint.offset);
3016
+ if (distance < closestDistance) {
3017
+ closestDistance = distance;
3018
+ closestSnapPoint = snapPoint;
3019
+ }
3020
+ }
3021
+ const closeOffset = popupHeight;
3022
+ const closeDistance = Math.abs(targetOffset - closeOffset);
3023
+ if (closeDistance < closestDistance) {
2799
3024
  return closeFromSnapPoints();
2800
3025
  }
2801
- setActiveSnapPoint(closestSnapPoint.value, snapPointEventDetails);
3026
+ setActiveSnapPoint?.(closestSnapPoint.value, snapPointEventDetails);
2802
3027
  clearSwipeRelease();
2803
3028
  return false;
2804
3029
  },
@@ -2817,7 +3042,7 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2817
3042
  if (dismissEventDetails.isCanceled) {
2818
3043
  const pendingSnapPoint = pendingSwipeCloseSnapPointRef.current;
2819
3044
  if (pendingSnapPoint !== undefined) {
2820
- setActiveSnapPoint(pendingSnapPoint, createChangeEventDetails(swipe, event));
3045
+ setActiveSnapPoint?.(pendingSnapPoint, createChangeEventDetails(swipe, event));
2821
3046
  }
2822
3047
  pendingSwipeCloseSnapPointRef.current = undefined;
2823
3048
  resetSwipeRef.current?.();
@@ -2839,7 +3064,7 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2839
3064
  // Parent rejected: revert animation and restore snap point.
2840
3065
  const pendingSnapPoint = pendingSwipeCloseSnapPointRef.current;
2841
3066
  if (pendingSnapPoint !== undefined) {
2842
- setActiveSnapPoint(pendingSnapPoint, createChangeEventDetails(swipe, savedEvent));
3067
+ setActiveSnapPoint?.(pendingSnapPoint, createChangeEventDetails(swipe, savedEvent));
2843
3068
  }
2844
3069
  pendingSwipeCloseSnapPointRef.current = undefined;
2845
3070
  clearSwipeRelease();
@@ -2869,18 +3094,42 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2869
3094
  }
2870
3095
  const resolvedRootElement = rootElement;
2871
3096
  const doc = ownerDocument(resolvedRootElement);
2872
- function processTouchMove(event, touchState, touch) {
3097
+ const win = getWindow(doc);
3098
+ function handleNativeTouchMove(event) {
3099
+ // The virtual keyboard provider observes the move to tell a tap apart from a drag.
3100
+ // It must run even when the swipe gesture below claims the event with
3101
+ // `stopPropagation()`, which would otherwise prevent React's delegated handlers
3102
+ // (and the provider) from ever seeing the move.
3103
+ virtualKeyboard?.onTouchMove(event);
3104
+ if (ignoreTouchSwipeRef.current) {
3105
+ return;
3106
+ }
3107
+ const touchState = touchScrollStateRef.current;
3108
+ const touch = event.touches[0];
3109
+ if (!touch || !touchState) {
3110
+ return;
3111
+ }
2873
3112
  const drawerAxisDelta = isVerticalScrollAxis ? touch.clientY - touchState.lastY : touch.clientX - touchState.lastX;
2874
3113
 
3114
+ // Preserve native range interaction by never locking touchmove for range inputs.
3115
+ if (isEventOnRangeInput(event, win)) {
3116
+ touchState.allowSwipe = false;
3117
+ updateTouchScrollPosition(touchState, touch);
3118
+ return;
3119
+ }
3120
+
2875
3121
  // Avoid blocking pinch zoom or text selection adjustments on iOS Safari.
2876
3122
  if (event.touches.length === 2) {
3123
+ updateTouchScrollPosition(touchState, touch);
2877
3124
  return;
2878
3125
  }
2879
3126
  const allowTouchMove = shouldIgnoreSwipeForTextSelection(doc, resolvedRootElement);
2880
3127
  if (allowTouchMove || !open || !mounted || nestedDrawerOpen) {
3128
+ updateTouchScrollPosition(touchState, touch);
2881
3129
  return;
2882
3130
  }
2883
- if (shouldYieldTouchMove(touchState, event, touch, isVerticalScrollAxis)) {
3131
+ if (preserveNativeCrossAxisScrollOnMove(touchState, touch, isVerticalScrollAxis)) {
3132
+ updateTouchScrollPosition(touchState, touch);
2884
3133
  return;
2885
3134
  }
2886
3135
  const scrollTarget = touchState.scrollTarget;
@@ -2892,21 +3141,27 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2892
3141
  // move through React re-rasterizes the popup content on every frame.
2893
3142
  event.stopPropagation();
2894
3143
  moveSwipeNative(event, resolvedRootElement);
3144
+ updateTouchScrollPosition(touchState, touch);
2895
3145
  return;
2896
3146
  }
2897
- if (!hasScrollableContentOnAxis(scrollTarget, scrollAxis)) {
3147
+ const hasScrollableContent = hasScrollableContentOnAxis(scrollTarget, scrollAxis);
3148
+ if (!hasScrollableContent) {
2898
3149
  // If the scroll container doesn't overflow on the drawer axis, prevent the window from
2899
3150
  // scrolling instead.
2900
3151
  if (event.cancelable) {
2901
3152
  event.preventDefault();
2902
3153
  }
2903
3154
  event.stopPropagation();
3155
+ updateTouchScrollPosition(touchState, touch);
2904
3156
  return;
2905
3157
  }
2906
- if (drawerAxisDelta !== 0) {
2907
- const canSwipeFromScrollEdge = canSwipeFromScrollEdgeOnMove(scrollTarget, scrollAxis, swipeDirection, drawerAxisDelta);
3158
+ const delta = drawerAxisDelta;
3159
+ if (delta !== 0) {
3160
+ const canSwipeFromScrollEdge = canSwipeFromScrollEdgeOnMove(scrollTarget, scrollAxis, swipeDirection, delta);
2908
3161
  if (!touchState.allowSwipe) {
2909
- if (event.cancelable && canSwipeFromScrollEdge) {
3162
+ if (!event.cancelable) {
3163
+ touchState.allowSwipe = false;
3164
+ } else if (canSwipeFromScrollEdge) {
2910
3165
  touchState.allowSwipe = true;
2911
3166
  event.preventDefault();
2912
3167
  } else {
@@ -2920,22 +3175,6 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2920
3175
  event.stopPropagation();
2921
3176
  moveSwipeNative(event, resolvedRootElement);
2922
3177
  }
2923
- }
2924
- function handleNativeTouchMove(event) {
2925
- // The virtual keyboard provider observes the move to tell a tap apart from a drag.
2926
- // It must run even when the swipe gesture below claims the event with
2927
- // `stopPropagation()`, which would otherwise prevent React's delegated handlers
2928
- // (and the provider) from ever seeing the move.
2929
- virtualKeyboard?.onTouchMove(event);
2930
- if (ignoreTouchSwipeRef.current) {
2931
- return;
2932
- }
2933
- const touchState = touchScrollStateRef.current;
2934
- const touch = event.touches[0];
2935
- if (!touch || !touchState) {
2936
- return;
2937
- }
2938
- processTouchMove(event, touchState, touch);
2939
3178
  updateTouchScrollPosition(touchState, touch);
2940
3179
  }
2941
3180
  return addEventListener(doc, 'touchmove', handleNativeTouchMove, {
@@ -2943,13 +3182,18 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2943
3182
  capture: true
2944
3183
  });
2945
3184
  }, [mounted, nestedDrawerOpen, open, popupElementState, isVerticalScrollAxis, scrollAxis, swipeDirection, moveSwipeNative, viewportElement, virtualKeyboard]);
2946
- useIsoLayoutEffect(() => {
3185
+ React.useEffect(() => {
2947
3186
  if (!snapPointRange || swipe$1.swiping) {
2948
3187
  return;
2949
3188
  }
2950
- applySwipeProgress(!open || nested ? 0 : snapPointProgress ?? 0, true, false);
3189
+ const resolvedProgress = !open || nested ? 0 : snapPointProgress ?? 0;
3190
+ applySwipeProgress({
3191
+ resolvedProgress,
3192
+ shouldTrackProgress: true,
3193
+ notifyParent: false
3194
+ });
2951
3195
  }, [applySwipeProgress, frontmostHeight, nested, notifyParentSwipeProgressChange, open, snapPointProgress, snapPointRange, swipe$1.swiping, store, visualStateStore]);
2952
- useIsoLayoutEffect(() => {
3196
+ React.useEffect(() => {
2953
3197
  if (!notifyParentSwipeProgressChange) {
2954
3198
  return undefined;
2955
3199
  }
@@ -2960,19 +3204,13 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2960
3204
  notifyParentSwipeProgressChange(0);
2961
3205
  };
2962
3206
  }, [notifyParentSwipeProgressChange, open]);
2963
- useIsoLayoutEffect(() => {
3207
+ React.useEffect(() => {
2964
3208
  if (open) {
2965
- // Skip `resetSwipe` while `Drawer.SwipeArea` is driving the open: it zeroes the popup's
2966
- // `--swipe-movement-*` (via `syncDragStyles(false)`), flashing it fully open for a frame.
2967
- // `clearSwipeRelease` doesn't touch those vars, so always run it to clear any leftover
2968
- // release state from a prior dismiss (e.g. when the popup is kept mounted).
2969
- if (!swipeAreaActiveRef.current) {
2970
- resetSwipe();
2971
- }
3209
+ resetSwipe();
2972
3210
  clearSwipeRelease();
2973
3211
  }
2974
- }, [clearSwipeRelease, open, resetSwipe, swipeAreaActiveRef]);
2975
- useIsoLayoutEffect(() => {
3212
+ }, [clearSwipeRelease, open, resetSwipe]);
3213
+ React.useEffect(() => {
2976
3214
  const backdropElement = backdropRef.current;
2977
3215
  return () => {
2978
3216
  visualStateStore?.set({
@@ -2997,19 +3235,12 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2997
3235
  swipeStrength: swipeRelease ?? null,
2998
3236
  setSwipeDismissed
2999
3237
  }), [setSwipeDismissed, swipe$1.getDragStyles, swipe$1.swiping, swipeRelease]);
3000
- function resetTouchSwipeState(ignoreSwipe) {
3001
- ignoreTouchSwipeRef.current = ignoreSwipe;
3002
- touchScrollStateRef.current = null;
3003
- }
3004
3238
  function resetTouchTrackingState() {
3005
- resetTouchSwipeState(false);
3239
+ ignoreTouchSwipeRef.current = false;
3240
+ touchScrollStateRef.current = null;
3006
3241
  lastPointerTypeRef.current = '';
3007
3242
  ignoreNextTouchStartFromPenRef.current = false;
3008
3243
  }
3009
- function handlePointerEnd(event) {
3010
- lastPointerTypeRef.current = '';
3011
- return event.pointerType !== 'touch';
3012
- }
3013
3244
  return /*#__PURE__*/jsx(DialogViewport, {
3014
3245
  ref: forwardedRef,
3015
3246
  className: className,
@@ -3022,7 +3253,8 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3022
3253
  if (!open || !mounted || nestedDrawerOpen) {
3023
3254
  return;
3024
3255
  }
3025
- const elementAtPoint = getElementAtPoint(event.currentTarget.getRootNode(), event.clientX, event.clientY);
3256
+ const doc = ownerDocument(event.currentTarget);
3257
+ const elementAtPoint = getElementAtPoint(doc, event.clientX, event.clientY);
3026
3258
  if (isSwipeIgnoredTarget(elementAtPoint) || isDrawerContentTarget(elementAtPoint)) {
3027
3259
  return;
3028
3260
  }
@@ -3038,24 +3270,34 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3038
3270
  swipePointerProps.onPointerMove?.(event);
3039
3271
  },
3040
3272
  onPointerUp(event) {
3041
- if (handlePointerEnd(event)) {
3042
- swipePointerProps.onPointerUp?.(event);
3273
+ if (lastPointerTypeRef.current === event.pointerType) {
3274
+ lastPointerTypeRef.current = '';
3275
+ }
3276
+ if (event.pointerType === 'touch') {
3277
+ return;
3043
3278
  }
3279
+ swipePointerProps.onPointerUp?.(event);
3044
3280
  },
3045
3281
  onPointerCancel(event) {
3046
- if (handlePointerEnd(event)) {
3047
- swipePointerProps.onPointerCancel?.(event);
3282
+ if (lastPointerTypeRef.current === event.pointerType) {
3283
+ lastPointerTypeRef.current = '';
3284
+ }
3285
+ if (event.pointerType === 'touch') {
3286
+ return;
3048
3287
  }
3288
+ swipePointerProps.onPointerCancel?.(event);
3049
3289
  },
3050
3290
  onTouchStart(event) {
3051
3291
  const startedFromPenPointerDown = lastPointerTypeRef.current === 'pen' && ignoreNextTouchStartFromPenRef.current;
3052
3292
  if (startedFromPenPointerDown) {
3053
3293
  ignoreNextTouchStartFromPenRef.current = false;
3054
- resetTouchSwipeState(false);
3294
+ ignoreTouchSwipeRef.current = false;
3295
+ touchScrollStateRef.current = null;
3055
3296
  return;
3056
3297
  }
3057
3298
  if (!open || !mounted || nestedDrawerOpen) {
3058
- resetTouchSwipeState(false);
3299
+ ignoreTouchSwipeRef.current = false;
3300
+ touchScrollStateRef.current = null;
3059
3301
  return;
3060
3302
  }
3061
3303
  const touch = event.touches[0];
@@ -3063,25 +3305,32 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3063
3305
  return;
3064
3306
  }
3065
3307
  if (isReactTouchEventOnRangeInput(event)) {
3066
- resetTouchSwipeState(false);
3308
+ ignoreTouchSwipeRef.current = false;
3309
+ touchScrollStateRef.current = null;
3067
3310
  return;
3068
3311
  }
3069
- const rootElement = event.currentTarget;
3070
- const elementAtPoint = getElementAtPoint(rootElement.getRootNode(), touch.clientX, touch.clientY);
3312
+ const doc = ownerDocument(event.currentTarget);
3313
+ const elementAtPoint = getElementAtPoint(doc, touch.clientX, touch.clientY);
3314
+ const rootElement = viewportElement ?? popupElementState;
3071
3315
  const eventTarget = getTarget(event.nativeEvent);
3072
- const target = isElement(eventTarget) ? eventTarget : rootElement;
3073
- if (!contains(rootElement, target)) {
3074
- resetTouchSwipeState(true);
3316
+ const target = isElement(eventTarget) ? eventTarget : null;
3317
+ if (rootElement && target && !contains(rootElement, target)) {
3318
+ ignoreTouchSwipeRef.current = true;
3319
+ touchScrollStateRef.current = null;
3075
3320
  return;
3076
3321
  }
3077
3322
  virtualKeyboard?.onTouchStart(event);
3078
- if (isSwipeIgnoredTarget(elementAtPoint)) {
3079
- resetTouchSwipeState(true);
3323
+ ignoreTouchSwipeRef.current = isSwipeIgnoredTarget(elementAtPoint);
3324
+ if (ignoreTouchSwipeRef.current) {
3325
+ touchScrollStateRef.current = null;
3080
3326
  return;
3081
3327
  }
3082
- ignoreTouchSwipeRef.current = false;
3083
- const scrollTarget = findScrollableTouchTarget(target, rootElement, scrollAxis);
3084
- const hasCrossAxisScrollableContent = findScrollableTouchTarget(target, rootElement, crossScrollAxis) != null;
3328
+ let scrollTarget = null;
3329
+ let hasCrossAxisScrollableContent = false;
3330
+ if (rootElement && target) {
3331
+ scrollTarget = findScrollableTouchTarget(target, rootElement, scrollAxis);
3332
+ hasCrossAxisScrollableContent = findScrollableTouchTarget(target, rootElement, crossScrollAxis) != null;
3333
+ }
3085
3334
  let allowSwipe = null;
3086
3335
  if (scrollTarget) {
3087
3336
  const canSwipeFromEdge = isAtSwipeStartEdge(scrollTarget, scrollAxis, swipeDirection);
@@ -3095,8 +3344,7 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3095
3344
  scrollTarget,
3096
3345
  hasCrossAxisScrollableContent,
3097
3346
  allowSwipe,
3098
- preserveNativeCrossAxisScroll: false,
3099
- drawerAxisAttributed: false
3347
+ preserveNativeCrossAxisScroll: false
3100
3348
  };
3101
3349
  swipeTouchProps.onTouchStart?.(event);
3102
3350
  },
@@ -3121,8 +3369,24 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3121
3369
  });
3122
3370
  });
3123
3371
  if (process.env.NODE_ENV !== "production") DrawerViewport.displayName = "DrawerViewport";
3372
+ function setSwipeDismissedElements(popupElement, backdropElement, dismissed) {
3373
+ if (dismissed) {
3374
+ popupElement?.setAttribute(DrawerPopupDataAttributes.swipeDismiss, '');
3375
+ backdropElement?.setAttribute(DrawerPopupDataAttributes.swipeDismiss, '');
3376
+ return;
3377
+ }
3378
+ popupElement?.removeAttribute(DrawerPopupDataAttributes.swipeDismiss);
3379
+ backdropElement?.removeAttribute(DrawerPopupDataAttributes.swipeDismiss);
3380
+ }
3124
3381
  function setBackdropSwipingAttribute(backdropElement, swiping) {
3125
- backdropElement?.toggleAttribute(DrawerPopupDataAttributes.swiping, swiping);
3382
+ if (!backdropElement) {
3383
+ return;
3384
+ }
3385
+ if (swiping) {
3386
+ backdropElement.setAttribute(DrawerPopupDataAttributes.swiping, '');
3387
+ return;
3388
+ }
3389
+ backdropElement.removeAttribute(DrawerPopupDataAttributes.swiping);
3126
3390
  }
3127
3391
  function isSwipeIgnoredTarget(target) {
3128
3392
  return Boolean(target?.closest(BASE_UI_SWIPE_IGNORE_SELECTOR));
@@ -3130,16 +3394,17 @@ function isSwipeIgnoredTarget(target) {
3130
3394
  function isDrawerContentTarget(target) {
3131
3395
  return Boolean(target?.closest(DRAWER_CONTENT_SELECTOR));
3132
3396
  }
3133
- function getBaseSwipeSize(element, direction) {
3134
- return direction === 'left' || direction === 'right' ? element.offsetWidth : element.offsetHeight;
3135
- }
3136
3397
  function getBaseSwipeThreshold(element, direction) {
3137
- return Math.max(getBaseSwipeSize(element, direction) * 0.5, MIN_SWIPE_THRESHOLD);
3398
+ const size = direction === 'left' || direction === 'right' ? element.offsetWidth : element.offsetHeight;
3399
+ return Math.max(size * 0.5, MIN_SWIPE_THRESHOLD);
3138
3400
  }
3139
3401
  function isRangeInput(target, win) {
3140
3402
  return target instanceof win.HTMLInputElement && target.type === 'range';
3141
3403
  }
3142
3404
  function isTextSelectionControl(target) {
3405
+ if (!isElement(target)) {
3406
+ return false;
3407
+ }
3143
3408
  return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA';
3144
3409
  }
3145
3410
  function hasExpandedSelectionWithinTarget(selection, target) {
@@ -3149,7 +3414,8 @@ function hasExpandedSelectionWithinTarget(selection, target) {
3149
3414
  }
3150
3415
  function shouldIgnoreSwipeForTextSelection(doc, rootElement) {
3151
3416
  const activeEl = activeElement(doc);
3152
- if (activeEl && contains(rootElement, activeEl) && isTextSelectionControl(activeEl)) {
3417
+ const activeElementWithinRoot = Boolean(activeEl && contains(rootElement, activeEl));
3418
+ if (activeElementWithinRoot && isTextSelectionControl(activeEl)) {
3153
3419
  const {
3154
3420
  selectionStart,
3155
3421
  selectionEnd
@@ -3165,7 +3431,11 @@ function shouldIgnoreSwipeForTextSelection(doc, rootElement) {
3165
3431
  return hasExpandedSelectionWithinTarget(selection, rootElement);
3166
3432
  }
3167
3433
  function isEventOnRangeInput(event, win) {
3168
- return event.composedPath().some(pathTarget => isRangeInput(pathTarget, win));
3434
+ const composedPath = event.composedPath();
3435
+ if (composedPath) {
3436
+ return composedPath.some(pathTarget => isRangeInput(pathTarget, win));
3437
+ }
3438
+ return isRangeInput(getTarget(event), win);
3169
3439
  }
3170
3440
  function isReactTouchEventOnRangeInput(event) {
3171
3441
  return isEventOnRangeInput(event.nativeEvent, getWindow(event.currentTarget));
@@ -3174,51 +3444,25 @@ function updateTouchScrollPosition(touchState, touch) {
3174
3444
  touchState.lastX = touch.clientX;
3175
3445
  touchState.lastY = touch.clientY;
3176
3446
  }
3177
-
3178
- /**
3179
- * Arbitrates a touchmove between the drawer swipe and a native cross-axis scroll.
3180
- * Returns `true` when the move must be left alone — either because the cross axis already won the
3181
- * gesture, or because neither axis has passed the slop yet and the gesture cannot be attributed.
3182
- */
3183
- function shouldYieldTouchMove(touchState, event, touch, isVerticalScrollAxis) {
3447
+ function preserveNativeCrossAxisScrollOnMove(touchState, touch, isVerticalScrollAxis) {
3184
3448
  if (touchState.preserveNativeCrossAxisScroll) {
3185
3449
  return true;
3186
3450
  }
3187
-
3188
- // Attribution happens once per gesture. Re-arbitrating after the drawer axis has won would let
3189
- // the pre-attribution branches below fire mid-drag (the slop is measured from the touch origin,
3190
- // which is never re-baselined), freezing the popup and dropping `preventDefault()`.
3191
- if (touchState.drawerAxisAttributed || touchState.allowSwipe === true || !touchState.hasCrossAxisScrollableContent) {
3451
+ if (touchState.allowSwipe === true || !touchState.hasCrossAxisScrollableContent) {
3192
3452
  return false;
3193
3453
  }
3194
-
3195
- // A non-cancelable touchmove means the browser has already committed the gesture to a native
3196
- // scroll; claiming it for the swipe would drag the popup alongside the scrolling content.
3197
- if (!event.cancelable) {
3198
- touchState.preserveNativeCrossAxisScroll = true;
3199
- return true;
3200
- }
3201
3454
  const drawerAxisGestureDelta = isVerticalScrollAxis ? touch.clientY - touchState.startY : touch.clientX - touchState.startX;
3202
3455
  const crossAxisGestureDelta = isVerticalScrollAxis ? touch.clientX - touchState.startX : touch.clientY - touchState.startY;
3203
3456
  const absDrawerAxisGestureDelta = Math.abs(drawerAxisGestureDelta);
3204
3457
  const absCrossAxisGestureDelta = Math.abs(crossAxisGestureDelta);
3205
- if (absCrossAxisGestureDelta >= AXIS_LOCK_SLOP && absCrossAxisGestureDelta > absDrawerAxisGestureDelta + AXIS_LOCK_BIAS) {
3206
- touchState.preserveNativeCrossAxisScroll = true;
3207
- return true;
3208
- }
3209
- if (absDrawerAxisGestureDelta >= AXIS_LOCK_SLOP) {
3210
- touchState.drawerAxisAttributed = true;
3458
+ if (absCrossAxisGestureDelta < 6 || absCrossAxisGestureDelta <= absDrawerAxisGestureDelta + 2) {
3211
3459
  return false;
3212
3460
  }
3213
-
3214
- // Neither axis has traveled past the slop yet, so the gesture cannot be attributed. Leave the
3215
- // event alone: on iOS, `preventDefault()` on the first cancelable touchmove cancels native
3216
- // scrolling for the entire gesture, which would lock a cross-axis scroll that only passes the
3217
- // slop on a later move.
3461
+ touchState.preserveNativeCrossAxisScroll = true;
3218
3462
  return true;
3219
3463
  }
3220
3464
  function hasScrollableContentOnAxis(scrollTarget, axis) {
3221
- return getScrollMetrics(scrollTarget, axis).max > 0;
3465
+ return axis === 'vertical' ? scrollTarget.scrollHeight > scrollTarget.clientHeight : scrollTarget.scrollWidth > scrollTarget.clientWidth;
3222
3466
  }
3223
3467
  function getScrollMetrics(scrollTarget, axis) {
3224
3468
  if (axis === 'vertical') {
@@ -3235,23 +3479,48 @@ function getScrollMetrics(scrollTarget, axis) {
3235
3479
  };
3236
3480
  }
3237
3481
  function isAtSwipeStartEdge(scrollTarget, axis, direction) {
3238
- const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
3239
3482
  const {
3240
3483
  offset,
3241
3484
  max
3242
3485
  } = getScrollMetrics(scrollTarget, axis);
3486
+ const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
3487
+ if (dismissFromStartEdge === null) {
3488
+ return false;
3489
+ }
3243
3490
  return dismissFromStartEdge ? offset <= 0 : offset >= max;
3244
3491
  }
3245
3492
  function canSwipeFromScrollEdgeOnMove(scrollTarget, axis, direction, delta) {
3493
+ const {
3494
+ offset,
3495
+ max
3496
+ } = getScrollMetrics(scrollTarget, axis);
3246
3497
  const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
3498
+ if (dismissFromStartEdge === null) {
3499
+ return false;
3500
+ }
3247
3501
  const movingTowardDismiss = dismissFromStartEdge ? delta > 0 : delta < 0;
3248
3502
  if (!movingTowardDismiss) {
3249
3503
  return false;
3250
3504
  }
3251
- return isAtSwipeStartEdge(scrollTarget, axis, direction);
3505
+ return dismissFromStartEdge ? offset <= 0 : offset >= max;
3252
3506
  }
3253
3507
  function shouldDismissFromStartEdge(direction, axis) {
3254
- return axis === 'vertical' ? direction === 'down' : direction === 'right';
3508
+ if (axis === 'vertical') {
3509
+ if (direction === 'down') {
3510
+ return true;
3511
+ }
3512
+ if (direction === 'up') {
3513
+ return false;
3514
+ }
3515
+ return null;
3516
+ }
3517
+ if (direction === 'right') {
3518
+ return true;
3519
+ }
3520
+ if (direction === 'left') {
3521
+ return false;
3522
+ }
3523
+ return null;
3255
3524
  }
3256
3525
 
3257
3526
  const ScrollAreaRootContext = /*#__PURE__*/React.createContext(undefined);
@@ -3264,6 +3533,20 @@ function useScrollAreaRootContext() {
3264
3533
  return context;
3265
3534
  }
3266
3535
 
3536
+ let ScrollAreaRootCssVars = /*#__PURE__*/function (ScrollAreaRootCssVars) {
3537
+ /**
3538
+ * The scroll area's corner height.
3539
+ * @type {number}
3540
+ */
3541
+ ScrollAreaRootCssVars["scrollAreaCornerHeight"] = "--scroll-area-corner-height";
3542
+ /**
3543
+ * The scroll area's corner width.
3544
+ * @type {number}
3545
+ */
3546
+ ScrollAreaRootCssVars["scrollAreaCornerWidth"] = "--scroll-area-corner-width";
3547
+ return ScrollAreaRootCssVars;
3548
+ }({});
3549
+
3267
3550
  const SCROLL_TIMEOUT = 500;
3268
3551
  const MIN_THUMB_SIZE = 16;
3269
3552
 
@@ -3272,28 +3555,108 @@ function getOffset(element, prop, axis) {
3272
3555
  return 0;
3273
3556
  }
3274
3557
  const styles = getComputedStyle(element);
3275
- const key = `${prop}${axis === 'x' ? 'Inline' : 'Block'}`;
3276
- const start = parseFloat(styles[`${key}Start`]);
3558
+ const propAxis = axis === 'x' ? 'Inline' : 'Block';
3277
3559
 
3278
3560
  // Safari misreports `marginInlineEnd` in RTL.
3279
3561
  // We have to assume the start/end values are symmetrical, which is likely.
3280
3562
  if (axis === 'x' && prop === 'margin') {
3281
- return start * 2;
3563
+ return parseFloat(styles[`${prop}InlineStart`]) * 2;
3282
3564
  }
3283
- return start + parseFloat(styles[`${key}End`]);
3565
+ return parseFloat(styles[`${prop}${propAxis}Start`]) + parseFloat(styles[`${prop}${propAxis}End`]);
3284
3566
  }
3285
3567
 
3286
- // Data-attribute strings inlined so `ScrollAreaRootDataAttributes` tree-shakes out.
3287
- const attr = name => value => value ? {
3288
- [name]: ''
3289
- } : null;
3568
+ let ScrollAreaScrollbarDataAttributes = /*#__PURE__*/function (ScrollAreaScrollbarDataAttributes) {
3569
+ /**
3570
+ * Indicates the orientation of the scrollbar.
3571
+ * @type {'horizontal' | 'vertical'}
3572
+ */
3573
+ ScrollAreaScrollbarDataAttributes["orientation"] = "data-orientation";
3574
+ /**
3575
+ * Present when the pointer is over the scroll area.
3576
+ */
3577
+ ScrollAreaScrollbarDataAttributes["hovering"] = "data-hovering";
3578
+ /**
3579
+ * Present when the user scrolls inside the scroll area.
3580
+ */
3581
+ ScrollAreaScrollbarDataAttributes["scrolling"] = "data-scrolling";
3582
+ /**
3583
+ * Present when the scroll area content is wider than the viewport.
3584
+ */
3585
+ ScrollAreaScrollbarDataAttributes["hasOverflowX"] = "data-has-overflow-x";
3586
+ /**
3587
+ * Present when the scroll area content is taller than the viewport.
3588
+ */
3589
+ ScrollAreaScrollbarDataAttributes["hasOverflowY"] = "data-has-overflow-y";
3590
+ /**
3591
+ * Present when there is overflow on the horizontal start side.
3592
+ */
3593
+ ScrollAreaScrollbarDataAttributes["overflowXStart"] = "data-overflow-x-start";
3594
+ /**
3595
+ * Present when there is overflow on the horizontal end side.
3596
+ */
3597
+ ScrollAreaScrollbarDataAttributes["overflowXEnd"] = "data-overflow-x-end";
3598
+ /**
3599
+ * Present when there is overflow on the vertical start side.
3600
+ */
3601
+ ScrollAreaScrollbarDataAttributes["overflowYStart"] = "data-overflow-y-start";
3602
+ /**
3603
+ * Present when there is overflow on the vertical end side.
3604
+ */
3605
+ ScrollAreaScrollbarDataAttributes["overflowYEnd"] = "data-overflow-y-end";
3606
+ return ScrollAreaScrollbarDataAttributes;
3607
+ }({});
3608
+
3609
+ let ScrollAreaRootDataAttributes = /*#__PURE__*/function (ScrollAreaRootDataAttributes) {
3610
+ /**
3611
+ * Present when the user scrolls inside the scroll area.
3612
+ */
3613
+ ScrollAreaRootDataAttributes["scrolling"] = "data-scrolling";
3614
+ /**
3615
+ * Present when the scroll area content is wider than the viewport.
3616
+ */
3617
+ ScrollAreaRootDataAttributes["hasOverflowX"] = "data-has-overflow-x";
3618
+ /**
3619
+ * Present when the scroll area content is taller than the viewport.
3620
+ */
3621
+ ScrollAreaRootDataAttributes["hasOverflowY"] = "data-has-overflow-y";
3622
+ /**
3623
+ * Present when there is overflow on the horizontal start side.
3624
+ */
3625
+ ScrollAreaRootDataAttributes["overflowXStart"] = "data-overflow-x-start";
3626
+ /**
3627
+ * Present when there is overflow on the horizontal end side.
3628
+ */
3629
+ ScrollAreaRootDataAttributes["overflowXEnd"] = "data-overflow-x-end";
3630
+ /**
3631
+ * Present when there is overflow on the vertical start side.
3632
+ */
3633
+ ScrollAreaRootDataAttributes["overflowYStart"] = "data-overflow-y-start";
3634
+ /**
3635
+ * Present when there is overflow on the vertical end side.
3636
+ */
3637
+ ScrollAreaRootDataAttributes["overflowYEnd"] = "data-overflow-y-end";
3638
+ return ScrollAreaRootDataAttributes;
3639
+ }({});
3640
+
3290
3641
  const scrollAreaStateAttributesMapping = {
3291
- hasOverflowX: attr('data-has-overflow-x'),
3292
- hasOverflowY: attr('data-has-overflow-y'),
3293
- overflowXStart: attr('data-overflow-x-start'),
3294
- overflowXEnd: attr('data-overflow-x-end'),
3295
- overflowYStart: attr('data-overflow-y-start'),
3296
- overflowYEnd: attr('data-overflow-y-end'),
3642
+ hasOverflowX: value => value ? {
3643
+ [ScrollAreaRootDataAttributes.hasOverflowX]: ''
3644
+ } : null,
3645
+ hasOverflowY: value => value ? {
3646
+ [ScrollAreaRootDataAttributes.hasOverflowY]: ''
3647
+ } : null,
3648
+ overflowXStart: value => value ? {
3649
+ [ScrollAreaRootDataAttributes.overflowXStart]: ''
3650
+ } : null,
3651
+ overflowXEnd: value => value ? {
3652
+ [ScrollAreaRootDataAttributes.overflowXEnd]: ''
3653
+ } : null,
3654
+ overflowYStart: value => value ? {
3655
+ [ScrollAreaRootDataAttributes.overflowYStart]: ''
3656
+ } : null,
3657
+ overflowYEnd: value => value ? {
3658
+ [ScrollAreaRootDataAttributes.overflowYEnd]: ''
3659
+ } : null,
3297
3660
  cornerHidden: () => null
3298
3661
  };
3299
3662
 
@@ -3359,145 +3722,99 @@ const ScrollAreaRoot = /*#__PURE__*/React.forwardRef(function ScrollAreaRoot(com
3359
3722
  const thumbYRef = React.useRef(null);
3360
3723
  const thumbXRef = React.useRef(null);
3361
3724
  const cornerRef = React.useRef(null);
3362
- const activePointerIdRef = React.useRef(null);
3725
+ const thumbDraggingRef = React.useRef(false);
3363
3726
  const startYRef = React.useRef(0);
3364
3727
  const startXRef = React.useRef(0);
3365
3728
  const startScrollTopRef = React.useRef(0);
3366
3729
  const startScrollLeftRef = React.useRef(0);
3367
3730
  const currentOrientationRef = React.useRef('vertical');
3368
3731
  const scrollPositionRef = React.useRef(DEFAULT_COORDS);
3369
- const savedSnapTypeRef = React.useRef(null);
3370
- function startScrolling(vertical) {
3371
- const setScrolling = vertical ? setScrollingY : setScrollingX;
3372
- const timeout = vertical ? scrollYTimeout : scrollXTimeout;
3373
- setScrolling(true);
3374
- timeout.start(SCROLL_TIMEOUT, () => {
3375
- setScrolling(false);
3376
- });
3377
- }
3378
3732
  const handleScroll = useStableCallback(scrollPosition => {
3379
3733
  const offsetX = scrollPosition.x - scrollPositionRef.current.x;
3380
3734
  const offsetY = scrollPosition.y - scrollPositionRef.current.y;
3381
3735
  scrollPositionRef.current = scrollPosition;
3382
3736
  if (offsetY !== 0) {
3383
- startScrolling(true);
3737
+ setScrollingY(true);
3738
+ scrollYTimeout.start(SCROLL_TIMEOUT, () => {
3739
+ setScrollingY(false);
3740
+ });
3384
3741
  }
3385
3742
  if (offsetX !== 0) {
3386
- startScrolling(false);
3387
- }
3388
- });
3389
-
3390
- // CSS scroll snap forces every programmatic scroll to land on a snap
3391
- // point, making thumb dragging jump between snap points. Native
3392
- // scrollbars suppress snapping while dragging, so disable it until the
3393
- // pointer is released; restoring the value re-snaps the viewport. The
3394
- // save is guarded so a second pointer during an active drag can't
3395
- // clobber the saved value with `none`.
3396
- const disableViewportSnap = useStableCallback(() => {
3397
- const viewportEl = viewportRef.current;
3398
- if (viewportEl && savedSnapTypeRef.current === null) {
3399
- savedSnapTypeRef.current = viewportEl.style.scrollSnapType;
3400
- viewportEl.style.scrollSnapType = 'none';
3743
+ setScrollingX(true);
3744
+ scrollXTimeout.start(SCROLL_TIMEOUT, () => {
3745
+ setScrollingX(false);
3746
+ });
3401
3747
  }
3402
3748
  });
3403
3749
  const handlePointerDown = useStableCallback(event => {
3404
3750
  if (event.button !== 0) {
3405
3751
  return;
3406
3752
  }
3407
- if (activePointerIdRef.current !== null) {
3408
- const activeThumb = currentOrientationRef.current === 'vertical' ? thumbYRef.current : thumbXRef.current;
3409
- // A live drag holds capture for the active pointer — ignore other pointers.
3410
- // No capture means the release went missing entirely (silent capture drop
3411
- // with an id that never reappears, e.g. a lost touch contact), so let the
3412
- // new pointer take over the latch instead of leaving dragging dead.
3413
- if (activeThumb?.hasPointerCapture(activePointerIdRef.current)) {
3414
- return;
3415
- }
3416
- }
3417
- activePointerIdRef.current = event.pointerId;
3753
+ thumbDraggingRef.current = true;
3418
3754
  startYRef.current = event.clientY;
3419
3755
  startXRef.current = event.clientX;
3420
- // Literal instead of `ScrollAreaScrollbarDataAttributes.orientation`: referencing an
3421
- // enum member retains its whole object in the bundle, so the strings are inlined and
3422
- // the enums kept for docs only.
3423
- currentOrientationRef.current = event.currentTarget.getAttribute('data-orientation');
3424
- const viewportEl = viewportRef.current;
3425
- if (viewportEl) {
3426
- startScrollTopRef.current = viewportEl.scrollTop;
3427
- startScrollLeftRef.current = viewportEl.scrollLeft;
3428
- disableViewportSnap();
3429
- }
3430
- const thumb = currentOrientationRef.current === 'vertical' ? thumbYRef.current : thumbXRef.current;
3431
- thumb?.setPointerCapture(event.pointerId);
3432
- });
3433
- const handlePointerUp = useStableCallback(event => {
3434
- if (event.pointerId !== activePointerIdRef.current) {
3435
- return;
3756
+ currentOrientationRef.current = event.currentTarget.getAttribute(ScrollAreaScrollbarDataAttributes.orientation);
3757
+ if (viewportRef.current) {
3758
+ startScrollTopRef.current = viewportRef.current.scrollTop;
3759
+ startScrollLeftRef.current = viewportRef.current.scrollLeft;
3436
3760
  }
3437
- activePointerIdRef.current = null;
3438
- // Clear the drag's scrolling state immediately rather than waiting for the
3439
- // `SCROLL_TIMEOUT` timer armed by the last drag move, so every release path
3440
- // (real, `pointercancel`, or the missed-release fallback) behaves the same.
3441
- (currentOrientationRef.current === 'vertical' ? setScrollingY : setScrollingX)(false);
3442
- if (savedSnapTypeRef.current !== null) {
3443
- if (viewportRef.current) {
3444
- viewportRef.current.style.scrollSnapType = savedSnapTypeRef.current;
3445
- }
3446
- savedSnapTypeRef.current = null;
3761
+ if (thumbYRef.current && currentOrientationRef.current === 'vertical') {
3762
+ thumbYRef.current.setPointerCapture(event.pointerId);
3447
3763
  }
3448
- const thumb = currentOrientationRef.current === 'vertical' ? thumbYRef.current : thumbXRef.current;
3449
- // `pointercancel` releases capture implicitly, so guard against releasing a
3450
- // capture we no longer hold (which would throw).
3451
- if (thumb?.hasPointerCapture(event.pointerId)) {
3452
- thumb.releasePointerCapture(event.pointerId);
3764
+ if (thumbXRef.current && currentOrientationRef.current === 'horizontal') {
3765
+ thumbXRef.current.setPointerCapture(event.pointerId);
3453
3766
  }
3454
3767
  });
3455
3768
  const handlePointerMove = useStableCallback(event => {
3456
- if (event.pointerId !== activePointerIdRef.current) {
3457
- return;
3458
- }
3459
-
3460
- // The release can go missing entirely (e.g. the browser drops pointer
3461
- // capture while the scrollbar is hidden mid-drag), leaving the drag
3462
- // latched so a buttonless hover over the thumb scrolls the viewport.
3463
- // Treat a move without the primary button held (`buttons` bit 1 unset)
3464
- // as the missed release.
3465
- if (event.buttons % 2 === 0) {
3466
- handlePointerUp(event);
3769
+ if (!thumbDraggingRef.current) {
3467
3770
  return;
3468
3771
  }
3469
- const viewportEl = viewportRef.current;
3470
- if (!viewportEl) {
3471
- return;
3772
+ const deltaY = event.clientY - startYRef.current;
3773
+ const deltaX = event.clientX - startXRef.current;
3774
+ if (viewportRef.current) {
3775
+ const scrollableContentHeight = viewportRef.current.scrollHeight;
3776
+ const viewportHeight = viewportRef.current.clientHeight;
3777
+ const scrollableContentWidth = viewportRef.current.scrollWidth;
3778
+ const viewportWidth = viewportRef.current.clientWidth;
3779
+ if (thumbYRef.current && scrollbarYRef.current && currentOrientationRef.current === 'vertical') {
3780
+ const scrollbarYOffset = getOffset(scrollbarYRef.current, 'padding', 'y');
3781
+ const thumbYOffset = getOffset(thumbYRef.current, 'margin', 'y');
3782
+ const thumbHeight = thumbYRef.current.offsetHeight;
3783
+ const maxThumbOffsetY = scrollbarYRef.current.offsetHeight - thumbHeight - scrollbarYOffset - thumbYOffset;
3784
+ const scrollRatioY = deltaY / maxThumbOffsetY;
3785
+ viewportRef.current.scrollTop = startScrollTopRef.current + scrollRatioY * (scrollableContentHeight - viewportHeight);
3786
+ event.preventDefault();
3787
+ setScrollingY(true);
3788
+ scrollYTimeout.start(SCROLL_TIMEOUT, () => {
3789
+ setScrollingY(false);
3790
+ });
3791
+ }
3792
+ if (thumbXRef.current && scrollbarXRef.current && currentOrientationRef.current === 'horizontal') {
3793
+ const scrollbarXOffset = getOffset(scrollbarXRef.current, 'padding', 'x');
3794
+ const thumbXOffset = getOffset(thumbXRef.current, 'margin', 'x');
3795
+ const thumbWidth = thumbXRef.current.offsetWidth;
3796
+ const maxThumbOffsetX = scrollbarXRef.current.offsetWidth - thumbWidth - scrollbarXOffset - thumbXOffset;
3797
+ const scrollRatioX = deltaX / maxThumbOffsetX;
3798
+ viewportRef.current.scrollLeft = startScrollLeftRef.current + scrollRatioX * (scrollableContentWidth - viewportWidth);
3799
+ event.preventDefault();
3800
+ setScrollingX(true);
3801
+ scrollXTimeout.start(SCROLL_TIMEOUT, () => {
3802
+ setScrollingX(false);
3803
+ });
3804
+ }
3472
3805
  }
3473
- const vertical = currentOrientationRef.current === 'vertical';
3474
- const thumbEl = vertical ? thumbYRef.current : thumbXRef.current;
3475
- const scrollbarEl = vertical ? scrollbarYRef.current : scrollbarXRef.current;
3476
- if (!thumbEl || !scrollbarEl) {
3477
- return;
3806
+ });
3807
+ const handlePointerUp = useStableCallback(event => {
3808
+ thumbDraggingRef.current = false;
3809
+
3810
+ // `pointercancel` releases capture implicitly, so guard against releasing a
3811
+ // capture we no longer hold (which would throw).
3812
+ if (thumbYRef.current && currentOrientationRef.current === 'vertical' && thumbYRef.current.hasPointerCapture(event.pointerId)) {
3813
+ thumbYRef.current.releasePointerCapture(event.pointerId);
3478
3814
  }
3479
- const axis = vertical ? 'y' : 'x';
3480
- const scrollbarOffset = getOffset(scrollbarEl, 'padding', axis);
3481
- const thumbOffset = getOffset(thumbEl, 'margin', axis);
3482
- const thumbSizePx = vertical ? thumbEl.offsetHeight : thumbEl.offsetWidth;
3483
- const trackSize = vertical ? scrollbarEl.offsetHeight : scrollbarEl.offsetWidth;
3484
- const maxThumbOffset = trackSize - thumbSizePx - scrollbarOffset - thumbOffset;
3485
- // A short or heavily padded track can drive `maxThumbOffset` to zero or
3486
- // negative once the thumb hits its `MIN_THUMB_SIZE` floor. Dividing by it
3487
- // would yield a non-finite (`Infinity`/`NaN`) or inverted scroll position.
3488
- const delta = vertical ? event.clientY - startYRef.current : event.clientX - startXRef.current;
3489
- const scrollRatio = maxThumbOffset <= 0 ? 0 : delta / maxThumbOffset;
3490
- const scrollableSize = vertical ? viewportEl.scrollHeight : viewportEl.scrollWidth;
3491
- const viewportSize = vertical ? viewportEl.clientHeight : viewportEl.clientWidth;
3492
- const startScroll = vertical ? startScrollTopRef.current : startScrollLeftRef.current;
3493
- const nextScroll = startScroll + scrollRatio * (scrollableSize - viewportSize);
3494
- if (vertical) {
3495
- viewportEl.scrollTop = nextScroll;
3496
- } else {
3497
- viewportEl.scrollLeft = nextScroll;
3815
+ if (thumbXRef.current && currentOrientationRef.current === 'horizontal' && thumbXRef.current.hasPointerCapture(event.pointerId)) {
3816
+ thumbXRef.current.releasePointerCapture(event.pointerId);
3498
3817
  }
3499
- event.preventDefault();
3500
- startScrolling(vertical);
3501
3818
  });
3502
3819
  function handleTouchModalityChange(event) {
3503
3820
  setTouchModality(event.pointerType === 'touch');
@@ -3529,8 +3846,8 @@ const ScrollAreaRoot = /*#__PURE__*/React.forwardRef(function ScrollAreaRoot(com
3529
3846
  },
3530
3847
  style: {
3531
3848
  position: 'relative',
3532
- ['--scroll-area-corner-height']: `${cornerSize.height}px`,
3533
- ['--scroll-area-corner-width']: `${cornerSize.width}px`
3849
+ [ScrollAreaRootCssVars.scrollAreaCornerHeight]: `${cornerSize.height}px`,
3850
+ [ScrollAreaRootCssVars.scrollAreaCornerWidth]: `${cornerSize.width}px`
3534
3851
  }
3535
3852
  };
3536
3853
  const element = useRenderElement('div', componentProps, {
@@ -3544,7 +3861,6 @@ const ScrollAreaRoot = /*#__PURE__*/React.forwardRef(function ScrollAreaRoot(com
3544
3861
  handlePointerMove,
3545
3862
  handlePointerUp,
3546
3863
  handleScroll,
3547
- disableViewportSnap,
3548
3864
  cornerSize,
3549
3865
  setCornerSize,
3550
3866
  thumbSize,
@@ -3554,10 +3870,13 @@ const ScrollAreaRoot = /*#__PURE__*/React.forwardRef(function ScrollAreaRoot(com
3554
3870
  touchModality,
3555
3871
  cornerRef,
3556
3872
  scrollingX,
3873
+ setScrollingX,
3557
3874
  scrollingY,
3875
+ setScrollingY,
3558
3876
  hovering,
3559
3877
  setHovering,
3560
3878
  viewportRef,
3879
+ rootRef,
3561
3880
  scrollbarYRef,
3562
3881
  scrollbarXRef,
3563
3882
  thumbYRef,
@@ -3574,7 +3893,7 @@ const ScrollAreaRoot = /*#__PURE__*/React.forwardRef(function ScrollAreaRoot(com
3574
3893
  yStart,
3575
3894
  yEnd
3576
3895
  }
3577
- }), [handlePointerDown, handlePointerMove, handlePointerUp, handleScroll, disableViewportSnap, cornerSize, thumbSize, hasMeasuredScrollbar, touchModality, scrollingX, scrollingY, hovering, rootId, hiddenState, overflowEdges, state, xStart, xEnd, yStart, yEnd]);
3896
+ }), [handlePointerDown, handlePointerMove, handlePointerUp, handleScroll, cornerSize, thumbSize, hasMeasuredScrollbar, touchModality, scrollingX, setScrollingX, scrollingY, setScrollingY, hovering, setHovering, rootId, hiddenState, overflowEdges, state, xStart, xEnd, yStart, yEnd]);
3578
3897
  return /*#__PURE__*/jsxs(ScrollAreaRootContext.Provider, {
3579
3898
  value: contextValue,
3580
3899
  children: [!disableStyleElements && styleDisableScrollbar.getElement(nonce), element]
@@ -3582,17 +3901,20 @@ const ScrollAreaRoot = /*#__PURE__*/React.forwardRef(function ScrollAreaRoot(com
3582
3901
  });
3583
3902
  if (process.env.NODE_ENV !== "production") ScrollAreaRoot.displayName = "ScrollAreaRoot";
3584
3903
  function normalizeOverflowEdgeThreshold(threshold) {
3585
- const thresholds = typeof threshold === 'number' ? {
3586
- xStart: threshold,
3587
- xEnd: threshold,
3588
- yStart: threshold,
3589
- yEnd: threshold
3590
- } : threshold;
3904
+ if (typeof threshold === 'number') {
3905
+ const value = Math.max(0, threshold);
3906
+ return {
3907
+ xStart: value,
3908
+ xEnd: value,
3909
+ yStart: value,
3910
+ yEnd: value
3911
+ };
3912
+ }
3591
3913
  return {
3592
- xStart: Math.max(0, thresholds?.xStart || 0),
3593
- xEnd: Math.max(0, thresholds?.xEnd || 0),
3594
- yStart: Math.max(0, thresholds?.yStart || 0),
3595
- yEnd: Math.max(0, thresholds?.yEnd || 0)
3914
+ xStart: Math.max(0, threshold?.xStart || 0),
3915
+ xEnd: Math.max(0, threshold?.xEnd || 0),
3916
+ yStart: Math.max(0, threshold?.yStart || 0),
3917
+ yEnd: Math.max(0, threshold?.yEnd || 0)
3596
3918
  };
3597
3919
  }
3598
3920
 
@@ -3606,10 +3928,30 @@ function useScrollAreaViewportContext() {
3606
3928
  return context;
3607
3929
  }
3608
3930
 
3609
- const OVERFLOW_EDGE_VARS = ['--scroll-area-overflow-x-start', '--scroll-area-overflow-x-end', '--scroll-area-overflow-y-start', '--scroll-area-overflow-y-end'];
3931
+ let ScrollAreaViewportCssVars = /*#__PURE__*/function (ScrollAreaViewportCssVars) {
3932
+ /**
3933
+ * The distance from the horizontal start edge in pixels.
3934
+ * @type {number}
3935
+ */
3936
+ ScrollAreaViewportCssVars["scrollAreaOverflowXStart"] = "--scroll-area-overflow-x-start";
3937
+ /**
3938
+ * The distance from the horizontal end edge in pixels.
3939
+ * @type {number}
3940
+ */
3941
+ ScrollAreaViewportCssVars["scrollAreaOverflowXEnd"] = "--scroll-area-overflow-x-end";
3942
+ /**
3943
+ * The distance from the vertical start edge in pixels.
3944
+ * @type {number}
3945
+ */
3946
+ ScrollAreaViewportCssVars["scrollAreaOverflowYStart"] = "--scroll-area-overflow-y-start";
3947
+ /**
3948
+ * The distance from the vertical end edge in pixels.
3949
+ * @type {number}
3950
+ */
3951
+ ScrollAreaViewportCssVars["scrollAreaOverflowYEnd"] = "--scroll-area-overflow-y-end";
3952
+ return ScrollAreaViewportCssVars;
3953
+ }({});
3610
3954
 
3611
- // Module-level flag to ensure we only register the CSS properties once,
3612
- // regardless of how many Scroll Area components are mounted.
3613
3955
  let scrollAreaOverflowVarsRegistered = false;
3614
3956
 
3615
3957
  /**
@@ -3628,7 +3970,7 @@ function removeCSSVariableInheritance() {
3628
3970
  return;
3629
3971
  }
3630
3972
  if (typeof CSS !== 'undefined' && 'registerProperty' in CSS) {
3631
- OVERFLOW_EDGE_VARS.forEach(name => {
3973
+ [ScrollAreaViewportCssVars.scrollAreaOverflowXStart, ScrollAreaViewportCssVars.scrollAreaOverflowXEnd, ScrollAreaViewportCssVars.scrollAreaOverflowYStart, ScrollAreaViewportCssVars.scrollAreaOverflowYEnd].forEach(name => {
3632
3974
  try {
3633
3975
  CSS.registerProperty({
3634
3976
  name,
@@ -3672,11 +4014,12 @@ const ScrollAreaViewport = /*#__PURE__*/React.forwardRef(function ScrollAreaView
3672
4014
  hiddenState,
3673
4015
  setHasMeasuredScrollbar,
3674
4016
  handleScroll,
3675
- touchModality,
3676
4017
  setHovering,
3677
4018
  setOverflowEdges,
4019
+ overflowEdges,
3678
4020
  overflowEdgeThreshold,
3679
- viewportState
4021
+ scrollingX,
4022
+ scrollingY
3680
4023
  } = useScrollAreaRootContext();
3681
4024
  const direction = useDirection();
3682
4025
  const programmaticScrollRef = React.useRef(true);
@@ -3721,12 +4064,18 @@ const ScrollAreaViewport = /*#__PURE__*/React.forwardRef(function ScrollAreaView
3721
4064
  let scrollLeftFromStart = 0;
3722
4065
  let scrollLeftFromEnd = 0;
3723
4066
  if (!scrollbarXHidden) {
3724
- // `normalizeScrollOffset` clamps internally.
3725
- scrollLeftFromStart = normalizeScrollOffset(direction === 'rtl' ? -scrollLeft : scrollLeft, maxScrollLeft);
4067
+ let rawScrollLeftFromStart = 0;
4068
+ if (direction === 'rtl') {
4069
+ rawScrollLeftFromStart = clamp(-scrollLeft, 0, maxScrollLeft);
4070
+ } else {
4071
+ rawScrollLeftFromStart = clamp(scrollLeft, 0, maxScrollLeft);
4072
+ }
4073
+ scrollLeftFromStart = normalizeScrollOffset(rawScrollLeftFromStart, maxScrollLeft);
3726
4074
  scrollLeftFromEnd = maxScrollLeft - scrollLeftFromStart;
3727
4075
  }
3728
- const scrollTopFromStart = scrollbarYHidden ? 0 : normalizeScrollOffset(scrollTop, maxScrollTop);
3729
- const scrollTopFromEnd = scrollbarYHidden ? 0 : maxScrollTop - scrollTopFromStart;
4076
+ const rawScrollTopFromStart = !scrollbarYHidden ? clamp(scrollTop, 0, maxScrollTop) : 0;
4077
+ const scrollTopFromStart = !scrollbarYHidden ? normalizeScrollOffset(rawScrollTopFromStart, maxScrollTop) : 0;
4078
+ const scrollTopFromEnd = !scrollbarYHidden ? maxScrollTop - scrollTopFromStart : 0;
3730
4079
  const nextWidth = scrollbarXHidden ? 0 : viewportWidth;
3731
4080
  const nextHeight = scrollbarYHidden ? 0 : viewportHeight;
3732
4081
  let nextCornerWidth = 0;
@@ -3751,53 +4100,76 @@ const ScrollAreaViewport = /*#__PURE__*/React.forwardRef(function ScrollAreaView
3751
4100
  const maxNextHeight = scrollbarYEl ? Math.min(scrollbarYEl.offsetHeight - cornerHeightOffset, idealNextHeight) : idealNextHeight;
3752
4101
  const clampedNextWidth = Math.max(MIN_THUMB_SIZE, maxNextWidth * ratioX);
3753
4102
  const clampedNextHeight = Math.max(MIN_THUMB_SIZE, maxNextHeight * ratioY);
3754
- setThumbSize(prevSize => pickState(prevSize, {
3755
- width: clampedNextWidth,
3756
- height: clampedNextHeight
3757
- }));
4103
+ setThumbSize(prevSize => {
4104
+ if (prevSize.height === clampedNextHeight && prevSize.width === clampedNextWidth) {
4105
+ return prevSize;
4106
+ }
4107
+ return {
4108
+ width: clampedNextWidth,
4109
+ height: clampedNextHeight
4110
+ };
4111
+ });
3758
4112
 
3759
4113
  // Handle Y (vertical) scroll
3760
4114
  if (scrollbarYEl && thumbYEl) {
3761
4115
  const maxThumbOffsetY = scrollbarYEl.offsetHeight - clampedNextHeight - scrollbarYOffset - thumbYOffset;
3762
- const thumbOffsetY = applyOverscrollThumb(thumbYEl, '--scroll-area-thumb-height', scrollTop, maxScrollTop, scrollableContentHeight, clampedNextHeight, maxThumbOffsetY);
4116
+ const scrollRangeY = scrollableContentHeight - viewportHeight;
4117
+ const scrollRatioY = scrollRangeY === 0 ? 0 : scrollTop / scrollRangeY;
4118
+
4119
+ // In Safari, don't allow it to go negative or too far as `scrollTop` considers the rubber
4120
+ // band effect.
4121
+ const thumbOffsetY = Math.min(maxThumbOffsetY, Math.max(0, scrollRatioY * maxThumbOffsetY));
3763
4122
  thumbYEl.style.transform = `translate3d(0,${thumbOffsetY}px,0)`;
3764
4123
  }
3765
4124
 
3766
4125
  // Handle X (horizontal) scroll
3767
4126
  if (scrollbarXEl && thumbXEl) {
3768
4127
  const maxThumbOffsetX = scrollbarXEl.offsetWidth - clampedNextWidth - scrollbarXOffset - thumbXOffset;
3769
- // RTL scrolls from 0 down to `-maxScrollLeft`; measure from the inline start edge so the
3770
- // overscroll math is direction-agnostic, then flip the resulting offset back below.
3771
- const scrollFromStart = direction === 'rtl' ? -scrollLeft : scrollLeft;
3772
- const offsetX = applyOverscrollThumb(thumbXEl, '--scroll-area-thumb-width', scrollFromStart, maxScrollLeft, scrollableContentWidth, clampedNextWidth, maxThumbOffsetX);
3773
- thumbXEl.style.transform = `translate3d(${direction === 'rtl' ? -offsetX : offsetX}px,0,0)`;
3774
- }
3775
- const overflowMetricsPx = [scrollLeftFromStart, scrollLeftFromEnd, scrollTopFromStart, scrollTopFromEnd];
3776
- OVERFLOW_EDGE_VARS.forEach((cssVar, index) => {
3777
- viewportEl.style.setProperty(cssVar, `${overflowMetricsPx[index]}px`);
3778
- });
4128
+ const scrollRangeX = scrollableContentWidth - viewportWidth;
4129
+ const scrollRatioX = scrollRangeX === 0 ? 0 : scrollLeft / scrollRangeX;
4130
+
4131
+ // In Safari, don't allow it to go negative or too far as `scrollLeft` considers the rubber
4132
+ // band effect.
4133
+ const thumbOffsetX = direction === 'rtl' ? clamp(scrollRatioX * maxThumbOffsetX, -maxThumbOffsetX, 0) : clamp(scrollRatioX * maxThumbOffsetX, 0, maxThumbOffsetX);
4134
+ thumbXEl.style.transform = `translate3d(${thumbOffsetX}px,0,0)`;
4135
+ }
4136
+ const overflowMetricsPx = [[ScrollAreaViewportCssVars.scrollAreaOverflowXStart, scrollLeftFromStart], [ScrollAreaViewportCssVars.scrollAreaOverflowXEnd, scrollLeftFromEnd], [ScrollAreaViewportCssVars.scrollAreaOverflowYStart, scrollTopFromStart], [ScrollAreaViewportCssVars.scrollAreaOverflowYEnd, scrollTopFromEnd]];
4137
+ for (const [cssVar, value] of overflowMetricsPx) {
4138
+ viewportEl.style.setProperty(cssVar, `${value}px`);
4139
+ }
3779
4140
  if (cornerEl) {
3780
- // Bail when the size is unchanged (like `setThumbSize` above); otherwise a
3781
- // fresh object literal on every scroll frame rebuilds the root context and
3782
- // re-renders every scroll-area part.
3783
- // `nextCornerWidth`/`nextCornerHeight` stay 0 when either scrollbar is hidden.
3784
- setCornerSize(prevSize => pickState(prevSize, {
3785
- width: nextCornerWidth,
3786
- height: nextCornerHeight
3787
- }));
3788
- }
3789
- setHiddenState(prevState => pickState(prevState, nextHiddenState));
4141
+ if (scrollbarXHidden || scrollbarYHidden) {
4142
+ setCornerSize({
4143
+ width: 0,
4144
+ height: 0
4145
+ });
4146
+ } else if (!scrollbarXHidden && !scrollbarYHidden) {
4147
+ setCornerSize({
4148
+ width: nextCornerWidth,
4149
+ height: nextCornerHeight
4150
+ });
4151
+ }
4152
+ }
4153
+ setHiddenState(prevState => mergeHiddenState(prevState, nextHiddenState));
3790
4154
  const nextOverflowEdges = {
3791
4155
  xStart: !scrollbarXHidden && scrollLeftFromStart > overflowEdgeThreshold.xStart,
3792
4156
  xEnd: !scrollbarXHidden && scrollLeftFromEnd > overflowEdgeThreshold.xEnd,
3793
4157
  yStart: !scrollbarYHidden && scrollTopFromStart > overflowEdgeThreshold.yStart,
3794
4158
  yEnd: !scrollbarYHidden && scrollTopFromEnd > overflowEdgeThreshold.yEnd
3795
4159
  };
3796
- setOverflowEdges(prev => pickState(prev, nextOverflowEdges));
4160
+ setOverflowEdges(prev => {
4161
+ if (prev.xStart === nextOverflowEdges.xStart && prev.xEnd === nextOverflowEdges.xEnd && prev.yStart === nextOverflowEdges.yStart && prev.yEnd === nextOverflowEdges.yEnd) {
4162
+ return prev;
4163
+ }
4164
+ return nextOverflowEdges;
4165
+ });
3797
4166
  });
3798
4167
  useIsoLayoutEffect(() => {
4168
+ if (!viewportRef.current) {
4169
+ return;
4170
+ }
3799
4171
  removeCSSVariableInheritance();
3800
- }, []);
4172
+ }, [viewportRef]);
3801
4173
  useIsoLayoutEffect(() => {
3802
4174
  // Wait for scrollbar and thumb refs after hidden-state toggles, refresh math on direction
3803
4175
  // flips, and re-evaluate overflow edges when the threshold changes.
@@ -3840,10 +4212,6 @@ const ScrollAreaViewport = /*#__PURE__*/React.forwardRef(function ScrollAreaView
3840
4212
  if (animations.length === 0) {
3841
4213
  return;
3842
4214
  }
3843
-
3844
- // `allSettled` never rejects, but `computeThumbPosition` can still run against a
3845
- // torn-down tree once the animations resolve. Swallow instead of leaking an unhandled
3846
- // rejection; `void` alone would only silence the floating-promise lint.
3847
4215
  Promise.allSettled(animations.map(animation => animation.finished)).then(computeThumbPosition).catch(() => {});
3848
4216
  });
3849
4217
  return () => {
@@ -3871,13 +4239,7 @@ const ScrollAreaViewport = /*#__PURE__*/React.forwardRef(function ScrollAreaView
3871
4239
  return;
3872
4240
  }
3873
4241
  computeThumbPosition();
3874
-
3875
- // WebKit consumes a touch that catches an in-flight momentum scroll or
3876
- // rubber-band bounce without dispatching any DOM events for the whole
3877
- // gesture (not even `touchstart`), so scrolls cannot be attributed to
3878
- // the user through events. Treat every scroll in touch modality as
3879
- // user-driven instead.
3880
- if (touchModality || !programmaticScrollRef.current) {
4242
+ if (!programmaticScrollRef.current) {
3881
4243
  handleScroll({
3882
4244
  x: viewportRef.current.scrollLeft,
3883
4245
  y: viewportRef.current.scrollTop
@@ -3895,10 +4257,21 @@ const ScrollAreaViewport = /*#__PURE__*/React.forwardRef(function ScrollAreaView
3895
4257
  });
3896
4258
  },
3897
4259
  onWheel: handleUserInteraction,
4260
+ onTouchMove: handleUserInteraction,
3898
4261
  onPointerMove: handleUserInteraction,
3899
4262
  onPointerEnter: handleUserInteraction,
3900
4263
  onKeyDown: handleUserInteraction
3901
4264
  };
4265
+ const viewportState = React.useMemo(() => ({
4266
+ scrolling: scrollingX || scrollingY,
4267
+ hasOverflowX: !hiddenState.x,
4268
+ hasOverflowY: !hiddenState.y,
4269
+ overflowXStart: overflowEdges.xStart,
4270
+ overflowXEnd: overflowEdges.xEnd,
4271
+ overflowYStart: overflowEdges.yStart,
4272
+ overflowYEnd: overflowEdges.yEnd,
4273
+ cornerHidden: hiddenState.corner
4274
+ }), [scrollingX, scrollingY, hiddenState.x, hiddenState.y, hiddenState.corner, overflowEdges]);
3902
4275
  const element = useRenderElement('div', componentProps, {
3903
4276
  ref: [forwardedRef, viewportRef],
3904
4277
  state: viewportState,
@@ -3923,37 +4296,11 @@ function getHiddenState(viewport) {
3923
4296
  corner: y || x
3924
4297
  };
3925
4298
  }
3926
-
3927
- /**
3928
- * Returns `prev` when `next` is shallow-equal to it so setState bails out and
3929
- * scroll-frame updates don't rebuild the root context.
3930
- */
3931
- function pickState(prev, next) {
3932
- for (const key in next) {
3933
- if (prev[key] !== next[key]) {
3934
- return next;
3935
- }
4299
+ function mergeHiddenState(prevState, nextState) {
4300
+ if (prevState.y === nextState.y && prevState.x === nextState.x && prevState.corner === nextState.corner) {
4301
+ return prevState;
3936
4302
  }
3937
- return prev;
3938
- }
3939
-
3940
- /**
3941
- * Sizes the thumb and returns its axis offset. On overscroll (Safari rubber-band only) it shrinks
3942
- * against the pinned edge, damped by `content / (content + overscroll)` to match native feedback;
3943
- * the size flows through the thumb-size variable so the resting `var(...)` still applies.
3944
- */
3945
- function applyOverscrollThumb(thumbEl, sizeVar, scrollFromStart, maxScroll, content, size, maxThumbOffset) {
3946
- const clamped = clamp(scrollFromStart, 0, maxScroll);
3947
- const overscroll = scrollFromStart - clamped;
3948
- const nextSize = Math.max(MIN_THUMB_SIZE, size * content / (content + Math.abs(overscroll)));
3949
-
3950
- // Passing an empty string removes the override, restoring the resting `var(...)` size.
3951
- thumbEl.style.setProperty(sizeVar, overscroll ? `${nextSize}px` : '');
3952
-
3953
- // Slide proportionally; at the end edge push down by the shrink so the thumb stays pinned to
3954
- // it, while a start overscroll pins to offset 0.
3955
- const offset = maxScroll ? clamped / maxScroll * maxThumbOffset : 0;
3956
- return offset + (overscroll > 0 ? size - nextSize : 0);
4303
+ return nextState;
3957
4304
  }
3958
4305
 
3959
- export { getElementTransform as A, DrawerVirtualKeyboardContext as B, getElementAtPoint as C, DrawerTrigger as D, useScrollAreaRootContext as E, getOffset as F, scrollAreaStateAttributesMapping as G, useScrollAreaViewportContext as H, NullStore as N, ScrollAreaRoot as S, DrawerRoot as a, DrawerPortal as b, DrawerBackdrop as c, DrawerViewport as d, DrawerPopup as e, ScrollAreaViewport as f, DrawerContent as g, DialogTrigger as h, DialogPortal as i, DialogViewport as j, useDialogPortalContext as k, dialogStateAttributesMapping as l, useRenderDialogRoot as m, createNullDialogStore as n, createInitialPopupStoreState as o, popupStoreSelectors as p, usePopupHandleStore as q, useDrawerProviderContext as r, DrawerBackdropCssVars as s, DrawerPopupCssVars as t, useDialogRootContext as u, DrawerProviderContext as v, useDrawerRootContext as w, useSwipeDismiss as x, getDisplacement as y, DrawerPopupDataAttributes as z };
4306
+ export { getElementAtPoint as A, findScrollableTouchTarget as B, useScrollAreaRootContext as C, DrawerTrigger as D, getOffset as E, ScrollAreaRootCssVars as F, scrollAreaStateAttributesMapping as G, useScrollAreaViewportContext as H, ScrollAreaRoot as S, DrawerRoot as a, DrawerPortal as b, DrawerBackdrop as c, DrawerViewport as d, DrawerPopup as e, ScrollAreaViewport as f, DrawerContent as g, DialogRoot as h, DialogTrigger as i, DialogPortal as j, DialogViewport as k, useDialogPortalContext as l, DialogStore as m, createInitialPopupStoreState as n, useDrawerProviderContext as o, popupStoreSelectors as p, DrawerBackdropCssVars as q, DrawerPopupCssVars as r, DrawerProviderContext as s, useDrawerRootContext as t, useDialogRootContext as u, useSwipeDismiss as v, getDisplacement as w, DrawerPopupDataAttributes as x, getElementTransform as y, DrawerVirtualKeyboardContext as z };