@jobber/components 9.5.3 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/Combobox.d.ts +1 -0
  2. package/Combobox.js +17 -0
  3. package/dist/Autocomplete/index.cjs +0 -2
  4. package/dist/Autocomplete/index.mjs +0 -2
  5. package/dist/Autocomplete-cjs.js +4 -5
  6. package/dist/Autocomplete-es.js +1 -2
  7. package/dist/Card/index.cjs +2 -2
  8. package/dist/Card/index.mjs +2 -2
  9. package/dist/Chip/index.cjs +0 -2
  10. package/dist/Chip/index.mjs +0 -2
  11. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +0 -2
  12. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +0 -2
  13. package/dist/Chips/InternalChipDismissible/index.cjs +0 -2
  14. package/dist/Chips/InternalChipDismissible/index.mjs +0 -2
  15. package/dist/Chips/index.cjs +0 -2
  16. package/dist/Chips/index.mjs +0 -2
  17. package/dist/Combobox/Combobox.contentSlots.d.ts +5 -0
  18. package/dist/Combobox/Combobox.context.d.ts +34 -0
  19. package/dist/Combobox/Combobox.d.ts +69 -0
  20. package/dist/Combobox/Combobox.footer.d.ts +7 -0
  21. package/dist/Combobox/Combobox.items.d.ts +25 -0
  22. package/dist/Combobox/Combobox.responsive.d.ts +54 -0
  23. package/dist/Combobox/Combobox.selectAll.d.ts +4 -0
  24. package/dist/Combobox/Combobox.slotFactory.d.ts +7 -0
  25. package/dist/Combobox/Combobox.testUtils.d.ts +12 -0
  26. package/dist/Combobox/Combobox.types.d.ts +166 -0
  27. package/dist/Combobox/index.cjs +35 -0
  28. package/dist/Combobox/index.d.ts +2 -0
  29. package/dist/Combobox/index.mjs +29 -0
  30. package/dist/Combobox-cjs.js +675 -0
  31. package/dist/Combobox-es.js +673 -0
  32. package/dist/ComboboxPrimitive-cjs.js +88 -47
  33. package/dist/ComboboxPrimitive-es.js +39 -23
  34. package/dist/{ComboboxChipRemove-cjs.js → ComboboxSeparator-cjs.js} +501 -455
  35. package/dist/{ComboboxChipRemove-es.js → ComboboxSeparator-es.js} +500 -456
  36. package/dist/ConfirmationModal/index.cjs +0 -2
  37. package/dist/ConfirmationModal/index.mjs +0 -2
  38. package/dist/ContentBlock/index.cjs +1 -0
  39. package/dist/ContentBlock/index.mjs +1 -0
  40. package/dist/ContentBlock-cjs.js +2 -18
  41. package/dist/ContentBlock-es.js +1 -17
  42. package/dist/DataDump/index.cjs +2 -2
  43. package/dist/DataDump/index.mjs +2 -2
  44. package/dist/DataList/components/DataListActions/index.cjs +0 -2
  45. package/dist/DataList/components/DataListActions/index.mjs +0 -2
  46. package/dist/DataList/components/DataListBulkActions/index.cjs +0 -2
  47. package/dist/DataList/components/DataListBulkActions/index.mjs +0 -2
  48. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +0 -2
  49. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +0 -2
  50. package/dist/DataList/components/DataListFilters/index.cjs +0 -2
  51. package/dist/DataList/components/DataListFilters/index.mjs +0 -2
  52. package/dist/DataList/components/DataListHeader/index.cjs +0 -2
  53. package/dist/DataList/components/DataListHeader/index.mjs +0 -2
  54. package/dist/DataList/components/DataListItem/index.cjs +0 -2
  55. package/dist/DataList/components/DataListItem/index.mjs +0 -2
  56. package/dist/DataList/components/DataListItemActions/index.cjs +0 -2
  57. package/dist/DataList/components/DataListItemActions/index.mjs +0 -2
  58. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +0 -2
  59. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +0 -2
  60. package/dist/DataList/components/DataListItems/index.cjs +0 -2
  61. package/dist/DataList/components/DataListItems/index.mjs +0 -2
  62. package/dist/DataList/components/DataListLayout/index.cjs +0 -2
  63. package/dist/DataList/components/DataListLayout/index.mjs +0 -2
  64. package/dist/DataList/components/DataListLayoutActions/index.cjs +0 -2
  65. package/dist/DataList/components/DataListLayoutActions/index.mjs +0 -2
  66. package/dist/DataList/index.cjs +0 -2
  67. package/dist/DataList/index.mjs +0 -2
  68. package/dist/DatePicker/index.cjs +0 -2
  69. package/dist/DatePicker/index.mjs +0 -2
  70. package/dist/DatePicker-cjs.js +4 -5
  71. package/dist/DatePicker-es.js +1 -2
  72. package/dist/Dialog/index.cjs +2 -2
  73. package/dist/Dialog/index.mjs +2 -2
  74. package/dist/DrawerDescription-cjs.js +268 -99
  75. package/dist/DrawerDescription-es.js +270 -103
  76. package/dist/DrawerTitle-cjs.js +2 -6
  77. package/dist/DrawerTitle-es.js +3 -7
  78. package/dist/FieldDescription-cjs.js +184 -181
  79. package/dist/FieldDescription-es.js +187 -184
  80. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +0 -2
  81. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +0 -2
  82. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +0 -2
  83. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +0 -2
  84. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +0 -2
  85. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +0 -2
  86. package/dist/FilterPicker/index.cjs +0 -2
  87. package/dist/FilterPicker/index.mjs +0 -2
  88. package/dist/FilterPickerContent-cjs.js +4 -5
  89. package/dist/FilterPickerContent-es.js +1 -2
  90. package/dist/FormatFile/index.cjs +0 -2
  91. package/dist/FormatFile/index.mjs +0 -2
  92. package/dist/Gallery/index.cjs +0 -2
  93. package/dist/Gallery/index.mjs +0 -2
  94. package/dist/InputDate/index.cjs +0 -2
  95. package/dist/InputDate/index.mjs +0 -2
  96. package/dist/InputNumber/index.cjs +2 -2
  97. package/dist/InputNumber/index.mjs +2 -2
  98. package/dist/InputNumber/types.d.ts +5 -0
  99. package/dist/InputNumber-cjs.js +2 -2
  100. package/dist/InputNumber-es.js +2 -2
  101. package/dist/LightBox/index.cjs +0 -2
  102. package/dist/LightBox/index.mjs +0 -2
  103. package/dist/Menu/index.cjs +2 -2
  104. package/dist/Menu/index.mjs +2 -2
  105. package/dist/Menu-cjs.js +4 -5
  106. package/dist/Menu-es.js +1 -2
  107. package/dist/MenuSubmenuTrigger-cjs.js +206 -164
  108. package/dist/MenuSubmenuTrigger-es.js +194 -152
  109. package/dist/Modal/index.cjs +0 -2
  110. package/dist/Modal/index.mjs +1 -3
  111. package/dist/NumberFieldInput-cjs.js +187 -308
  112. package/dist/NumberFieldInput-es.js +187 -308
  113. package/dist/Page/index.cjs +2 -2
  114. package/dist/Page/index.mjs +2 -2
  115. package/dist/Popover/index.cjs +0 -2
  116. package/dist/Popover/index.mjs +0 -2
  117. package/dist/Popover-cjs.js +7 -8
  118. package/dist/Popover-es.js +1 -2
  119. package/dist/ScrollAreaViewport-cjs.js +747 -1093
  120. package/dist/ScrollAreaViewport-es.js +734 -1081
  121. package/dist/Select/index.cjs +2 -2
  122. package/dist/Select/index.mjs +2 -2
  123. package/dist/SelectGroupLabel-cjs.js +207 -306
  124. package/dist/SelectGroupLabel-es.js +203 -302
  125. package/dist/SelectPrimitive-cjs.js +11 -17
  126. package/dist/SelectPrimitive-es.js +1 -7
  127. package/dist/Tooltip/index.cjs +0 -2
  128. package/dist/Tooltip/index.mjs +0 -2
  129. package/dist/Tooltip-cjs.js +6 -7
  130. package/dist/Tooltip-es.js +1 -2
  131. package/dist/buttonRenderAdapter-cjs.js +2 -1
  132. package/dist/buttonRenderAdapter-es.js +2 -1
  133. package/dist/dialogReturnFocus-cjs.js +1 -1
  134. package/dist/dialogReturnFocus-es.js +2 -2
  135. package/dist/docs/InputNumber/InputNumber.md +2 -2
  136. package/dist/docs/SelectPrimitive/SelectPrimitive.md +1 -1
  137. package/dist/floating-ui.react-cjs.js +2557 -185
  138. package/dist/floating-ui.react-es.js +2512 -149
  139. package/dist/getMappedBreakpointWidth-cjs.js +21 -0
  140. package/dist/getMappedBreakpointWidth-es.js +18 -0
  141. package/dist/index.cjs +15 -10
  142. package/dist/index.d.mts +1 -0
  143. package/dist/index.d.ts +1 -0
  144. package/dist/index.mjs +14 -10
  145. package/dist/mergeClassName-cjs.js +12 -0
  146. package/dist/mergeClassName-es.js +10 -0
  147. package/dist/primitives/BottomSheet/index.cjs +1 -1
  148. package/dist/primitives/BottomSheet/index.mjs +1 -1
  149. package/dist/primitives/ComboboxPrimitive/ComboboxPrimitive.d.ts +2 -2
  150. package/dist/primitives/ComboboxPrimitive/index.cjs +4 -5
  151. package/dist/primitives/ComboboxPrimitive/index.mjs +4 -5
  152. package/dist/primitives/SelectPrimitive/index.cjs +1 -2
  153. package/dist/primitives/SelectPrimitive/index.mjs +1 -2
  154. package/dist/primitives/index.cjs +3 -3
  155. package/dist/primitives/index.mjs +3 -3
  156. package/dist/scrollEdges-cjs.js +1 -11
  157. package/dist/scrollEdges-es.js +1 -11
  158. package/dist/scrollable-cjs.js +58 -0
  159. package/dist/scrollable-es.js +54 -0
  160. package/dist/styles.css +759 -42
  161. package/dist/unstyledPrimitives/index.cjs +720 -478
  162. package/dist/unstyledPrimitives/index.mjs +663 -421
  163. package/dist/useCompositeListItem-cjs.js +2108 -361
  164. package/dist/useCompositeListItem-es.js +2076 -331
  165. package/dist/useLabel-cjs.js +2 -3
  166. package/dist/useLabel-es.js +2 -3
  167. package/dist/useRenderElement-cjs.js +9 -7
  168. package/dist/useRenderElement-es.js +9 -7
  169. package/dist/useScrollLock-cjs.js +819 -344
  170. package/dist/useScrollLock-es.js +732 -296
  171. package/dist/useScrollToActive-cjs.js +4 -5
  172. package/dist/useScrollToActive-es.js +1 -2
  173. package/dist/utils/meta/meta.json +43 -1
  174. package/package.json +8 -3
  175. package/dist/floating-ui.react-dom-cjs.js +0 -2103
  176. package/dist/floating-ui.react-dom-es.js +0 -2072
  177. package/dist/floating-ui.utils-cjs.js +0 -342
  178. package/dist/floating-ui.utils-es.js +0 -300
@@ -1,24 +1,23 @@
1
1
  'use strict';
2
2
 
3
3
  var React = require('react');
4
- var ComboboxChipRemove = require('../ComboboxChipRemove-cjs.js');
4
+ var ComboboxSeparator = require('../ComboboxSeparator-cjs.js');
5
5
  var useCompositeListItem = require('../useCompositeListItem-cjs.js');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
7
  var useScrollLock = require('../useScrollLock-cjs.js');
8
- var Separator = require('../Separator-cjs.js');
9
8
  var DrawerDescription = require('../DrawerDescription-cjs.js');
10
9
  var DrawerTitle = require('../DrawerTitle-cjs.js');
11
10
  var ScrollAreaViewport = require('../ScrollAreaViewport-cjs.js');
12
11
  var useRenderElement = require('../useRenderElement-cjs.js');
13
- var floatingUi_utils = require('../floating-ui.utils-cjs.js');
14
12
  var scrollEdges = require('../scrollEdges-cjs.js');
13
+ var scrollable = require('../scrollable-cjs.js');
15
14
  var MenuSubmenuTrigger = require('../MenuSubmenuTrigger-cjs.js');
16
15
  var ReactDOM = require('react-dom');
17
16
  var SelectGroupLabel = require('../SelectGroupLabel-cjs.js');
17
+ var Separator = require('../Separator-cjs.js');
18
18
  var NumberFieldInput = require('../NumberFieldInput-cjs.js');
19
19
  var useLabel = require('../useLabel-cjs.js');
20
20
  require('../stringifyLocale-cjs.js');
21
- require('../floating-ui.react-dom-cjs.js');
22
21
 
23
22
  function _interopNamespaceDefault(e) {
24
23
  var n = Object.create(null);
@@ -73,7 +72,9 @@ function AutocompleteRoot(props) {
73
72
  } else {
74
73
  resolvedInputValue = internalValue;
75
74
  }
76
- const collator = ComboboxChipRemove.useCoreFilter();
75
+ const collator = ComboboxSeparator.useCoreFilter({
76
+ locale: other.locale
77
+ });
77
78
  const baseFilter = React__namespace.useMemo(() => {
78
79
  if (other.filter !== undefined) {
79
80
  return other.filter;
@@ -106,17 +107,9 @@ function AutocompleteRoot(props) {
106
107
  if (eventDetails.reason === useScrollLock.pointer) {
107
108
  return;
108
109
  }
109
- if (enableInline) {
110
- if (highlightedValue == null) {
111
- setInlineInputValue('');
112
- } else {
113
- setInlineInputValue(useCompositeListItem.stringifyAsLabel(highlightedValue, itemToStringValue));
114
- }
115
- } else {
116
- setInlineInputValue('');
117
- }
110
+ setInlineInputValue(enableInline && highlightedValue != null ? useCompositeListItem.stringifyAsLabel(highlightedValue, itemToStringValue) : '');
118
111
  }
119
- return /*#__PURE__*/jsxRuntime.jsx(ComboboxChipRemove.AriaCombobox, {
112
+ return /*#__PURE__*/jsxRuntime.jsx(ComboboxSeparator.AriaCombobox, {
120
113
  ...other,
121
114
  itemToStringLabel: itemToStringValue,
122
115
  openOnInputClick: openOnInputClick,
@@ -135,7 +128,7 @@ function AutocompleteValue(props) {
135
128
  const {
136
129
  children
137
130
  } = props;
138
- const inputValue = ComboboxChipRemove.useComboboxInputValueContext();
131
+ const inputValue = ComboboxSeparator.useComboboxInputValueContext();
139
132
  let returnValue = null;
140
133
  if (typeof children === 'function') {
141
134
  returnValue = children(String(inputValue));
@@ -155,7 +148,7 @@ function AutocompleteValue(props) {
155
148
  *
156
149
  * Documentation: [Base UI Autocomplete](https://base-ui.com/react/components/autocomplete)
157
150
  */
158
- const AutocompleteTrigger = ComboboxChipRemove.ComboboxTrigger;
151
+ const AutocompleteTrigger = ComboboxSeparator.ComboboxTrigger;
159
152
 
160
153
  /**
161
154
  * A wrapper for the input and its associated controls.
@@ -163,7 +156,7 @@ const AutocompleteTrigger = ComboboxChipRemove.ComboboxTrigger;
163
156
  *
164
157
  * Documentation: [Base UI Autocomplete](https://base-ui.com/react/components/autocomplete)
165
158
  */
166
- const AutocompleteInputGroup = ComboboxChipRemove.ComboboxInputGroup;
159
+ const AutocompleteInputGroup = ComboboxSeparator.ComboboxInputGroup;
167
160
 
168
161
  /**
169
162
  * An individual item in the list.
@@ -171,33 +164,41 @@ const AutocompleteInputGroup = ComboboxChipRemove.ComboboxInputGroup;
171
164
  *
172
165
  * Documentation: [Base UI Autocomplete](https://base-ui.com/react/components/autocomplete)
173
166
  */
174
- const AutocompleteItem = ComboboxChipRemove.ComboboxItem;
167
+ const AutocompleteItem = ComboboxSeparator.ComboboxItem;
168
+
169
+ /**
170
+ * A visual separator between items or groups.
171
+ * Renders a `<div>` element.
172
+ *
173
+ * Documentation: [Base UI Autocomplete](https://base-ui.com/react/components/autocomplete)
174
+ */
175
+ const AutocompleteSeparator = ComboboxSeparator.ListboxSeparator;
175
176
 
176
177
  var index_parts$7 = /*#__PURE__*/Object.freeze({
177
178
  __proto__: null,
178
- Arrow: ComboboxChipRemove.ComboboxArrow,
179
- Backdrop: ComboboxChipRemove.ComboboxBackdrop,
180
- Clear: ComboboxChipRemove.ComboboxClear,
181
- Collection: ComboboxChipRemove.ComboboxCollection,
182
- Empty: ComboboxChipRemove.ComboboxEmpty,
183
- Group: ComboboxChipRemove.ComboboxGroup,
184
- GroupLabel: ComboboxChipRemove.ComboboxGroupLabel,
185
- Icon: ComboboxChipRemove.ComboboxIcon,
186
- Input: ComboboxChipRemove.ComboboxInput,
179
+ Arrow: ComboboxSeparator.ComboboxArrow,
180
+ Backdrop: ComboboxSeparator.ComboboxBackdrop,
181
+ Clear: ComboboxSeparator.ComboboxClear,
182
+ Collection: ComboboxSeparator.ComboboxCollection,
183
+ Empty: ComboboxSeparator.ComboboxEmpty,
184
+ Group: ComboboxSeparator.ComboboxGroup,
185
+ GroupLabel: ComboboxSeparator.ComboboxGroupLabel,
186
+ Icon: ComboboxSeparator.ComboboxIcon,
187
+ Input: ComboboxSeparator.ComboboxInput,
187
188
  InputGroup: AutocompleteInputGroup,
188
189
  Item: AutocompleteItem,
189
- List: ComboboxChipRemove.ComboboxList,
190
- Popup: ComboboxChipRemove.ComboboxPopup,
191
- Portal: ComboboxChipRemove.ComboboxPortal,
192
- Positioner: ComboboxChipRemove.ComboboxPositioner,
190
+ List: ComboboxSeparator.ComboboxList,
191
+ Popup: ComboboxSeparator.ComboboxPopup,
192
+ Portal: ComboboxSeparator.ComboboxPortal,
193
+ Positioner: ComboboxSeparator.ComboboxPositioner,
193
194
  Root: AutocompleteRoot,
194
- Row: ComboboxChipRemove.ComboboxRow,
195
- Separator: Separator.Separator,
196
- Status: ComboboxChipRemove.ComboboxStatus,
195
+ Row: ComboboxSeparator.ComboboxRow,
196
+ Separator: AutocompleteSeparator,
197
+ Status: ComboboxSeparator.ComboboxStatus,
197
198
  Trigger: AutocompleteTrigger,
198
199
  Value: AutocompleteValue,
199
- useFilter: ComboboxChipRemove.useCoreFilter,
200
- useFilteredItems: ComboboxChipRemove.useFilteredItems
200
+ useFilter: ComboboxSeparator.useCoreFilter,
201
+ useFilteredItems: ComboboxSeparator.useFilteredItems
201
202
  });
202
203
 
203
204
  var index_parts$6 = /*#__PURE__*/Object.freeze({
@@ -208,14 +209,14 @@ var index_parts$6 = /*#__PURE__*/Object.freeze({
208
209
  Handle: DrawerDescription.DialogHandle,
209
210
  Popup: DrawerDescription.DialogPopup,
210
211
  Portal: ScrollAreaViewport.DialogPortal,
211
- Root: ScrollAreaViewport.DialogRoot,
212
+ Root: DrawerDescription.DialogRoot,
212
213
  Title: DrawerTitle.DialogTitle,
213
214
  Trigger: ScrollAreaViewport.DialogTrigger,
214
215
  Viewport: ScrollAreaViewport.DialogViewport,
215
216
  createHandle: DrawerDescription.createDialogHandle
216
217
  });
217
218
 
218
- const stateAttributesMapping$5 = {
219
+ const stateAttributesMapping$3 = {
219
220
  active(value) {
220
221
  if (value) {
221
222
  return {
@@ -286,12 +287,12 @@ const DrawerIndent = /*#__PURE__*/React__namespace.forwardRef(function DrawerInd
286
287
  [ScrollAreaViewport.DrawerBackdropCssVars.swipeProgress]: '0'
287
288
  }
288
289
  }, elementProps],
289
- stateAttributesMapping: stateAttributesMapping$5
290
+ stateAttributesMapping: stateAttributesMapping$3
290
291
  });
291
292
  });
292
293
  if (process.env.NODE_ENV !== "production") DrawerIndent.displayName = "DrawerIndent";
293
294
 
294
- const stateAttributesMapping$4 = {
295
+ const stateAttributesMapping$2 = {
295
296
  active(value) {
296
297
  if (value) {
297
298
  return {
@@ -326,7 +327,7 @@ const DrawerIndentBackground = /*#__PURE__*/React__namespace.forwardRef(function
326
327
  ref: forwardedRef,
327
328
  state,
328
329
  props: elementProps,
329
- stateAttributesMapping: stateAttributesMapping$4
330
+ stateAttributesMapping: stateAttributesMapping$2
330
331
  });
331
332
  });
332
333
  if (process.env.NODE_ENV !== "production") DrawerIndentBackground.displayName = "DrawerIndentBackground";
@@ -335,37 +336,26 @@ function DrawerProvider(props) {
335
336
  const {
336
337
  children
337
338
  } = props;
338
- const [openById, setOpenById] = React__namespace.useState(() => new Map());
339
+ const [openDrawers, setOpenDrawers] = React__namespace.useState(() => new Set());
339
340
  const [visualStateStore] = React__namespace.useState(createVisualStateStore);
340
- const setDrawerOpen = useScrollLock.useStableCallback((drawerId, open) => {
341
- setOpenById(prev => {
342
- const prevOpen = prev.get(drawerId);
343
- if (prevOpen === open) {
341
+ const setDrawerOpen = useScrollLock.useStableCallback((drawer, open) => {
342
+ setOpenDrawers(prev => {
343
+ if (prev.has(drawer) === open) {
344
344
  return prev;
345
345
  }
346
- const next = new Map(prev);
347
- next.set(drawerId, open);
348
- return next;
349
- });
350
- });
351
- const removeDrawer = useScrollLock.useStableCallback(drawerId => {
352
- setOpenById(prev => {
353
- if (!prev.has(drawerId)) {
354
- return prev;
346
+ const next = new Set(prev);
347
+ if (open) {
348
+ next.add(drawer);
349
+ } else {
350
+ next.delete(drawer);
355
351
  }
356
- const next = new Map(prev);
357
- next.delete(drawerId);
358
352
  return next;
359
353
  });
360
354
  });
361
- const active = React__namespace.useMemo(() => {
362
- for (const open of openById.values()) {
363
- if (open) {
364
- return true;
365
- }
366
- }
367
- return false;
368
- }, [openById]);
355
+ const removeDrawer = useScrollLock.useStableCallback(drawer => {
356
+ setDrawerOpen(drawer, false);
357
+ });
358
+ const active = openDrawers.size > 0;
369
359
  const contextValue = React__namespace.useMemo(() => ({
370
360
  setDrawerOpen,
371
361
  removeDrawer,
@@ -455,7 +445,7 @@ const SWIPE_AREA_SWIPING_HOOK = {
455
445
  const SWIPE_AREA_DISABLED_HOOK = {
456
446
  [DrawerSwipeAreaDataAttributes.disabled]: ''
457
447
  };
458
- const stateAttributesMapping$3 = {
448
+ const stateAttributesMapping$1 = {
459
449
  open(value) {
460
450
  return value ? SWIPE_AREA_OPEN_HOOK : SWIPE_AREA_CLOSED_HOOK;
461
451
  },
@@ -463,9 +453,9 @@ const stateAttributesMapping$3 = {
463
453
  return value ? SWIPE_AREA_SWIPING_HOOK : null;
464
454
  },
465
455
  swipeDirection(value) {
466
- return value ? {
456
+ return {
467
457
  [DrawerSwipeAreaDataAttributes.swipeDirection]: value
468
- } : null;
458
+ };
469
459
  },
470
460
  disabled(value) {
471
461
  return value ? SWIPE_AREA_DISABLED_HOOK : null;
@@ -496,16 +486,14 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
496
486
  swipeDirection: swipeDirectionProp,
497
487
  ...elementProps
498
488
  } = componentProps;
499
- const {
500
- store
501
- } = ScrollAreaViewport.useDialogRootContext();
489
+ const store = ScrollAreaViewport.useDialogRootContext();
502
490
  const {
503
491
  swipeDirection,
504
- frontmostHeight
492
+ frontmostHeight,
493
+ swipeAreaActiveRef
505
494
  } = ScrollAreaViewport.useDrawerRootContext();
506
495
  const providerContext = ScrollAreaViewport.useDrawerProviderContext();
507
496
  const [swipeActive, setSwipeActive] = React__namespace.useState(false);
508
- const releaseDismissTimeout = useScrollLock.useTimeout();
509
497
  const swipeAreaRef = React__namespace.useRef(null);
510
498
  const swipeStartEventRef = React__namespace.useRef(null);
511
499
  const openedBySwipeRef = React__namespace.useRef(false);
@@ -515,7 +503,10 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
515
503
  });
516
504
  const closedOffsetRef = React__namespace.useRef(null);
517
505
  const appliedSwipeStylesRef = React__namespace.useRef(false);
506
+ const swipePopupElementRef = React__namespace.useRef(null);
507
+ const swipeBackdropElementRef = React__namespace.useRef(null);
518
508
  const popupTransitionRef = React__namespace.useRef(null);
509
+ const releaseGuardCleanupRef = React__namespace.useRef(useRenderElement.NOOP);
519
510
  const swipeAreaId = useScrollLock.useBaseUiId(componentProps.id);
520
511
  const registerTrigger = useScrollLock.useTriggerRegistration(swipeAreaId, store);
521
512
  const open = store.useState('open');
@@ -527,21 +518,40 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
527
518
  const dismissDirection = oppositeSwipeDirection[resolvedSwipeDirection];
528
519
  const enabled = !disabled && (!open || swipeActive);
529
520
  function disableDismissForSwipe() {
530
- releaseDismissTimeout.clear();
521
+ releaseGuardCleanupRef.current();
531
522
  store.context.outsidePressEnabledRef.current = false;
532
523
  }
533
- function enableDismissAfterRelease() {
534
- // Safari can dispatch outside-press for the same swipe-open gesture
535
- // after release, so defer re-enabling dismissal to the next macrotask.
536
- releaseDismissTimeout.start(0, () => {
524
+ const enableDismissAfterRelease = useScrollLock.useStableCallback(() => {
525
+ releaseGuardCleanupRef.current();
526
+ const doc = useScrollLock.ownerDocument(swipeAreaRef.current);
527
+ function restore(event) {
528
+ // The gesture's trailing release click is the one physical click with no `pointerdown` of
529
+ // its own. Ignore it and keep waiting, so it cannot dismiss the drawer it just opened,
530
+ // while a click-only activation (keyboard or assistive tech) still re-enables in time.
531
+ if (event?.type === 'click' && event.detail !== 0 && !useScrollLock.isVirtualClick(event)) {
532
+ return;
533
+ }
534
+ releaseGuardCleanupRef.current = useRenderElement.NOOP;
535
+ doc.removeEventListener('pointerdown', restore, true);
536
+ doc.removeEventListener('click', restore, true);
537
537
  store.context.outsidePressEnabledRef.current = true;
538
- });
539
- }
540
- function resolvePopupSize() {
541
- const popupElement = store.context.popupRef.current;
542
- if (!popupElement) {
543
- return null;
544
538
  }
539
+
540
+ // The pointerup that ends a swipe-open gesture synthesizes a `click`. When the drag released
541
+ // outside the popup (e.g. it was dragged past the popup's size), that click would be treated as
542
+ // an outside press and immediately dismiss the drawer that was just opened. Keep outside-press
543
+ // dismissal disabled until the next interaction that isn't that release click: a deliberate
544
+ // outside press starts with a `pointerdown`, and a click-only activation (keyboard or
545
+ // assistive tech) is distinguishable from a physical release. This is deterministic, unlike
546
+ // re-enabling on a timer that can race the synthesized click and dismiss at random.
547
+ //
548
+ // `restore` runs in document capture, ahead of floating-ui's own outside-press check (which
549
+ // happens on the event target, after capture), so the triggering press still dismisses.
550
+ releaseGuardCleanupRef.current = restore;
551
+ doc.addEventListener('pointerdown', restore, true);
552
+ doc.addEventListener('click', restore, true);
553
+ });
554
+ function getPopupSize(popupElement) {
545
555
  const isHorizontal = dismissDirection === 'left' || dismissDirection === 'right';
546
556
  const size = isHorizontal ? popupElement.offsetWidth : popupElement.offsetHeight;
547
557
  if (size <= 0) {
@@ -549,15 +559,15 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
549
559
  }
550
560
  return size;
551
561
  }
552
- function resolveClosedOffset() {
553
- const offset = resolvePopupSize();
562
+ function resolvePopupSize() {
563
+ const popupElement = store.context.popupRef.current;
564
+ return popupElement ? getPopupSize(popupElement) : null;
565
+ }
566
+ function resolveClosedOffset(popupElement) {
567
+ const offset = getPopupSize(popupElement);
554
568
  if (offset == null) {
555
569
  return null;
556
570
  }
557
- const popupElement = store.context.popupRef.current;
558
- if (!popupElement) {
559
- return offset;
560
- }
561
571
  const isHorizontal = dismissDirection === 'left' || dismissDirection === 'right';
562
572
  const transform = ScrollAreaViewport.getElementTransform(popupElement);
563
573
  const transformOffset = isHorizontal ? transform.x : transform.y;
@@ -574,9 +584,6 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
574
584
  return popupSize * DEFAULT_SWIPE_OPEN_RATIO;
575
585
  }
576
586
  function applySwipeMovement() {
577
- if (!swipeActive) {
578
- return;
579
- }
580
587
  const popupElement = store.context.popupRef.current;
581
588
  if (!popupElement) {
582
589
  return;
@@ -585,10 +592,10 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
585
592
  return;
586
593
  }
587
594
  if (closedOffsetRef.current == null) {
588
- closedOffsetRef.current = resolveClosedOffset();
595
+ closedOffsetRef.current = resolveClosedOffset(popupElement);
589
596
  }
590
597
  const closedOffset = closedOffsetRef.current;
591
- if (!closedOffset || !Number.isFinite(closedOffset) || closedOffset <= 0) {
598
+ if (closedOffset === null) {
592
599
  return;
593
600
  }
594
601
  const {
@@ -609,6 +616,7 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
609
616
  popupElement.style.setProperty(ScrollAreaViewport.DrawerPopupCssVars.swipeMovementX, `${movementX}px`);
610
617
  popupElement.style.setProperty(ScrollAreaViewport.DrawerPopupCssVars.swipeMovementY, `${movementY}px`);
611
618
  popupElement.setAttribute(ScrollAreaViewport.DrawerPopupDataAttributes.swiping, '');
619
+ swipePopupElementRef.current = popupElement;
612
620
  if (popupTransitionRef.current === null) {
613
621
  popupTransitionRef.current = popupElement.style.transition;
614
622
  }
@@ -616,6 +624,7 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
616
624
  const backdropElement = store.context.backdropRef.current;
617
625
  if (backdropElement) {
618
626
  backdropElement.setAttribute(ScrollAreaViewport.DrawerPopupDataAttributes.swiping, '');
627
+ swipeBackdropElementRef.current = backdropElement;
619
628
  backdropElement.style.setProperty(ScrollAreaViewport.DrawerBackdropCssVars.swipeProgress, `${backdropProgress}`);
620
629
  if (openProgress > 0 && frontmostHeight > 0) {
621
630
  backdropElement.style.setProperty(ScrollAreaViewport.DrawerPopupCssVars.height, `${frontmostHeight}px`);
@@ -628,10 +637,11 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
628
637
  frontmostHeight: openProgress > 0 ? frontmostHeight : 0
629
638
  });
630
639
  appliedSwipeStylesRef.current = true;
640
+ swipeAreaActiveRef.current = true;
631
641
  }
632
642
  const clearSwipeStyles = useScrollLock.useStableCallback(() => {
633
- const popupElement = store.context.popupRef.current;
634
- if (popupElement && appliedSwipeStylesRef.current) {
643
+ const popupElement = swipePopupElementRef.current;
644
+ if (popupElement) {
635
645
  popupElement.style.removeProperty(ScrollAreaViewport.DrawerPopupCssVars.swipeMovementX);
636
646
  popupElement.style.removeProperty(ScrollAreaViewport.DrawerPopupCssVars.swipeMovementY);
637
647
  popupElement.removeAttribute(ScrollAreaViewport.DrawerPopupDataAttributes.swiping);
@@ -640,7 +650,7 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
640
650
  popupElement.style.transition = popupTransitionRef.current;
641
651
  popupTransitionRef.current = null;
642
652
  }
643
- const backdropElement = store.context.backdropRef.current;
653
+ const backdropElement = swipeBackdropElementRef.current;
644
654
  if (backdropElement) {
645
655
  backdropElement.removeAttribute(ScrollAreaViewport.DrawerPopupDataAttributes.swiping);
646
656
  backdropElement.style.setProperty(ScrollAreaViewport.DrawerBackdropCssVars.swipeProgress, '0');
@@ -651,19 +661,16 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
651
661
  frontmostHeight: 0
652
662
  });
653
663
  appliedSwipeStylesRef.current = false;
664
+ swipePopupElementRef.current = null;
665
+ swipeBackdropElementRef.current = null;
666
+ swipeAreaActiveRef.current = false;
654
667
  });
655
668
  function openDrawer(event) {
656
- if (store.select('open')) {
657
- return;
658
- }
659
669
  openedBySwipeRef.current = true;
660
- store.setOpen(true, useScrollLock.createChangeEventDetails(useScrollLock.swipe, event, swipeAreaRef.current ?? undefined));
670
+ store.setOpen(true, useScrollLock.createChangeEventDetails(useScrollLock.swipe, event, swipeAreaRef.current));
661
671
  }
662
672
  function closeDrawer(event) {
663
- if (!store.select('open')) {
664
- return;
665
- }
666
- store.setOpen(false, useScrollLock.createChangeEventDetails(useScrollLock.swipe, event, swipeAreaRef.current ?? undefined));
673
+ store.setOpen(false, useScrollLock.createChangeEventDetails(useScrollLock.swipe, event, swipeAreaRef.current));
667
674
  }
668
675
  function resetSwipeInteractionState() {
669
676
  swipeStartEventRef.current = null;
@@ -706,10 +713,10 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
706
713
  return;
707
714
  }
708
715
  const displacement = ScrollAreaViewport.getDisplacement(resolvedSwipeDirection, details.deltaX, details.deltaY);
709
- if (displacement < MIN_SWIPE_START_DISTANCE && !openedBySwipeRef.current) {
716
+ if (!openedBySwipeRef.current && displacement < MIN_SWIPE_START_DISTANCE) {
710
717
  return;
711
718
  }
712
- if (!openedBySwipeRef.current) {
719
+ if (!openedBySwipeRef.current && !store.select('open')) {
713
720
  openDrawer(swipeStartEventRef.current);
714
721
  }
715
722
  applySwipeMovement();
@@ -725,14 +732,14 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
725
732
  const displacement = ScrollAreaViewport.getDisplacement(resolvedSwipeDirection, deltaX, deltaY);
726
733
  const releaseVelocity = ScrollAreaViewport.getDisplacement(resolvedSwipeDirection, releaseVelocityX, releaseVelocityY);
727
734
  const threshold = resolveSwipeOpenThreshold();
728
- const hasEnoughDistance = threshold != null && displacement >= threshold;
735
+ const hasEnoughDistance = displacement >= threshold;
729
736
  const hasEnoughVelocity = releaseVelocity >= VELOCITY_THRESHOLD;
730
- const shouldOpen = threshold != null && direction === resolvedSwipeDirection && (hasEnoughDistance || hasEnoughVelocity) && !disabled;
737
+ const shouldOpen = direction === resolvedSwipeDirection && (hasEnoughDistance || hasEnoughVelocity) && !disabled;
731
738
  if (shouldOpen) {
732
739
  if (!store.select('open')) {
733
740
  openDrawer(event);
734
741
  }
735
- } else if (openedBySwipeRef.current) {
742
+ } else if (openedBySwipeRef.current && store.select('open')) {
736
743
  closeDrawer(event);
737
744
  }
738
745
  finishSwipeInteraction();
@@ -743,16 +750,29 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
743
750
  const swipePointerProps = swipe.getPointerProps();
744
751
  const swipeTouchProps = swipe.getTouchProps();
745
752
  const resetSwipe = swipe.reset;
746
- React__namespace.useEffect(() => {
753
+
754
+ // The commit that opens the drawer re-renders the popup, resetting `--swipe-movement-*` to `0px`
755
+ // (the viewport isn't swiping). Re-assert after the DOM mutation but before paint. No deps: must
756
+ // run on every commit the swipe area participates in.
757
+ useScrollLock.useIsoLayoutEffect(() => {
758
+ if (swipeActive && appliedSwipeStylesRef.current) {
759
+ applySwipeMovement();
760
+ }
761
+ });
762
+ useScrollLock.useIsoLayoutEffect(() => {
747
763
  if (!enabled) {
764
+ if (swipeActive) {
765
+ enableDismissAfterRelease();
766
+ }
748
767
  resetSwipe();
749
768
  resetDragDelta();
750
769
  clearSwipeStyles();
751
770
  resetSwipeInteractionState();
752
771
  }
753
- }, [clearSwipeStyles, enabled, resetDragDelta, resetSwipe]);
772
+ }, [clearSwipeStyles, enableDismissAfterRelease, enabled, resetDragDelta, resetSwipe, swipeActive]);
754
773
  React__namespace.useEffect(() => {
755
774
  return () => {
775
+ releaseGuardCleanupRef.current();
756
776
  store.context.outsidePressEnabledRef.current = true;
757
777
  };
758
778
  }, [store]);
@@ -765,7 +785,7 @@ const DrawerSwipeArea = /*#__PURE__*/React__namespace.forwardRef(function Drawer
765
785
  return useRenderElement.useRenderElement('div', componentProps, {
766
786
  state,
767
787
  ref: [forwardedRef, swipeAreaRef, registerTrigger],
768
- stateAttributesMapping: stateAttributesMapping$3,
788
+ stateAttributesMapping: stateAttributesMapping$1,
769
789
  props: [{
770
790
  role: 'presentation',
771
791
  'aria-hidden': true,
@@ -825,6 +845,15 @@ const KEYBOARD_VISIBILITY_MARGIN = 16;
825
845
  // keyboard overlap, so the field can be scrolled clear of the keyboard instead of
826
846
  // ending up flush against it. Only applied when there is actual overlap.
827
847
  const KEYBOARD_SCROLL_SLACK = 48;
848
+ // Cadence of the settle-watching realign passes after focus moves with the keyboard open:
849
+ // long enough for a smooth scroll to show progress between passes, short enough to recover
850
+ // quickly from a scroll canceled by WebKit's reveal; the pass count covers CSS transitions
851
+ // reacting to the focus change (e.g. a 260ms footer resize) with room to spare.
852
+ const KEYBOARD_REALIGN_INTERVAL = 150;
853
+ const KEYBOARD_REALIGN_MAX_PASSES = 4;
854
+ // Frames the alignment waits for the scroll destination to stop moving (layout reacting to
855
+ // the focus change, e.g. a footer resizing over a CSS transition) before scrolling anyway.
856
+ const KEYBOARD_SETTLE_FRAME_LIMIT = 60;
828
857
  const INPUT_TAP_MOVE_THRESHOLD = 10;
829
858
  const INPUT_TAP_HIT_SLOP = 16;
830
859
  const KEYBOARD_INPUT_TYPES = new Set(['email', 'number', 'password', 'search', 'tel', 'text', 'url']);
@@ -848,11 +877,10 @@ function DrawerVirtualKeyboardProvider(props) {
848
877
  const {
849
878
  children
850
879
  } = props;
851
- const {
852
- store
853
- } = ScrollAreaViewport.useDialogRootContext();
880
+ const store = ScrollAreaViewport.useDialogRootContext();
854
881
  const open = store.useState('open');
855
882
  const mounted = store.useState('mounted');
883
+ const modal = store.useState('modal');
856
884
  const nestedOpenDialogCount = store.useState('nestedOpenDialogCount');
857
885
  const viewportElement = store.useState('viewportElement');
858
886
 
@@ -865,7 +893,9 @@ function DrawerVirtualKeyboardProvider(props) {
865
893
  const keyboardTouchStartRef = React__namespace.useRef(null);
866
894
  const focusedKeyboardTargetRef = React__namespace.useRef(null);
867
895
  const keyboardScrollAdjustmentRef = React__namespace.useRef(null);
896
+ const programmaticKeyboardFocusRef = React__namespace.useRef(false);
868
897
  const keyboardFocusFrame = useScrollLock.useAnimationFrame();
898
+ const keyboardRealignTimeout = useScrollLock.useTimeout();
869
899
  const restoreKeyboardScrollAdjustment = useScrollLock.useStableCallback(() => {
870
900
  const adjustment = keyboardScrollAdjustmentRef.current;
871
901
  if (!adjustment) {
@@ -892,7 +922,7 @@ function DrawerVirtualKeyboardProvider(props) {
892
922
  adjustment = null;
893
923
  }
894
924
  if (!adjustment) {
895
- const styles = floatingUi_utils.getComputedStyle(element);
925
+ const styles = useScrollLock.getComputedStyle(element);
896
926
  adjustment = {
897
927
  element,
898
928
  overflowAnchor: element.style.overflowAnchor,
@@ -908,7 +938,7 @@ function DrawerVirtualKeyboardProvider(props) {
908
938
  element.style.scrollPaddingBottom = `${adjustment.computedScrollPaddingBottom + KEYBOARD_VISIBILITY_MARGIN}px`;
909
939
  });
910
940
  const animateKeyboardScroll = useScrollLock.useStableCallback((element, scrollTop) => {
911
- const win = floatingUi_utils.getWindow(element);
941
+ const win = useScrollLock.getWindow(element);
912
942
  const behavior = win.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ? 'auto' : 'smooth';
913
943
  element.scrollTo({
914
944
  top: scrollTop,
@@ -931,48 +961,102 @@ function DrawerVirtualKeyboardProvider(props) {
931
961
  return undefined;
932
962
  }
933
963
  const doc = useScrollLock.ownerDocument(rootElement);
934
- const win = floatingUi_utils.getWindow(rootElement);
964
+ const win = useScrollLock.getWindow(rootElement);
935
965
  const visualViewport = win.visualViewport;
966
+
967
+ // Alignment scroll bookkeeping: destination stability, whether a scroll was issued,
968
+ // and the last observed progress so delayed passes can distinguish moving from stalled.
969
+ let keyboardScrollElement = null;
970
+ let keyboardScrollDestination = 0;
971
+ let keyboardScrollChecks = 0;
972
+ let keyboardScrollObserved = -1;
936
973
  const setDrawerKeyboardInset = inset => {
937
974
  rootElement.style.setProperty(DrawerViewportCssVars.keyboardInset, `${Math.max(0, Math.ceil(inset))}px`);
938
975
  };
939
- const resetDrawerKeyboardInset = () => {
940
- setDrawerKeyboardInset(0);
941
- };
942
976
  const clearFocusedKeyboardTarget = () => {
943
977
  focusedKeyboardTargetRef.current = null;
944
- resetDrawerKeyboardInset();
978
+ keyboardScrollElement = null;
979
+ setDrawerKeyboardInset(0);
945
980
  restoreKeyboardScrollAdjustment();
946
981
  keyboardFocusFrame.cancel();
982
+ keyboardRealignTimeout.clear();
983
+ };
984
+
985
+ // WebKit's native reveal scroll can move the page even while the scroll lock hides
986
+ // overflow (e.g. when the software keyboard's previous/next field arrows move focus).
987
+ // While the drawer is modal, any window scroll during keyboard interaction is spurious,
988
+ // so pin the page to the position it had when the drawer opened.
989
+ const baseScrollX = win.scrollX;
990
+ const baseScrollY = win.scrollY;
991
+ const restoreWindowScroll = () => {
992
+ if (modal !== true || nestedDrawerOpen || !focusedKeyboardTargetRef.current || getKeyboardVisualViewport(win) == null) {
993
+ return false;
994
+ }
995
+ if (win.scrollX !== baseScrollX || win.scrollY !== baseScrollY) {
996
+ // Force an instant jump: the two-argument form defaults `behavior` to `auto`, which
997
+ // obeys the page's `scroll-behavior`, so a global `scroll-behavior: smooth` would
998
+ // animate the restore. The measurements that follow assume the page is already back
999
+ // at rest, and a smooth restore also re-emits `scroll`, re-entering this handler.
1000
+ win.scrollTo({
1001
+ left: baseScrollX,
1002
+ top: baseScrollY,
1003
+ behavior: 'instant'
1004
+ });
1005
+ return true;
1006
+ }
1007
+ return false;
1008
+ };
1009
+
1010
+ // Focus moved by the drawer itself goes through `focusKeyboardInputWithoutPageScroll`,
1011
+ // but native focus changes (the iOS keyboard's previous/next field arrows) commit
1012
+ // WebKit's reveal scroll before `focusin` reaches us — cancelling it afterwards
1013
+ // visibly jitters the sheet. `focusout` on the outgoing field fires before focus
1014
+ // lands, so override the incoming field's geometry there and restore it in `focusin`.
1015
+ let restorePreemptedFocus = null;
1016
+ const consumePreemptedFocus = () => {
1017
+ restorePreemptedFocus?.();
1018
+ restorePreemptedFocus = null;
1019
+ };
1020
+ const preemptFocusReveal = (target, keyboardViewport) => {
1021
+ consumePreemptedFocus();
1022
+
1023
+ // Native focus carries no preventScroll, so a fake off-screen rect would make WebKit
1024
+ // scroll the drawer's own scroll containers thousands of pixels chasing it. Presenting
1025
+ // the field as already centered in the visible band above the keyboard instead makes
1026
+ // both the in-page reveal and the viewport pan no-ops.
1027
+ const rect = target.getBoundingClientRect();
1028
+ restorePreemptedFocus = overrideGeometryDuringFocus(target, (keyboardViewport.top + keyboardViewport.bottom - rect.top - rect.bottom) / 2);
947
1029
  };
948
1030
  const alignFocusedKeyboardTarget = () => {
1031
+ // If focus never lands on a preempted target, the focusout-scheduled alignment still
1032
+ // restores it on the next frame before paint.
1033
+ consumePreemptedFocus();
949
1034
  const target = focusedKeyboardTargetRef.current;
950
1035
  // If the focused field is removed from the DOM without firing `focusout` (e.g. it is
951
1036
  // conditionally rendered away), any applied scroll slack is restored here on the next
952
1037
  // focus/viewport event or when the drawer closes. This self-corrects rather than
953
1038
  // tracking each field's lifecycle.
954
1039
  if (nestedDrawerOpen || !target || !useScrollLock.contains(rootElement, target)) {
955
- resetDrawerKeyboardInset();
1040
+ setDrawerKeyboardInset(0);
956
1041
  restoreKeyboardScrollAdjustment();
957
1042
  return;
958
1043
  }
1044
+
1045
+ // Undo any reveal scroll WebKit applied to the locked page before measuring, so the
1046
+ // keyboard inset and alignment are computed against the resting viewport.
1047
+ restoreWindowScroll();
959
1048
  const keyboardViewport = getKeyboardVisualViewport(win);
960
1049
  if (!keyboardViewport) {
961
- resetDrawerKeyboardInset();
1050
+ setDrawerKeyboardInset(0);
962
1051
  restoreKeyboardScrollAdjustment();
963
1052
  return;
964
1053
  }
965
- setDrawerKeyboardInset(getDrawerKeyboardInset(win, keyboardViewport));
1054
+ setDrawerKeyboardInset(Math.max(0, win.innerHeight - keyboardViewport.bottom));
966
1055
  const scrollTarget = findKeyboardScrollTarget(target, rootElement);
967
1056
  if (!scrollTarget) {
968
1057
  restoreKeyboardScrollAdjustment();
969
1058
  return;
970
1059
  }
971
- if (!scrollTarget.isConnected || !useScrollLock.contains(rootElement, scrollTarget)) {
972
- resetDrawerKeyboardInset();
973
- restoreKeyboardScrollAdjustment();
974
- return;
975
- }
976
1060
  const scrollTargetRect = scrollTarget.getBoundingClientRect();
977
1061
  const clippedBottom = Math.min(scrollTargetRect.bottom, keyboardViewport.bottom);
978
1062
  const overlap = Math.max(0, scrollTargetRect.bottom - keyboardViewport.bottom);
@@ -988,14 +1072,70 @@ function DrawerVirtualKeyboardProvider(props) {
988
1072
  return;
989
1073
  }
990
1074
  const targetRect = target.getBoundingClientRect();
991
- const targetCenter = (targetRect.top + targetRect.bottom) / 2;
992
- const visibleCenter = (visibleTop + visibleBottom) / 2;
993
- const nextScrollTop = scrollTarget.scrollTop + targetCenter - visibleCenter;
994
- animateKeyboardScroll(scrollTarget, scrollEdges.clamp(nextScrollTop, 0, maxScrollTop));
1075
+ const nextScrollTop = scrollTarget.scrollTop + (targetRect.top + targetRect.bottom - visibleTop - visibleBottom) / 2;
1076
+ const destination = Math.round(scrollEdges.clamp(nextScrollTop, 0, maxScrollTop));
1077
+ const settled = keyboardScrollElement === scrollTarget && Math.abs(keyboardScrollDestination - destination) <= 1;
1078
+ if (!settled) {
1079
+ // Commit the scroll only once the destination holds across two consecutive checks.
1080
+ // Layout may still be reacting to the focus change (e.g. a footer sized by
1081
+ // `:focus-within` resizing over a transition), and scrolling toward a
1082
+ // mid-transition destination overshoots, then visibly pulls back once corrected.
1083
+ // The destination is scroll-position-invariant, so an in-flight scroll of the
1084
+ // container itself never defers the commit.
1085
+ const checks = keyboardScrollElement === scrollTarget ? keyboardScrollChecks + 1 : 1;
1086
+ keyboardScrollElement = scrollTarget;
1087
+ keyboardScrollDestination = destination;
1088
+ keyboardScrollChecks = checks;
1089
+ keyboardScrollObserved = -1;
1090
+ // Re-check next frame; give up and scroll anyway if layout never settles.
1091
+ if (checks <= KEYBOARD_SETTLE_FRAME_LIMIT) {
1092
+ keyboardFocusFrame.request(alignFocusedKeyboardTarget);
1093
+ return;
1094
+ }
1095
+ } else if (keyboardScrollObserved >= 0) {
1096
+ // A scroll toward this destination is already out. Leave it alone while it has
1097
+ // arrived or is still progressing — re-issuing restarts the smooth-scroll easing
1098
+ // curve, a visible stutter on every realign pass — and re-issue only when it
1099
+ // stalled short (WebKit cancels in-flight smooth scrolls when its native reveal
1100
+ // for the focus resolves).
1101
+ const current = scrollTarget.scrollTop;
1102
+ if (Math.abs(current - destination) <= 1) {
1103
+ return;
1104
+ }
1105
+ if (current !== keyboardScrollObserved) {
1106
+ keyboardScrollObserved = current;
1107
+ return;
1108
+ }
1109
+ }
1110
+ keyboardScrollElement = scrollTarget;
1111
+ keyboardScrollDestination = destination;
1112
+ keyboardScrollChecks = 0;
1113
+ keyboardScrollObserved = scrollTarget.scrollTop;
1114
+ animateKeyboardScroll(scrollTarget, destination);
995
1115
  };
996
1116
  const scheduleKeyboardFocusAlignment = () => {
997
1117
  keyboardFocusFrame.request(alignFocusedKeyboardTarget);
998
1118
  };
1119
+
1120
+ // When focus moves with the keyboard already up, no viewport resize events follow to
1121
+ // re-run alignment, and the single frame-scheduled pass is unreliable: WebKit resolves
1122
+ // the reveal for the focus asynchronously (a UI-process round trip) and cancels any
1123
+ // in-flight scroll the pass started when it lands, and layout reacting to the focus
1124
+ // change (e.g. a footer sized by `:focus-within` and the keyboard inset) settles only
1125
+ // after its transition, so the pass measures stale geometry. Re-align on an interval
1126
+ // until both settle; the alignment's scroll bookkeeping observes before re-issuing,
1127
+ // so passes that find the scroll on course are inert.
1128
+ const scheduleDelayedKeyboardRealign = () => {
1129
+ let remainingPasses = KEYBOARD_REALIGN_MAX_PASSES;
1130
+ const realign = () => {
1131
+ alignFocusedKeyboardTarget();
1132
+ remainingPasses -= 1;
1133
+ if (remainingPasses > 0) {
1134
+ keyboardRealignTimeout.start(KEYBOARD_REALIGN_INTERVAL, realign);
1135
+ }
1136
+ };
1137
+ keyboardRealignTimeout.start(KEYBOARD_REALIGN_INTERVAL, realign);
1138
+ };
999
1139
  const captureFocusedKeyboardTarget = eventTarget => {
1000
1140
  if (nestedDrawerOpen) {
1001
1141
  return false;
@@ -1007,16 +1147,56 @@ function DrawerVirtualKeyboardProvider(props) {
1007
1147
  if (!target || !useScrollLock.contains(rootElement, target)) {
1008
1148
  return false;
1009
1149
  }
1150
+
1151
+ // A new field starts a fresh alignment; the scroll bookkeeping must not carry
1152
+ // over from the previous field's scroll.
1153
+ if (focusedKeyboardTargetRef.current !== target) {
1154
+ keyboardScrollElement = null;
1155
+ }
1010
1156
  focusedKeyboardTargetRef.current = target;
1011
1157
  return true;
1012
1158
  };
1013
1159
  const handleFocusIn = event => {
1014
- if (captureFocusedKeyboardTarget(useScrollLock.getTarget(event))) {
1015
- scheduleKeyboardFocusAlignment();
1160
+ // The programmatic transition is over once focus lands, which happens before
1161
+ // `.focus()` returns. Any later `focusout` is the consumer's own — an `onFocus`
1162
+ // handler that blurs or moves focus — and must be reconciled rather than
1163
+ // suppressed, so stop suppressing here instead of waiting for `.focus()` to return.
1164
+ programmaticKeyboardFocusRef.current = false;
1165
+ consumePreemptedFocus();
1166
+ if (!captureFocusedKeyboardTarget(useScrollLock.getTarget(event))) {
1167
+ // Focus landed outside any drawer keyboard input. The `focusout` on the previous
1168
+ // field normally clears the tracked target, but the tap path suppresses that
1169
+ // `focusout` via `programmaticKeyboardFocusRef`, so a consumer `onFocus` handler that
1170
+ // redirects focus out of the drawer would otherwise leave a stale target holding its
1171
+ // inset, slack, and pending realign. Reconcile against the real focus here.
1172
+ // A handler that ends focus entirely emits no `focusin` to reconcile from; that case
1173
+ // is covered by clearing the suppression flag above so its `focusout` is handled.
1174
+ clearFocusedKeyboardTarget();
1175
+ return;
1176
+ }
1177
+
1178
+ // Covers every way focus can land with the keyboard already up: native moves (the
1179
+ // keyboard's previous/next arrows) and the tap path (which suppresses `focusout`
1180
+ // handling via the programmatic flag).
1181
+ if (getKeyboardVisualViewport(win) != null) {
1182
+ scheduleDelayedKeyboardRealign();
1016
1183
  }
1184
+ scheduleKeyboardFocusAlignment();
1017
1185
  };
1018
1186
  const handleFocusOut = event => {
1187
+ // The blur inside `focusKeyboardInputWithoutPageScroll` is followed synchronously by
1188
+ // a re-focus; clearing state here would drop the keyboard inset for a frame.
1189
+ if (programmaticKeyboardFocusRef.current) {
1190
+ return;
1191
+ }
1019
1192
  if (captureFocusedKeyboardTarget(event.relatedTarget)) {
1193
+ const target = focusedKeyboardTargetRef.current;
1194
+ const keyboardViewport = getKeyboardVisualViewport(win);
1195
+ // The delayed realign passes are scheduled by the `focusin` that follows once
1196
+ // focus lands on the captured target.
1197
+ if (target && keyboardViewport) {
1198
+ preemptFocusReveal(target, keyboardViewport);
1199
+ }
1020
1200
  scheduleKeyboardFocusAlignment();
1021
1201
  return;
1022
1202
  }
@@ -1031,25 +1211,36 @@ function DrawerVirtualKeyboardProvider(props) {
1031
1211
  if (visualViewport) {
1032
1212
  cleanupListeners.push(useScrollLock.addEventListener(visualViewport, 'resize', handleViewportUpdate), useScrollLock.addEventListener(visualViewport, 'scroll', handleViewportUpdate));
1033
1213
  }
1034
- cleanupListeners.push(useScrollLock.addEventListener(doc, 'focusin', handleFocusIn, true), useScrollLock.addEventListener(doc, 'focusout', handleFocusOut, true));
1214
+ const handleWindowScroll = () => {
1215
+ if (restoreWindowScroll()) {
1216
+ // Recompute the keyboard inset once the page is back at rest; measurements taken
1217
+ // mid-reveal (a non-zero visual viewport offset) inflate it and push the popup up.
1218
+ scheduleKeyboardFocusAlignment();
1219
+ }
1220
+ };
1221
+
1222
+ // Once the user puts a finger down they own the scroll position. Stop both the frame settle
1223
+ // loop, which can center after changing layout settles, and the delayed passes, which can't
1224
+ // distinguish a user scroll from a reveal scroll WebKit canceled. A later focus or viewport
1225
+ // change reschedules alignment if it's still needed.
1226
+ const cancelKeyboardRealignOnPointerDown = () => {
1227
+ keyboardFocusFrame.cancel();
1228
+ keyboardRealignTimeout.clear();
1229
+ keyboardScrollElement = null;
1230
+ };
1231
+ cleanupListeners.push(useScrollLock.addEventListener(doc, 'focusin', handleFocusIn, true), useScrollLock.addEventListener(doc, 'focusout', handleFocusOut, true), useScrollLock.addEventListener(win, 'scroll', handleWindowScroll), useScrollLock.addEventListener(doc, 'pointerdown', cancelKeyboardRealignOnPointerDown, true));
1035
1232
  if (captureFocusedKeyboardTarget(useScrollLock.activeElement(doc))) {
1036
1233
  scheduleKeyboardFocusAlignment();
1037
1234
  }
1038
1235
  return () => {
1039
1236
  cleanupListeners.forEach(cleanup => cleanup());
1237
+ consumePreemptedFocus();
1040
1238
  clearFocusedKeyboardTarget();
1041
1239
  rootElement.style.removeProperty(DrawerViewportCssVars.keyboardInset);
1042
1240
  };
1043
- }, [animateKeyboardScroll, keyboardFocusFrame, mounted, nestedDrawerOpen, open, restoreKeyboardScrollAdjustment, rootElement, setKeyboardScrollSlack]);
1241
+ }, [animateKeyboardScroll, keyboardFocusFrame, keyboardRealignTimeout, modal, mounted, nestedDrawerOpen, open, restoreKeyboardScrollAdjustment, rootElement, setKeyboardScrollSlack]);
1044
1242
  const onTouchStart = useScrollLock.useStableCallback(event => {
1045
- if (!open || !mounted || nestedDrawerOpen) {
1046
- resetTouchTrackingState();
1047
- return;
1048
- }
1049
1243
  const touch = event.touches[0];
1050
- if (!touch) {
1051
- return;
1052
- }
1053
1244
  pendingKeyboardFocusMovedRef.current = false;
1054
1245
  keyboardTouchStartRef.current = {
1055
1246
  x: touch.clientX,
@@ -1075,9 +1266,9 @@ function DrawerVirtualKeyboardProvider(props) {
1075
1266
  return;
1076
1267
  }
1077
1268
  const touch = event.changedTouches[0] ?? event.touches[0];
1078
- const doc = useScrollLock.ownerDocument(event.currentTarget);
1269
+ const root = rootElement.getRootNode();
1079
1270
  const nativeEventTarget = useScrollLock.getTarget(event.nativeEvent);
1080
- const pointTarget = touch ? resolveKeyboardTouchTargetFromPoint(doc, touch.clientX, touch.clientY) : null;
1271
+ const pointTarget = touch ? resolveKeyboardTouchTargetFromPoint(root, touch.clientX, touch.clientY) : null;
1081
1272
 
1082
1273
  // The lift point landed on another interactive/label element; let its native tap
1083
1274
  // through instead of stealing it for the touchstart input.
@@ -1095,7 +1286,7 @@ function DrawerVirtualKeyboardProvider(props) {
1095
1286
  clickTarget: keyboardClickTarget,
1096
1287
  focusTarget: keyboardFocusTarget
1097
1288
  } = keyboardTarget;
1098
- const win = floatingUi_utils.getWindow(keyboardFocusTarget);
1289
+ const win = useScrollLock.getWindow(keyboardFocusTarget);
1099
1290
 
1100
1291
  // While pinch-zoomed, keyboard alignment is suspended; let native behavior
1101
1292
  // handle focus and caret placement instead of blurring and re-focusing.
@@ -1106,15 +1297,24 @@ function DrawerVirtualKeyboardProvider(props) {
1106
1297
 
1107
1298
  // Already focused with the keyboard up: let the native tap through so it can
1108
1299
  // reposition the caret, rather than blurring and re-focusing the same input.
1109
- if (useScrollLock.activeElement(useScrollLock.ownerDocument(keyboardFocusTarget)) === keyboardFocusTarget && isKeyboardVisualViewportOpen(win)) {
1300
+ if (useScrollLock.activeElement(useScrollLock.ownerDocument(keyboardFocusTarget)) === keyboardFocusTarget && (!win.visualViewport || getKeyboardVisualViewport(win) != null)) {
1110
1301
  resetTouchTrackingState();
1111
1302
  return;
1112
1303
  }
1113
1304
 
1114
1305
  // iOS only opens the software keyboard when focus happens synchronously
1115
- // inside the touch gesture.
1306
+ // inside the touch gesture. The flag suppresses the `focusout` cleanup the
1307
+ // intermediate blur would otherwise trigger, so the keyboard inset isn't
1308
+ // dropped for a frame between the blur and the re-focus. It is cleared by the
1309
+ // `focusin` that lands, so only that blur is suppressed; the `finally` is the
1310
+ // backstop for a target that never takes focus.
1116
1311
  event.preventDefault();
1117
- focusKeyboardInputWithoutPageScroll(keyboardFocusTarget);
1312
+ programmaticKeyboardFocusRef.current = true;
1313
+ try {
1314
+ focusKeyboardInputWithoutPageScroll(keyboardFocusTarget);
1315
+ } finally {
1316
+ programmaticKeyboardFocusRef.current = false;
1317
+ }
1118
1318
  // Preventing the touchend default also suppresses the compatibility mouse
1119
1319
  // events, including `click`; redispatch an untrusted replacement on the
1120
1320
  // original tap target so click handlers still run with the tap coordinates.
@@ -1139,7 +1339,7 @@ function isKeyboardInputElement(element) {
1139
1339
  if (element.isContentEditable) {
1140
1340
  return true;
1141
1341
  }
1142
- const win = floatingUi_utils.getWindow(element);
1342
+ const win = useScrollLock.getWindow(element);
1143
1343
  if (element instanceof win.HTMLTextAreaElement || element instanceof win.HTMLInputElement && KEYBOARD_INPUT_TYPES.has(element.type)) {
1144
1344
  // Disabled controls can't focus or open the keyboard, so tap-to-focus must skip them —
1145
1345
  // otherwise the dispatched click fires handlers a native tap on a disabled control never would.
@@ -1148,7 +1348,7 @@ function isKeyboardInputElement(element) {
1148
1348
  return false;
1149
1349
  }
1150
1350
  function resolveKeyboardInputTarget(target) {
1151
- if (!floatingUi_utils.isHTMLElement(target)) {
1351
+ if (!useScrollLock.isHTMLElement(target)) {
1152
1352
  return null;
1153
1353
  }
1154
1354
  if (isKeyboardInputElement(target)) {
@@ -1156,16 +1356,19 @@ function resolveKeyboardInputTarget(target) {
1156
1356
  }
1157
1357
  const label = target.closest('label');
1158
1358
  const control = label?.control ?? null;
1159
- return floatingUi_utils.isHTMLElement(control) && isKeyboardInputElement(control) ? control : null;
1359
+ return useScrollLock.isHTMLElement(control) && isKeyboardInputElement(control) ? control : null;
1160
1360
  }
1161
1361
  function resolveKeyboardTouchTarget(target) {
1362
+ if (!useScrollLock.isHTMLElement(target)) {
1363
+ return null;
1364
+ }
1162
1365
  const focusTarget = resolveKeyboardInputTarget(target);
1163
1366
  if (!focusTarget) {
1164
1367
  return null;
1165
1368
  }
1166
1369
  return {
1167
1370
  focusTarget,
1168
- clickTarget: floatingUi_utils.isHTMLElement(target) ? target : focusTarget
1371
+ clickTarget: target
1169
1372
  };
1170
1373
  }
1171
1374
 
@@ -1178,14 +1381,16 @@ function getContentEditableHost(element) {
1178
1381
  }
1179
1382
  return host;
1180
1383
  }
1181
- function resolveKeyboardTouchTargetFromPoint(doc, clientX, clientY) {
1182
- const exactTarget = ScrollAreaViewport.getElementAtPoint(doc, clientX, clientY);
1183
- const exactKeyboardTarget = resolveKeyboardInputTarget(exactTarget);
1184
- if (exactKeyboardTarget) {
1185
- return {
1186
- focusTarget: exactKeyboardTarget,
1187
- clickTarget: floatingUi_utils.isHTMLElement(exactTarget) ? exactTarget : exactKeyboardTarget
1188
- };
1384
+ function resolveKeyboardTouchTargetFromPoint(root, clientX, clientY) {
1385
+ const exactTarget = ScrollAreaViewport.getElementAtPoint(root, clientX, clientY);
1386
+ if (useScrollLock.isHTMLElement(exactTarget)) {
1387
+ const exactKeyboardTarget = resolveKeyboardInputTarget(exactTarget);
1388
+ if (exactKeyboardTarget) {
1389
+ return {
1390
+ focusTarget: exactKeyboardTarget,
1391
+ clickTarget: exactTarget
1392
+ };
1393
+ }
1189
1394
  }
1190
1395
 
1191
1396
  // Probing nearby points compensates for iOS retargeting taps while the page reacts
@@ -1198,7 +1403,7 @@ function resolveKeyboardTouchTargetFromPoint(doc, clientX, clientY) {
1198
1403
  return KEYBOARD_TAP_BLOCKED;
1199
1404
  }
1200
1405
  for (const [offsetX, offsetY] of [[0, INPUT_TAP_HIT_SLOP], [0, -INPUT_TAP_HIT_SLOP], [INPUT_TAP_HIT_SLOP, 0], [-INPUT_TAP_HIT_SLOP, 0]]) {
1201
- const keyboardTarget = resolveKeyboardInputTarget(ScrollAreaViewport.getElementAtPoint(doc, clientX + offsetX, clientY + offsetY));
1406
+ const keyboardTarget = resolveKeyboardInputTarget(ScrollAreaViewport.getElementAtPoint(root, clientX + offsetX, clientY + offsetY));
1202
1407
  if (keyboardTarget) {
1203
1408
  return {
1204
1409
  focusTarget: keyboardTarget,
@@ -1209,7 +1414,7 @@ function resolveKeyboardTouchTargetFromPoint(doc, clientX, clientY) {
1209
1414
  return null;
1210
1415
  }
1211
1416
  function dispatchKeyboardClick(target, touch) {
1212
- const win = floatingUi_utils.getWindow(target);
1417
+ const win = useScrollLock.getWindow(target);
1213
1418
  const ClickEvent = win.PointerEvent ?? win.MouseEvent;
1214
1419
  target.dispatchEvent(new ClickEvent('click', {
1215
1420
  bubbles: true,
@@ -1222,15 +1427,10 @@ function dispatchKeyboardClick(target, touch) {
1222
1427
  }
1223
1428
  function focusKeyboardInputWithoutPageScroll(target) {
1224
1429
  const wasFocused = useScrollLock.activeElement(useScrollLock.ownerDocument(target)) === target;
1225
- const previousOpacity = target.style.opacity;
1226
- const previousTransform = target.style.transform;
1227
- const previousTransition = target.style.transition;
1228
-
1229
1430
  // iOS Safari can still scroll the page for transformed sheets even with preventScroll.
1230
- // Move the input off-screen only for the synchronous focus call.
1231
- target.style.transition = 'none';
1232
- target.style.opacity = '0';
1233
- target.style.transform = 'translateY(-2000px)';
1431
+ // Move the input off-screen only for the synchronous focus call; `preventScroll`
1432
+ // suppresses the in-page ancestor scrolling that would otherwise chase the fake rect.
1433
+ const restoreStyles = overrideGeometryDuringFocus(target, -2000);
1234
1434
  try {
1235
1435
  if (wasFocused) {
1236
1436
  target.blur();
@@ -1239,10 +1439,25 @@ function focusKeyboardInputWithoutPageScroll(target) {
1239
1439
  preventScroll: true
1240
1440
  });
1241
1441
  } finally {
1442
+ restoreStyles();
1443
+ }
1444
+ }
1445
+
1446
+ // Overrides the painted geometry WebKit samples when an element gains focus. The rect is
1447
+ // hidden (opacity) rather than detached so layout is unaffected; the caller must restore
1448
+ // synchronously before the next paint.
1449
+ function overrideGeometryDuringFocus(target, translateY) {
1450
+ const previousOpacity = target.style.opacity;
1451
+ const previousTransform = target.style.transform;
1452
+ const previousTransition = target.style.transition;
1453
+ target.style.transition = 'none';
1454
+ target.style.opacity = '0';
1455
+ target.style.transform = `translateY(${translateY}px)`;
1456
+ return () => {
1242
1457
  target.style.opacity = previousOpacity;
1243
1458
  target.style.transform = previousTransform;
1244
1459
  target.style.transition = previousTransition;
1245
- }
1460
+ };
1246
1461
  }
1247
1462
  function findKeyboardScrollTarget(target, root) {
1248
1463
  // Start at the parent: scrolling the focused field's own content (an overflowing
@@ -1251,8 +1466,8 @@ function findKeyboardScrollTarget(target, root) {
1251
1466
  // the drawer body scroller. Prefer an already-scrollable ancestor, then fall back to one
1252
1467
  // that only becomes scrollable once keyboard slack is added (overflow intent without
1253
1468
  // current overflow).
1254
- const scrollStart = floatingUi_utils.getParentNode(target);
1255
- return ScrollAreaViewport.findScrollableTouchTarget(scrollStart, root, 'vertical') ?? ScrollAreaViewport.findScrollableTouchTarget(scrollStart, root, 'vertical', true);
1469
+ const scrollStart = useScrollLock.getParentNode(target);
1470
+ return scrollable.findScrollableTouchTarget(scrollStart, root, 'vertical') ?? scrollable.findScrollableTouchTarget(scrollStart, root, 'vertical', true);
1256
1471
  }
1257
1472
  function getKeyboardVisualViewport(win) {
1258
1473
  const visualViewport = win.visualViewport;
@@ -1270,11 +1485,22 @@ function getKeyboardVisualViewport(win) {
1270
1485
  bottom: Math.min(win.innerHeight, top + visualViewport.height)
1271
1486
  };
1272
1487
  }
1273
- function getDrawerKeyboardInset(win, keyboardViewport) {
1274
- return Math.max(0, win.innerHeight - keyboardViewport.bottom);
1275
- }
1276
- function isKeyboardVisualViewportOpen(win) {
1277
- return !win.visualViewport || getKeyboardVisualViewport(win) != null;
1488
+
1489
+ /**
1490
+ * Controls a Drawer imperatively and associates detached `Drawer.Trigger` components with a
1491
+ * `Drawer.Root`. Create one with `Drawer.createHandle()` and pass it to the `handle` prop of the
1492
+ * root and of any triggers rendered outside of it.
1493
+ *
1494
+ * The imperative methods take effect only while a root using this handle is mounted; calls made
1495
+ * before a root attaches (or after it unmounts) are ignored.
1496
+ */
1497
+ class DrawerHandle extends DrawerDescription.DialogHandle {}
1498
+
1499
+ /**
1500
+ * Creates a new handle to connect a Drawer.Root with detached Drawer.Trigger components.
1501
+ */
1502
+ function createDrawerHandle() {
1503
+ return new DrawerHandle();
1278
1504
  }
1279
1505
 
1280
1506
  var index_parts$5 = /*#__PURE__*/Object.freeze({
@@ -1283,7 +1509,7 @@ var index_parts$5 = /*#__PURE__*/Object.freeze({
1283
1509
  Close: DrawerTitle.DrawerClose,
1284
1510
  Content: ScrollAreaViewport.DrawerContent,
1285
1511
  Description: DrawerDescription.DrawerDescription,
1286
- Handle: DrawerDescription.DialogHandle,
1512
+ Handle: DrawerHandle,
1287
1513
  Indent: DrawerIndent,
1288
1514
  IndentBackground: DrawerIndentBackground,
1289
1515
  Popup: ScrollAreaViewport.DrawerPopup,
@@ -1295,7 +1521,7 @@ var index_parts$5 = /*#__PURE__*/Object.freeze({
1295
1521
  Trigger: ScrollAreaViewport.DrawerTrigger,
1296
1522
  Viewport: ScrollAreaViewport.DrawerViewport,
1297
1523
  VirtualKeyboardProvider: DrawerVirtualKeyboardProvider,
1298
- createHandle: DrawerDescription.createDialogHandle
1524
+ createHandle: createDrawerHandle
1299
1525
  });
1300
1526
 
1301
1527
  /**
@@ -1364,7 +1590,7 @@ const MenuCheckboxItem = /*#__PURE__*/React__namespace.forwardRef(function MenuC
1364
1590
  id: idProp,
1365
1591
  label,
1366
1592
  nativeButton = false,
1367
- disabled = false,
1593
+ disabled: disabledProp = false,
1368
1594
  closeOnClick = false,
1369
1595
  checked: checkedProp,
1370
1596
  defaultChecked,
@@ -1373,6 +1599,7 @@ const MenuCheckboxItem = /*#__PURE__*/React__namespace.forwardRef(function MenuC
1373
1599
  ...elementProps
1374
1600
  } = componentProps;
1375
1601
  const listItem = useCompositeListItem.useCompositeListItem({
1602
+ guess: true,
1376
1603
  label
1377
1604
  });
1378
1605
  const menuPositionerContext = MenuSubmenuTrigger.useMenuPositionerContext(true);
@@ -1380,6 +1607,8 @@ const MenuCheckboxItem = /*#__PURE__*/React__namespace.forwardRef(function MenuC
1380
1607
  const {
1381
1608
  store
1382
1609
  } = MenuSubmenuTrigger.useMenuRootContext();
1610
+ const rootDisabled = store.useState('disabled');
1611
+ const disabled = disabledProp || rootDisabled;
1383
1612
  const highlighted = store.useState('isActive', listItem.index);
1384
1613
  const itemProps = store.useState('itemProps');
1385
1614
  const [checked, setChecked] = useScrollLock.useControlled({
@@ -1408,7 +1637,7 @@ const MenuCheckboxItem = /*#__PURE__*/React__namespace.forwardRef(function MenuC
1408
1637
  }), [disabled, highlighted, checked]);
1409
1638
  function handleClick(event) {
1410
1639
  const details = useScrollLock.createChangeEventDetails(useScrollLock.itemPress, event.nativeEvent, undefined, {
1411
- preventUnmountOnClose() {}
1640
+ preventUnmountOnClose: useRenderElement.NOOP
1412
1641
  });
1413
1642
  onCheckedChange?.(!checked, details);
1414
1643
  if (details.isCanceled) {
@@ -1451,6 +1680,7 @@ const MenuCheckboxItemIndicator = /*#__PURE__*/React__namespace.forwardRef(funct
1451
1680
  const indicatorRef = React__namespace.useRef(null);
1452
1681
  const {
1453
1682
  transitionStatus,
1683
+ mounted,
1454
1684
  setMounted
1455
1685
  } = useScrollLock.useTransitionStatus(item.checked);
1456
1686
  useScrollLock.useOpenChangeComplete({
@@ -1476,22 +1706,22 @@ const MenuCheckboxItemIndicator = /*#__PURE__*/React__namespace.forwardRef(funct
1476
1706
  'aria-hidden': true,
1477
1707
  ...elementProps
1478
1708
  },
1479
- enabled: keepMounted || item.checked
1709
+ enabled: keepMounted || mounted
1480
1710
  });
1481
1711
  return element;
1482
1712
  });
1483
1713
  if (process.env.NODE_ENV !== "production") MenuCheckboxItemIndicator.displayName = "MenuCheckboxItemIndicator";
1484
1714
 
1485
1715
  function getCssDimensions(element) {
1486
- const css = floatingUi_utils.getComputedStyle(element);
1716
+ const css = useScrollLock.getComputedStyle(element);
1487
1717
  // In testing environments, the `width` and `height` properties are empty
1488
1718
  // strings for SVG elements, returning NaN. Fallback to `0` in this case.
1489
1719
  let width = parseFloat(css.width) || 0;
1490
1720
  let height = parseFloat(css.height) || 0;
1491
- const hasOffset = floatingUi_utils.isHTMLElement(element);
1721
+ const hasOffset = useScrollLock.isHTMLElement(element);
1492
1722
  const offsetWidth = hasOffset ? element.offsetWidth : width;
1493
1723
  const offsetHeight = hasOffset ? element.offsetHeight : height;
1494
- const shouldFallback = floatingUi_utils.round(width) !== offsetWidth || floatingUi_utils.round(height) !== offsetHeight;
1724
+ const shouldFallback = useScrollLock.round(width) !== offsetWidth || useScrollLock.round(height) !== offsetHeight;
1495
1725
  if (shouldFallback) {
1496
1726
  width = offsetWidth;
1497
1727
  height = offsetHeight;
@@ -1516,30 +1746,14 @@ function usePopupAutoResize(parameters) {
1516
1746
  side,
1517
1747
  direction
1518
1748
  } = parameters;
1519
- const runOnceAnimationsFinish = useScrollLock.useAnimationsFinished(popupElement, true, false);
1749
+ const runOnceAnimationsFinish = useScrollLock.useAnimationsFinished(popupElement, true);
1520
1750
  const animationFrame = useScrollLock.useAnimationFrame();
1521
1751
  const committedDimensionsRef = React__namespace.useRef(null);
1522
1752
  const isInitialRenderRef = React__namespace.useRef(true);
1523
1753
  const restoreAnchoringStylesRef = React__namespace.useRef(useRenderElement.NOOP);
1524
1754
  const onMeasureLayout = useScrollLock.useStableCallback(onMeasureLayoutParam);
1525
1755
  const onMeasureLayoutComplete = useScrollLock.useStableCallback(onMeasureLayoutCompleteParam);
1526
- const anchoringStyles = React__namespace.useMemo(() => {
1527
- // Ensure popup size transitions correctly when anchored to `bottom` (side=top) or `right` (side=left).
1528
- let isOriginSide = side === 'top';
1529
- let isPhysicalLeft = side === 'left';
1530
- if (direction === 'rtl') {
1531
- isOriginSide = isOriginSide || side === 'inline-end';
1532
- isPhysicalLeft = isPhysicalLeft || side === 'inline-end';
1533
- } else {
1534
- isOriginSide = isOriginSide || side === 'inline-start';
1535
- isPhysicalLeft = isPhysicalLeft || side === 'inline-start';
1536
- }
1537
- return isOriginSide ? {
1538
- position: 'absolute',
1539
- [side === 'top' ? 'bottom' : 'top']: '0',
1540
- [isPhysicalLeft ? 'right' : 'left']: '0'
1541
- } : useRenderElement.EMPTY_OBJECT;
1542
- }, [side, direction]);
1756
+ const anchoringStyles = React__namespace.useMemo(() => getPopupAnchoringStyles(side, direction), [side, direction]);
1543
1757
  useScrollLock.useIsoLayoutEffect(() => {
1544
1758
  // Reset the state when the popup is closed.
1545
1759
  if (!mounted) {
@@ -1615,6 +1829,19 @@ function usePopupAutoResize(parameters) {
1615
1829
  };
1616
1830
  }, [content, popupElement, positionerElement, runOnceAnimationsFinish, animationFrame, mounted, onMeasureLayout, onMeasureLayoutComplete, anchoringStyles]);
1617
1831
  }
1832
+ function getPopupAnchoringStyles(side, direction) {
1833
+ // Ensure popup size transitions correctly when anchored to `bottom` (side=top) or `right` (side=left).
1834
+ const isPhysicalTop = side === 'top';
1835
+ const isPhysicalLeft = side === 'left' || side === (direction === 'rtl' ? 'inline-end' : 'inline-start');
1836
+ if (!isPhysicalTop && !isPhysicalLeft) {
1837
+ return useRenderElement.EMPTY_OBJECT;
1838
+ }
1839
+ return {
1840
+ position: 'absolute',
1841
+ [isPhysicalTop ? 'bottom' : 'top']: '0',
1842
+ [isPhysicalLeft ? 'right' : 'left']: '0'
1843
+ };
1844
+ }
1618
1845
  function overrideElementStyle(element, property, value) {
1619
1846
  const originalValue = element.style.getPropertyValue(property);
1620
1847
  element.style.setProperty(property, value);
@@ -1644,6 +1871,80 @@ function setPositionerCssSize(positionerElement, size) {
1644
1871
  positionerElement.style.setProperty('--positioner-height', height);
1645
1872
  }
1646
1873
 
1874
+ const adaptiveOrigin = {
1875
+ name: 'adaptiveOrigin',
1876
+ async fn(state) {
1877
+ const {
1878
+ x: rawX,
1879
+ y: rawY,
1880
+ rects: {
1881
+ floating: floatRect
1882
+ },
1883
+ elements: {
1884
+ floating
1885
+ },
1886
+ platform,
1887
+ strategy,
1888
+ placement
1889
+ } = state;
1890
+ const win = useScrollLock.getWindow(floating);
1891
+ const styles = win.getComputedStyle(floating);
1892
+ const hasTransition = styles.transitionDuration !== '0s' && styles.transitionDuration !== '';
1893
+ if (!hasTransition) {
1894
+ return {
1895
+ x: rawX,
1896
+ y: rawY,
1897
+ data: useCompositeListItem.DEFAULT_SIDES
1898
+ };
1899
+ }
1900
+ const offsetParent = await platform.getOffsetParent?.(floating);
1901
+ let offsetDimensions = {
1902
+ width: 0,
1903
+ height: 0
1904
+ };
1905
+
1906
+ // For fixed strategy, prefer visualViewport if available
1907
+ if (strategy === 'fixed' && win?.visualViewport) {
1908
+ offsetDimensions = {
1909
+ width: win.visualViewport.width,
1910
+ height: win.visualViewport.height
1911
+ };
1912
+ } else if (offsetParent === win) {
1913
+ const doc = useScrollLock.ownerDocument(floating);
1914
+ offsetDimensions = {
1915
+ width: doc.documentElement.clientWidth,
1916
+ height: doc.documentElement.clientHeight
1917
+ };
1918
+ } else if (await platform.isElement?.(offsetParent)) {
1919
+ offsetDimensions = await platform.getDimensions(offsetParent);
1920
+ }
1921
+ const currentSide = useScrollLock.getSide(placement);
1922
+ let x = rawX;
1923
+ let y = rawY;
1924
+ if (currentSide === 'left') {
1925
+ x = offsetDimensions.width - (rawX + floatRect.width);
1926
+ }
1927
+ if (currentSide === 'top') {
1928
+ y = offsetDimensions.height - (rawY + floatRect.height);
1929
+ }
1930
+ const sideX = currentSide === 'left' ? 'right' : useCompositeListItem.DEFAULT_SIDES.sideX;
1931
+ const sideY = currentSide === 'top' ? 'bottom' : useCompositeListItem.DEFAULT_SIDES.sideY;
1932
+ return {
1933
+ x,
1934
+ y,
1935
+ data: {
1936
+ sideX,
1937
+ sideY
1938
+ }
1939
+ };
1940
+ }
1941
+ };
1942
+
1943
+ const popupViewportStateMapping = {
1944
+ activationDirection: value => value ? {
1945
+ 'data-activation-direction': value
1946
+ } : null
1947
+ };
1647
1948
  /**
1648
1949
  * Builds morphing viewport containers for popups that animate between trigger-based content.
1649
1950
  * Handles previous-content snapshots, auto-resize, and state attributes for transitions.
@@ -1652,7 +1953,6 @@ function usePopupViewport(parameters) {
1652
1953
  const {
1653
1954
  store,
1654
1955
  side,
1655
- cssVars,
1656
1956
  children
1657
1957
  } = parameters;
1658
1958
  const direction = useScrollLock.useDirection();
@@ -1672,14 +1972,15 @@ function usePopupViewport(parameters) {
1672
1972
  const [newTriggerOffset, setNewTriggerOffset] = React__namespace.useState(null);
1673
1973
  const currentContainerRef = React__namespace.useRef(null);
1674
1974
  const previousContainerRef = React__namespace.useRef(null);
1675
- const onAnimationsFinished = useScrollLock.useAnimationsFinished(currentContainerRef, true, false);
1975
+ const onAnimationsFinished = useScrollLock.useAnimationsFinished(currentContainerRef, true);
1676
1976
  const cleanupFrame = useScrollLock.useAnimationFrame();
1977
+ const cleanupControllerRef = React__namespace.useRef(null);
1677
1978
  const [previousContentDimensions, setPreviousContentDimensions] = React__namespace.useState(null);
1678
1979
  const [showStartingStyleAttribute, setShowStartingStyleAttribute] = React__namespace.useState(false);
1679
1980
  useScrollLock.useIsoLayoutEffect(() => {
1680
- store.set('hasViewport', true);
1981
+ store.set('adaptiveOrigin', adaptiveOrigin);
1681
1982
  return () => {
1682
- store.set('hasViewport', false);
1983
+ store.set('adaptiveOrigin', undefined);
1683
1984
  };
1684
1985
  }, [store]);
1685
1986
  const handleMeasureLayout = useScrollLock.useStableCallback(() => {
@@ -1695,6 +1996,16 @@ function usePopupViewport(parameters) {
1695
1996
  setPreviousContentDimensions(previousDimensions);
1696
1997
  }
1697
1998
  });
1999
+ const armViewportCleanup = useScrollLock.useStableCallback(() => {
2000
+ cleanupControllerRef.current?.abort();
2001
+ const controller = new AbortController();
2002
+ cleanupControllerRef.current = controller;
2003
+ onAnimationsFinished(() => {
2004
+ setPreviousContentNode(null);
2005
+ setPreviousContentDimensions(null);
2006
+ capturedNodeRef.current = null;
2007
+ }, controller.signal);
2008
+ });
1698
2009
  const lastHandledTriggerRef = React__namespace.useRef(null);
1699
2010
  useScrollLock.useIsoLayoutEffect(() => {
1700
2011
  if (!open || !mounted) {
@@ -1712,19 +2023,33 @@ function usePopupViewport(parameters) {
1712
2023
  // so we can pass it to the style hook for animation purposes.
1713
2024
  const offset = calculateRelativePosition(previousActiveTrigger, activeTrigger);
1714
2025
  setNewTriggerOffset(offset);
1715
- cleanupFrame.request(() => {
1716
- ReactDOM__namespace.flushSync(() => {
1717
- setShowStartingStyleAttribute(false);
1718
- });
1719
- onAnimationsFinished(() => {
1720
- setPreviousContentNode(null);
1721
- setPreviousContentDimensions(null);
1722
- capturedNodeRef.current = null;
1723
- });
1724
- });
1725
2026
  lastHandledTriggerRef.current = activeTrigger;
1726
2027
  }
1727
- }, [activeTrigger, previousActiveTrigger, previousContentNode, onAnimationsFinished, cleanupFrame]);
2028
+ }, [activeTrigger, previousActiveTrigger]);
2029
+
2030
+ // Arm cleanup after a trigger change, and re-arm it if the current container remounts
2031
+ // mid-transition when a lagging payload bumps `currentContentKey`. The remount discards
2032
+ // the running entry animation (and with transition-style CSS the replacement mounts at
2033
+ // final styles with no animation at all), so re-run the starting-style choreography —
2034
+ // otherwise the watcher either strands or fires before the previous container's exit
2035
+ // animation finishes.
2036
+ useScrollLock.useIsoLayoutEffect(() => {
2037
+ if (previousContentNode == null) {
2038
+ return;
2039
+ }
2040
+
2041
+ // Abort the stale watcher synchronously. The remount cancels the old container's
2042
+ // animations, and the resulting promise rejection would otherwise run the cleanup
2043
+ // in a microtask before the re-armed watcher below is in place.
2044
+ cleanupControllerRef.current?.abort();
2045
+ setShowStartingStyleAttribute(true);
2046
+ cleanupFrame.request(() => {
2047
+ ReactDOM__namespace.flushSync(() => {
2048
+ setShowStartingStyleAttribute(false);
2049
+ });
2050
+ armViewportCleanup();
2051
+ });
2052
+ }, [currentContentKey, previousContentNode, armViewportCleanup, cleanupFrame]);
1728
2053
 
1729
2054
  // Capture a clone of the current content DOM subtree when not transitioning.
1730
2055
  // We can't store previous React nodes as they may be stateful; instead we capture DOM clones for visual continuity.
@@ -1761,8 +2086,8 @@ function usePopupViewport(parameters) {
1761
2086
  ref: previousContainerRef,
1762
2087
  style: {
1763
2088
  ...(previousContentDimensions ? {
1764
- [cssVars.popupWidth]: `${previousContentDimensions.width}px`,
1765
- [cssVars.popupHeight]: `${previousContentDimensions.height}px`
2089
+ '--popup-width': `${previousContentDimensions.width}px`,
2090
+ '--popup-height': `${previousContentDimensions.height}px`
1766
2091
  } : null),
1767
2092
  position: 'absolute'
1768
2093
  },
@@ -1887,28 +2212,6 @@ function usePopupContentKey(activeTriggerId, payload) {
1887
2212
  return `${activeTriggerId ?? 'current'}-${contentKey}`;
1888
2213
  }
1889
2214
 
1890
- let MenuViewportCssVars = /*#__PURE__*/function (MenuViewportCssVars) {
1891
- /**
1892
- * The width of the parent popup.
1893
- * This variable is placed on the 'previous' container and stores the width of the popup when the previous content was rendered.
1894
- * It can be used to freeze the dimensions of the popup when animating between different content.
1895
- */
1896
- MenuViewportCssVars["popupWidth"] = "--popup-width";
1897
- /**
1898
- * The height of the parent popup.
1899
- * This variable is placed on the 'previous' container and stores the height of the popup when the previous content was rendered.
1900
- * It can be used to freeze the dimensions of the popup when animating between different content.
1901
- */
1902
- MenuViewportCssVars["popupHeight"] = "--popup-height";
1903
- return MenuViewportCssVars;
1904
- }({});
1905
-
1906
- const stateAttributesMapping$2 = {
1907
- activationDirection: value => value ? {
1908
- 'data-activation-direction': value
1909
- } : null
1910
- };
1911
-
1912
2215
  /**
1913
2216
  * A viewport for displaying content transitions.
1914
2217
  * This component is only required if one popup can be opened by multiple triggers, its content
@@ -1938,7 +2241,6 @@ const MenuViewport = /*#__PURE__*/React__namespace.forwardRef(function MenuViewp
1938
2241
  } = usePopupViewport({
1939
2242
  store,
1940
2243
  side,
1941
- cssVars: MenuViewportCssVars,
1942
2244
  children
1943
2245
  });
1944
2246
  const state = {
@@ -1952,47 +2254,50 @@ const MenuViewport = /*#__PURE__*/React__namespace.forwardRef(function MenuViewp
1952
2254
  props: [elementProps, {
1953
2255
  children: childrenToRender
1954
2256
  }],
1955
- stateAttributesMapping: stateAttributesMapping$2
2257
+ stateAttributesMapping: popupViewportStateMapping
1956
2258
  });
1957
2259
  });
1958
2260
  if (process.env.NODE_ENV !== "production") MenuViewport.displayName = "MenuViewport";
1959
2261
 
1960
- class MenuHandle {
1961
- /**
1962
- * Internal store holding the menu's state.
1963
- * @internal
1964
- */
1965
-
2262
+ /**
2263
+ * Controls a Menu imperatively and associates detached `Menu.Trigger` components with a `Menu.Root`.
2264
+ * Create one with `Menu.createHandle()` and pass it to the `handle` prop of the root and of any
2265
+ * triggers rendered outside of it.
2266
+ *
2267
+ * The imperative methods take effect only while a root using this handle is mounted; calls made
2268
+ * before a root attaches (or after it unmounts) are ignored.
2269
+ */
2270
+ class MenuHandle extends DrawerDescription.BasePopupHandle {
1966
2271
  constructor() {
1967
- this.store = new MenuSubmenuTrigger.MenuStore();
2272
+ super(MenuSubmenuTrigger.createNullMenuStore(), 'Menu');
1968
2273
  }
1969
2274
 
1970
2275
  /**
1971
2276
  * Opens the menu and associates it with the trigger with the given id.
1972
- * The trigger must be a Menu.Trigger component with this handle passed as a prop.
1973
2277
  *
1974
- * @param triggerId ID of the trigger to associate with the menu.
2278
+ * This method should only be called in an event handler or an effect (not during rendering).
2279
+ *
2280
+ * @param triggerId ID of the trigger to associate with the menu. The trigger must be a matching
2281
+ * `Menu.Trigger` with this handle passed as a prop.
1975
2282
  */
1976
2283
  open(triggerId) {
1977
- const triggerElement = triggerId ? this.store.context.triggerElements.getById(triggerId) : undefined;
1978
- if (triggerId && !triggerElement) {
1979
- throw new Error(process.env.NODE_ENV !== "production" ? `Base UI: MenuHandle.open: No trigger found with id "${triggerId}".` : useRenderElement.formatErrorMessage(83, triggerId));
1980
- }
1981
- this.store.setOpen(true, useScrollLock.createChangeEventDetails('imperative-action', undefined, triggerElement));
2284
+ this.openByTrigger(triggerId);
1982
2285
  }
1983
2286
 
1984
2287
  /**
1985
2288
  * Closes the menu.
2289
+ *
2290
+ * This method should only be called in an event handler or an effect (not during rendering).
1986
2291
  */
1987
2292
  close() {
1988
- this.store.setOpen(false, useScrollLock.createChangeEventDetails('imperative-action', undefined, undefined));
2293
+ this.closePopup();
1989
2294
  }
1990
2295
 
1991
2296
  /**
1992
- * Indicates whether the menu is currently open.
2297
+ * Whether the menu is currently open. Returns `false` while no root is attached to the handle.
1993
2298
  */
1994
2299
  get isOpen() {
1995
- return this.store.select('open');
2300
+ return this.attachedStore?.select('open') ?? false;
1996
2301
  }
1997
2302
  }
1998
2303
 
@@ -2039,20 +2344,6 @@ function useScrollAreaScrollbarContext() {
2039
2344
  return context;
2040
2345
  }
2041
2346
 
2042
- let ScrollAreaScrollbarCssVars = /*#__PURE__*/function (ScrollAreaScrollbarCssVars) {
2043
- /**
2044
- * The scroll area thumb's height.
2045
- * @type {number}
2046
- */
2047
- ScrollAreaScrollbarCssVars["scrollAreaThumbHeight"] = "--scroll-area-thumb-height";
2048
- /**
2049
- * The scroll area thumb's width.
2050
- * @type {number}
2051
- */
2052
- ScrollAreaScrollbarCssVars["scrollAreaThumbWidth"] = "--scroll-area-thumb-width";
2053
- return ScrollAreaScrollbarCssVars;
2054
- }({});
2055
-
2056
2347
  /**
2057
2348
  * A vertical or horizontal scrollbar for the scroll area.
2058
2349
  * Renders a `<div>` element.
@@ -2073,7 +2364,6 @@ const ScrollAreaScrollbar = /*#__PURE__*/React__namespace.forwardRef(function Sc
2073
2364
  scrollingX,
2074
2365
  scrollingY,
2075
2366
  hiddenState,
2076
- overflowEdges,
2077
2367
  scrollbarYRef,
2078
2368
  scrollbarXRef,
2079
2369
  viewportRef,
@@ -2082,43 +2372,37 @@ const ScrollAreaScrollbar = /*#__PURE__*/React__namespace.forwardRef(function Sc
2082
2372
  handlePointerDown,
2083
2373
  handlePointerUp,
2084
2374
  handleScroll,
2375
+ disableViewportSnap,
2085
2376
  rootId,
2086
2377
  thumbSize,
2087
- hasMeasuredScrollbar
2378
+ hasMeasuredScrollbar,
2379
+ viewportState
2088
2380
  } = ScrollAreaViewport.useScrollAreaRootContext();
2381
+ const vertical = orientation === 'vertical';
2089
2382
  const state = {
2383
+ ...viewportState,
2090
2384
  hovering,
2091
- scrolling: {
2092
- horizontal: scrollingX,
2093
- vertical: scrollingY
2094
- }[orientation],
2095
- orientation,
2096
- hasOverflowX: !hiddenState.x,
2097
- hasOverflowY: !hiddenState.y,
2098
- overflowXStart: overflowEdges.xStart,
2099
- overflowXEnd: overflowEdges.xEnd,
2100
- overflowYStart: overflowEdges.yStart,
2101
- overflowYEnd: overflowEdges.yEnd,
2102
- cornerHidden: hiddenState.corner
2385
+ scrolling: vertical ? scrollingY : scrollingX,
2386
+ orientation
2103
2387
  };
2104
2388
  const direction = useScrollLock.useDirection();
2105
2389
  const hideTrackUntilMeasured = !hasMeasuredScrollbar && !keepMounted;
2106
- const isHidden = orientation === 'vertical' ? hiddenState.y : hiddenState.x;
2390
+ const isHidden = vertical ? hiddenState.y : hiddenState.x;
2107
2391
  const shouldRender = keepMounted || !isHidden;
2108
2392
  React__namespace.useEffect(() => {
2109
2393
  if (!shouldRender) {
2110
2394
  return undefined;
2111
2395
  }
2112
2396
  const viewportEl = viewportRef.current;
2113
- const scrollbarEl = orientation === 'vertical' ? scrollbarYRef.current : scrollbarXRef.current;
2397
+ const scrollbarEl = vertical ? scrollbarYRef.current : scrollbarXRef.current;
2114
2398
  if (!scrollbarEl) {
2115
2399
  return undefined;
2116
2400
  }
2117
2401
  function handleWheel(event) {
2118
- if (!viewportEl || !scrollbarEl || event.ctrlKey) {
2402
+ if (!viewportEl || event.ctrlKey) {
2119
2403
  return;
2120
2404
  }
2121
- const horizontal = orientation === 'horizontal';
2405
+ const horizontal = !vertical;
2122
2406
  const scrollProperty = horizontal ? 'scrollLeft' : 'scrollTop';
2123
2407
  const delta = horizontal ? event.deltaX : event.deltaY;
2124
2408
  if (delta === 0) {
@@ -2145,7 +2429,7 @@ const ScrollAreaScrollbar = /*#__PURE__*/React__namespace.forwardRef(function Sc
2145
2429
  return useScrollLock.addEventListener(scrollbarEl, 'wheel', handleWheel, {
2146
2430
  passive: false
2147
2431
  });
2148
- }, [direction, handleScroll, orientation, scrollbarXRef, scrollbarYRef, shouldRender, viewportRef]);
2432
+ }, [direction, handleScroll, vertical, scrollbarXRef, scrollbarYRef, shouldRender, viewportRef]);
2149
2433
  const props = {
2150
2434
  ...(rootId && {
2151
2435
  'data-id': `${rootId}-scrollbar`
@@ -2155,58 +2439,55 @@ const ScrollAreaScrollbar = /*#__PURE__*/React__namespace.forwardRef(function Sc
2155
2439
  return;
2156
2440
  }
2157
2441
  const target = useScrollLock.getTarget(event.nativeEvent);
2158
- const thumb = orientation === 'vertical' ? thumbYRef.current : thumbXRef.current;
2442
+ const thumbEl = vertical ? thumbYRef.current : thumbXRef.current;
2159
2443
 
2160
2444
  // Ignore clicks on thumb, including cases where React retargets the
2161
2445
  // synthetic event to the track host across a shadow boundary.
2162
- if (thumb && useScrollLock.contains(thumb, target)) {
2446
+ if (thumbEl && useScrollLock.contains(thumbEl, target)) {
2163
2447
  return;
2164
2448
  }
2165
- if (!viewportRef.current) {
2449
+ const viewportEl = viewportRef.current;
2450
+ if (!viewportEl) {
2166
2451
  return;
2167
2452
  }
2168
-
2169
- // Handle Y-axis (vertical) scroll
2170
- if (thumbYRef.current && scrollbarYRef.current && orientation === 'vertical') {
2171
- const thumbYOffset = ScrollAreaViewport.getOffset(thumbYRef.current, 'margin', 'y');
2172
- const scrollbarYOffset = ScrollAreaViewport.getOffset(scrollbarYRef.current, 'padding', 'y');
2173
- const thumbHeight = thumbYRef.current.offsetHeight;
2174
- const trackRectY = scrollbarYRef.current.getBoundingClientRect();
2175
- const clickY = event.clientY - trackRectY.top - thumbHeight / 2 - scrollbarYOffset + thumbYOffset / 2;
2176
- const scrollableContentHeight = viewportRef.current.scrollHeight;
2177
- const viewportHeight = viewportRef.current.clientHeight;
2178
- const maxThumbOffsetY = scrollbarYRef.current.offsetHeight - thumbHeight - scrollbarYOffset - thumbYOffset;
2179
- const scrollRatioY = clickY / maxThumbOffsetY;
2180
- const newScrollTop = scrollRatioY * (scrollableContentHeight - viewportHeight);
2181
- viewportRef.current.scrollTop = newScrollTop;
2182
- }
2183
- if (thumbXRef.current && scrollbarXRef.current && orientation === 'horizontal') {
2184
- const thumbXOffset = ScrollAreaViewport.getOffset(thumbXRef.current, 'margin', 'x');
2185
- const scrollbarXOffset = ScrollAreaViewport.getOffset(scrollbarXRef.current, 'padding', 'x');
2186
- const thumbWidth = thumbXRef.current.offsetWidth;
2187
- const trackRectX = scrollbarXRef.current.getBoundingClientRect();
2188
- const clickX = event.clientX - trackRectX.left - thumbWidth / 2 - scrollbarXOffset + thumbXOffset / 2;
2189
- const scrollableContentWidth = viewportRef.current.scrollWidth;
2190
- const viewportWidth = viewportRef.current.clientWidth;
2191
- const maxThumbOffsetX = scrollbarXRef.current.offsetWidth - thumbWidth - scrollbarXOffset - thumbXOffset;
2192
- const scrollRatioX = clickX / maxThumbOffsetX;
2193
- let newScrollLeft;
2194
- if (direction === 'rtl') {
2195
- // In RTL, invert the scroll direction
2196
- newScrollLeft = (1 - scrollRatioX) * (scrollableContentWidth - viewportWidth);
2197
-
2198
- // Adjust for browsers that use negative scrollLeft in RTL
2199
- if (viewportRef.current.scrollLeft <= 0) {
2200
- newScrollLeft = -newScrollLeft;
2201
- }
2202
- } else {
2203
- newScrollLeft = scrollRatioX * (scrollableContentWidth - viewportWidth);
2204
- }
2205
- viewportRef.current.scrollLeft = newScrollLeft;
2453
+ const scrollbarEl = vertical ? scrollbarYRef.current : scrollbarXRef.current;
2454
+ if (!thumbEl || !scrollbarEl) {
2455
+ return;
2456
+ }
2457
+ const axis = vertical ? 'y' : 'x';
2458
+ const thumbOffset = ScrollAreaViewport.getOffset(thumbEl, 'margin', axis);
2459
+ const scrollbarOffset = ScrollAreaViewport.getOffset(scrollbarEl, 'padding', axis);
2460
+ const thumbSizePx = vertical ? thumbEl.offsetHeight : thumbEl.offsetWidth;
2461
+ const trackRect = scrollbarEl.getBoundingClientRect();
2462
+ const clickPosition = vertical ? event.clientY - trackRect.top - thumbSizePx / 2 - scrollbarOffset + thumbOffset / 2 : event.clientX - trackRect.left - thumbSizePx / 2 - scrollbarOffset + thumbOffset / 2;
2463
+ const scrollableSize = vertical ? viewportEl.scrollHeight : viewportEl.scrollWidth;
2464
+ const viewportSize = vertical ? viewportEl.clientHeight : viewportEl.clientWidth;
2465
+ const trackSize = vertical ? scrollbarEl.offsetHeight : scrollbarEl.offsetWidth;
2466
+ const maxThumbOffset = trackSize - thumbSizePx - scrollbarOffset - thumbOffset;
2467
+ // A short or heavily padded track can drive `maxThumbOffset` to zero or
2468
+ // negative once the thumb hits its `MIN_THUMB_SIZE` floor. Dividing by it
2469
+ // would yield a non-finite (`Infinity`/`NaN`) or inverted scroll position.
2470
+ if (maxThumbOffset <= 0) {
2471
+ return;
2472
+ }
2473
+ const scrollRatio = clickPosition / maxThumbOffset;
2474
+ const maxScrollDistance = scrollableSize - viewportSize;
2475
+
2476
+ // Disable snapping before the jump-to-click assignment, or the
2477
+ // assigned position quantizes to the nearest snap point and the thumb
2478
+ // stays offset from the pointer for the whole drag. `handlePointerDown`
2479
+ // below re-runs this as a guarded no-op for the thumb-drag path.
2480
+ disableViewportSnap();
2481
+ if (vertical) {
2482
+ viewportEl.scrollTop = scrollRatio * maxScrollDistance;
2483
+ } else if (direction === 'rtl') {
2484
+ viewportEl.scrollLeft = -(1 - scrollRatio) * maxScrollDistance;
2485
+ } else {
2486
+ viewportEl.scrollLeft = scrollRatio * maxScrollDistance;
2206
2487
  }
2207
2488
  handleScroll({
2208
- x: viewportRef.current.scrollLeft,
2209
- y: viewportRef.current.scrollTop
2489
+ x: viewportEl.scrollLeft,
2490
+ y: viewportEl.scrollTop
2210
2491
  });
2211
2492
  handlePointerDown(event);
2212
2493
  },
@@ -2220,34 +2501,30 @@ const ScrollAreaScrollbar = /*#__PURE__*/React__namespace.forwardRef(function Sc
2220
2501
  WebkitUserSelect: 'none',
2221
2502
  userSelect: 'none',
2222
2503
  visibility: hideTrackUntilMeasured ? 'hidden' : undefined,
2223
- ...(orientation === 'vertical' && {
2504
+ ...(vertical ? {
2224
2505
  top: 0,
2225
- bottom: `var(${ScrollAreaViewport.ScrollAreaRootCssVars.scrollAreaCornerHeight})`,
2506
+ bottom: 'var(--scroll-area-corner-height)',
2226
2507
  insetInlineEnd: 0,
2227
- [ScrollAreaScrollbarCssVars.scrollAreaThumbHeight]: `${thumbSize.height}px`
2228
- }),
2229
- ...(orientation === 'horizontal' && {
2508
+ ['--scroll-area-thumb-height']: `${thumbSize.height}px`
2509
+ } : {
2230
2510
  insetInlineStart: 0,
2231
- insetInlineEnd: `var(${ScrollAreaViewport.ScrollAreaRootCssVars.scrollAreaCornerWidth})`,
2511
+ insetInlineEnd: 'var(--scroll-area-corner-width)',
2232
2512
  bottom: 0,
2233
- [ScrollAreaScrollbarCssVars.scrollAreaThumbWidth]: `${thumbSize.width}px`
2513
+ ['--scroll-area-thumb-width']: `${thumbSize.width}px`
2234
2514
  })
2235
2515
  }
2236
2516
  };
2237
2517
  const element = useRenderElement.useRenderElement('div', componentProps, {
2238
- ref: [forwardedRef, orientation === 'vertical' ? scrollbarYRef : scrollbarXRef],
2518
+ ref: [forwardedRef, vertical ? scrollbarYRef : scrollbarXRef],
2239
2519
  state,
2240
2520
  props: [props, elementProps],
2241
2521
  stateAttributesMapping: ScrollAreaViewport.scrollAreaStateAttributesMapping
2242
2522
  });
2243
- const contextValue = React__namespace.useMemo(() => ({
2244
- orientation
2245
- }), [orientation]);
2246
2523
  if (!shouldRender) {
2247
2524
  return null;
2248
2525
  }
2249
2526
  return /*#__PURE__*/jsxRuntime.jsx(ScrollAreaScrollbarContext.Provider, {
2250
- value: contextValue,
2527
+ value: orientation,
2251
2528
  children: element
2252
2529
  });
2253
2530
  });
@@ -2335,43 +2612,30 @@ const ScrollAreaThumb = /*#__PURE__*/React__namespace.forwardRef(function Scroll
2335
2612
  handlePointerDown,
2336
2613
  handlePointerMove,
2337
2614
  handlePointerUp,
2338
- setScrollingX,
2339
- setScrollingY,
2340
2615
  scrollingX,
2341
2616
  scrollingY,
2342
2617
  hasMeasuredScrollbar
2343
2618
  } = ScrollAreaViewport.useScrollAreaRootContext();
2344
- const {
2345
- orientation
2346
- } = useScrollAreaScrollbarContext();
2619
+ const orientation = useScrollAreaScrollbarContext();
2620
+ const vertical = orientation === 'vertical';
2347
2621
  const state = {
2348
- scrolling: orientation === 'horizontal' ? scrollingX : scrollingY,
2622
+ scrolling: vertical ? scrollingY : scrollingX,
2349
2623
  orientation
2350
2624
  };
2351
- function endDrag(event) {
2352
- if (orientation === 'vertical') {
2353
- setScrollingY(false);
2354
- }
2355
- if (orientation === 'horizontal') {
2356
- setScrollingX(false);
2357
- }
2358
- handlePointerUp(event);
2359
- }
2360
2625
  const element = useRenderElement.useRenderElement('div', componentProps, {
2361
- ref: [forwardedRef, orientation === 'vertical' ? thumbYRef : thumbXRef],
2626
+ ref: [forwardedRef, vertical ? thumbYRef : thumbXRef],
2362
2627
  state,
2363
2628
  props: [{
2364
2629
  onPointerDown: handlePointerDown,
2365
2630
  onPointerMove: handlePointerMove,
2366
- onPointerUp: endDrag,
2367
- onPointerCancel: endDrag,
2631
+ onPointerUp: handlePointerUp,
2632
+ onPointerCancel: handlePointerUp,
2368
2633
  style: {
2369
2634
  visibility: hasMeasuredScrollbar ? undefined : 'hidden',
2370
- ...(orientation === 'vertical' && {
2371
- height: `var(${ScrollAreaScrollbarCssVars.scrollAreaThumbHeight})`
2372
- }),
2373
- ...(orientation === 'horizontal' && {
2374
- width: `var(${ScrollAreaScrollbarCssVars.scrollAreaThumbWidth})`
2635
+ ...(vertical ? {
2636
+ height: 'var(--scroll-area-thumb-height)'
2637
+ } : {
2638
+ width: 'var(--scroll-area-thumb-width)'
2375
2639
  })
2376
2640
  }
2377
2641
  }, elementProps]
@@ -2429,34 +2693,34 @@ var index_parts$3 = /*#__PURE__*/Object.freeze({
2429
2693
 
2430
2694
  var index_parts$2 = /*#__PURE__*/Object.freeze({
2431
2695
  __proto__: null,
2432
- Arrow: ComboboxChipRemove.ComboboxArrow,
2433
- Backdrop: ComboboxChipRemove.ComboboxBackdrop,
2434
- Chip: ComboboxChipRemove.ComboboxChip,
2435
- ChipRemove: ComboboxChipRemove.ComboboxChipRemove,
2436
- Chips: ComboboxChipRemove.ComboboxChips,
2437
- Clear: ComboboxChipRemove.ComboboxClear,
2438
- Collection: ComboboxChipRemove.ComboboxCollection,
2439
- Empty: ComboboxChipRemove.ComboboxEmpty,
2440
- Group: ComboboxChipRemove.ComboboxGroup,
2441
- GroupLabel: ComboboxChipRemove.ComboboxGroupLabel,
2442
- Icon: ComboboxChipRemove.ComboboxIcon,
2443
- Input: ComboboxChipRemove.ComboboxInput,
2444
- InputGroup: ComboboxChipRemove.ComboboxInputGroup,
2445
- Item: ComboboxChipRemove.ComboboxItem,
2446
- ItemIndicator: ComboboxChipRemove.ComboboxItemIndicator,
2447
- Label: ComboboxChipRemove.ComboboxLabel,
2448
- List: ComboboxChipRemove.ComboboxList,
2449
- Popup: ComboboxChipRemove.ComboboxPopup,
2450
- Portal: ComboboxChipRemove.ComboboxPortal,
2451
- Positioner: ComboboxChipRemove.ComboboxPositioner,
2452
- Root: ComboboxChipRemove.ComboboxRoot,
2453
- Row: ComboboxChipRemove.ComboboxRow,
2454
- Separator: Separator.Separator,
2455
- Status: ComboboxChipRemove.ComboboxStatus,
2456
- Trigger: ComboboxChipRemove.ComboboxTrigger,
2457
- Value: ComboboxChipRemove.ComboboxValue,
2458
- useFilter: ComboboxChipRemove.useComboboxFilter,
2459
- useFilteredItems: ComboboxChipRemove.useFilteredItems
2696
+ Arrow: ComboboxSeparator.ComboboxArrow,
2697
+ Backdrop: ComboboxSeparator.ComboboxBackdrop,
2698
+ Chip: ComboboxSeparator.ComboboxChip,
2699
+ ChipRemove: ComboboxSeparator.ComboboxChipRemove,
2700
+ Chips: ComboboxSeparator.ComboboxChips,
2701
+ Clear: ComboboxSeparator.ComboboxClear,
2702
+ Collection: ComboboxSeparator.ComboboxCollection,
2703
+ Empty: ComboboxSeparator.ComboboxEmpty,
2704
+ Group: ComboboxSeparator.ComboboxGroup,
2705
+ GroupLabel: ComboboxSeparator.ComboboxGroupLabel,
2706
+ Icon: ComboboxSeparator.ComboboxIcon,
2707
+ Input: ComboboxSeparator.ComboboxInput,
2708
+ InputGroup: ComboboxSeparator.ComboboxInputGroup,
2709
+ Item: ComboboxSeparator.ComboboxItem,
2710
+ ItemIndicator: ComboboxSeparator.ComboboxItemIndicator,
2711
+ Label: ComboboxSeparator.ComboboxLabel,
2712
+ List: ComboboxSeparator.ComboboxList,
2713
+ Popup: ComboboxSeparator.ComboboxPopup,
2714
+ Portal: ComboboxSeparator.ComboboxPortal,
2715
+ Positioner: ComboboxSeparator.ComboboxPositioner,
2716
+ Root: ComboboxSeparator.ComboboxRoot,
2717
+ Row: ComboboxSeparator.ComboboxRow,
2718
+ Separator: ComboboxSeparator.ComboboxSeparator,
2719
+ Status: ComboboxSeparator.ComboboxStatus,
2720
+ Trigger: ComboboxSeparator.ComboboxTrigger,
2721
+ Value: ComboboxSeparator.ComboboxValue,
2722
+ useFilter: ComboboxSeparator.useComboboxFilter,
2723
+ useFilteredItems: ComboboxSeparator.useFilteredItems
2460
2724
  });
2461
2725
 
2462
2726
  const NumberFieldScrubAreaContext = /*#__PURE__*/React__namespace.createContext(undefined);
@@ -2471,7 +2735,7 @@ function useNumberFieldScrubAreaContext() {
2471
2735
 
2472
2736
  // Calculates the bounds the virtual cursor wraps within, as absolute edge coordinates.
2473
2737
  function getViewportRect(teleportDistance, scrubAreaEl) {
2474
- const win = floatingUi_utils.getWindow(scrubAreaEl);
2738
+ const win = useScrollLock.getWindow(scrubAreaEl);
2475
2739
  if (teleportDistance != null) {
2476
2740
  const rect = scrubAreaEl.getBoundingClientRect();
2477
2741
  return {
@@ -2498,22 +2762,6 @@ function getViewportRect(teleportDistance, scrubAreaEl) {
2498
2762
  };
2499
2763
  }
2500
2764
 
2501
- // This lets us invert the scale of the cursor to match the OS scale, in which the cursor doesn't
2502
- // scale with the content on pinch-zoom.
2503
- function subscribeToVisualViewportResize(element, visualScaleRef) {
2504
- const vV = floatingUi_utils.getWindow(element).visualViewport;
2505
- if (!vV) {
2506
- return () => {};
2507
- }
2508
- function handleVisualResize() {
2509
- if (vV) {
2510
- visualScaleRef.current = vV.scale;
2511
- }
2512
- }
2513
- handleVisualResize();
2514
- return useScrollLock.addEventListener(vV, 'resize', handleVisualResize);
2515
- }
2516
-
2517
2765
  const SCRUB_AREA_STYLE = {
2518
2766
  touchAction: 'none',
2519
2767
  WebkitUserSelect: 'none',
@@ -2539,8 +2787,6 @@ const NumberFieldScrubArea = /*#__PURE__*/React__namespace.forwardRef(function N
2539
2787
  const {
2540
2788
  state,
2541
2789
  setIsScrubbing: setRootScrubbing,
2542
- disabled,
2543
- readOnly,
2544
2790
  inputRef,
2545
2791
  incrementValue,
2546
2792
  allowInputSyncRef,
@@ -2549,6 +2795,10 @@ const NumberFieldScrubArea = /*#__PURE__*/React__namespace.forwardRef(function N
2549
2795
  lastChangedValueRef,
2550
2796
  valueRef
2551
2797
  } = NumberFieldInput.useNumberFieldRootContext();
2798
+ const {
2799
+ disabled,
2800
+ readOnly
2801
+ } = state;
2552
2802
  const scrubAreaRef = React__namespace.useRef(null);
2553
2803
  const isScrubbingRef = React__namespace.useRef(false);
2554
2804
  const didMoveRef = React__namespace.useRef(false);
@@ -2558,21 +2808,15 @@ const NumberFieldScrubArea = /*#__PURE__*/React__namespace.forwardRef(function N
2558
2808
  x: 0,
2559
2809
  y: 0
2560
2810
  });
2561
- const visualScaleRef = React__namespace.useRef(1);
2562
2811
  const exitPointerLockTimeout = useScrollLock.useTimeout();
2563
2812
  const [isTouchInput, setIsTouchInput] = React__namespace.useState(false);
2564
2813
  const [isPointerLockDenied, setIsPointerLockDenied] = React__namespace.useState(false);
2565
2814
  const [isScrubbing, setIsScrubbing] = React__namespace.useState(false);
2566
- React__namespace.useEffect(() => {
2567
- if (!isScrubbing || !scrubAreaCursorRef.current) {
2568
- return undefined;
2569
- }
2570
- return subscribeToVisualViewportResize(scrubAreaCursorRef.current, visualScaleRef);
2571
- }, [isScrubbing]);
2572
- function updateCursorTransform(x, y) {
2573
- if (scrubAreaCursorRef.current) {
2574
- scrubAreaCursorRef.current.style.transform = `translate3d(${x}px,${y}px,0) scale(${1 / visualScaleRef.current})`;
2575
- }
2815
+ function updateCursorTransform(virtualCursor, x, y) {
2816
+ // Invert the visual viewport scale so the cursor matches the OS cursor, which doesn't
2817
+ // scale with the content on pinch-zoom.
2818
+ const scale = useScrollLock.getWindow(virtualCursor).visualViewport?.scale ?? 1;
2819
+ virtualCursor.style.transform = `translate3d(${x}px,${y}px,0) scale(${1 / scale})`;
2576
2820
  }
2577
2821
  const onScrub = useScrollLock.useStableCallback(({
2578
2822
  movementX,
@@ -2585,24 +2829,23 @@ const NumberFieldScrubArea = /*#__PURE__*/React__namespace.forwardRef(function N
2585
2829
  }
2586
2830
  const rect = getViewportRect(teleportDistance, scrubAreaEl);
2587
2831
  const coords = virtualCursorCoords.current;
2832
+
2833
+ // Wrap the cursor to the opposite edge when its center crosses a viewport bound.
2834
+ const wrap = (coord, halfSize, low, high) => {
2835
+ if (coord + halfSize < low) {
2836
+ return high - halfSize;
2837
+ }
2838
+ if (coord + halfSize > high) {
2839
+ return low - halfSize;
2840
+ }
2841
+ return coord;
2842
+ };
2588
2843
  const newCoords = {
2589
- x: Math.round(coords.x + movementX),
2590
- y: Math.round(coords.y + movementY)
2844
+ x: wrap(Math.round(coords.x + movementX), virtualCursor.offsetWidth / 2, rect.left, rect.right),
2845
+ y: wrap(Math.round(coords.y + movementY), virtualCursor.offsetHeight / 2, rect.top, rect.bottom)
2591
2846
  };
2592
- const cursorWidth = virtualCursor.offsetWidth;
2593
- const cursorHeight = virtualCursor.offsetHeight;
2594
- if (newCoords.x + cursorWidth / 2 < rect.left) {
2595
- newCoords.x = rect.right - cursorWidth / 2;
2596
- } else if (newCoords.x + cursorWidth / 2 > rect.right) {
2597
- newCoords.x = rect.left - cursorWidth / 2;
2598
- }
2599
- if (newCoords.y + cursorHeight / 2 < rect.top) {
2600
- newCoords.y = rect.bottom - cursorHeight / 2;
2601
- } else if (newCoords.y + cursorHeight / 2 > rect.bottom) {
2602
- newCoords.y = rect.top - cursorHeight / 2;
2603
- }
2604
2847
  virtualCursorCoords.current = newCoords;
2605
- updateCursorTransform(newCoords.x, newCoords.y);
2848
+ updateCursorTransform(virtualCursor, newCoords.x, newCoords.y);
2606
2849
  });
2607
2850
  const onScrubbingChange = useScrollLock.useStableCallback((scrubbingValue, {
2608
2851
  clientX,
@@ -2621,7 +2864,7 @@ const NumberFieldScrubArea = /*#__PURE__*/React__namespace.forwardRef(function N
2621
2864
  y: clientY - virtualCursor.offsetHeight / 2
2622
2865
  };
2623
2866
  virtualCursorCoords.current = initialCoords;
2624
- updateCursorTransform(initialCoords.x, initialCoords.y);
2867
+ updateCursorTransform(virtualCursor, initialCoords.x, initialCoords.y);
2625
2868
  });
2626
2869
  React__namespace.useEffect(function registerGlobalScrubbingEventListeners() {
2627
2870
  // Only listen while actively scrubbing; avoids unrelated pointerup events committing.
@@ -2645,7 +2888,7 @@ const NumberFieldScrubArea = /*#__PURE__*/React__namespace.forwardRef(function N
2645
2888
  const pointerDownTarget = pointerDownTargetRef.current;
2646
2889
  const input = inputRef.current;
2647
2890
  if (!didMoveRef.current && pointerDownTarget != null && input) {
2648
- pointerDownTarget.dispatchEvent(new (floatingUi_utils.getWindow(input).MouseEvent)('click', {
2891
+ pointerDownTarget.dispatchEvent(new (useScrollLock.getWindow(input).MouseEvent)('click', {
2649
2892
  bubbles: true,
2650
2893
  cancelable: true
2651
2894
  }));
@@ -2663,6 +2906,9 @@ const NumberFieldScrubArea = /*#__PURE__*/React__namespace.forwardRef(function N
2663
2906
  }
2664
2907
  }
2665
2908
  function handleScrubPointerMove(event) {
2909
+ // The effects below can tear down and re-run without unmounting (`<Activity>`), which
2910
+ // clears the ref while `isScrubbing` stays `true` and re-attaches this listener. The ref
2911
+ // is the source of truth for whether a pointer is actually down.
2666
2912
  if (!isScrubbingRef.current) {
2667
2913
  return;
2668
2914
  }
@@ -2691,7 +2937,7 @@ const NumberFieldScrubArea = /*#__PURE__*/React__namespace.forwardRef(function N
2691
2937
  }
2692
2938
  }
2693
2939
  }
2694
- const win = floatingUi_utils.getWindow(inputRef.current);
2940
+ const win = useScrollLock.getWindow(inputRef.current);
2695
2941
  const unsubscribe = useScrollLock.mergeCleanups(useScrollLock.addEventListener(win, 'pointerup', handleScrubPointerUp, true), useScrollLock.addEventListener(win, 'pointermove', handleScrubPointerMove, true));
2696
2942
  return () => {
2697
2943
  exitPointerLockTimeout.clear();
@@ -2730,8 +2976,7 @@ const NumberFieldScrubArea = /*#__PURE__*/React__namespace.forwardRef(function N
2730
2976
  role: 'presentation',
2731
2977
  style: SCRUB_AREA_STYLE,
2732
2978
  async onPointerDown(event) {
2733
- const isMainButton = !event.button || event.button === 0;
2734
- if (event.defaultPrevented || readOnly || !isMainButton || disabled) {
2979
+ if (event.defaultPrevented || readOnly || event.button || disabled) {
2735
2980
  return;
2736
2981
  }
2737
2982
  const isTouch = event.pointerType === 'touch';
@@ -2871,7 +3116,8 @@ const SelectLabel = /*#__PURE__*/React__namespace.forwardRef(function SelectLabe
2871
3116
  id: defaultLabelId,
2872
3117
  fallbackControlId: triggerElement?.id ?? rootId,
2873
3118
  setLabelId(nextLabelId) {
2874
- store.set('labelId', nextLabelId);
3119
+ const resolvedLabelId = typeof nextLabelId === 'function' ? nextLabelId(store.state.labelId) : nextLabelId;
3120
+ store.set('labelId', resolvedLabelId);
2875
3121
  }
2876
3122
  });
2877
3123
  return useRenderElement.useRenderElement('div', componentProps, {
@@ -2916,7 +3162,7 @@ const SelectIcon = /*#__PURE__*/React__namespace.forwardRef(function SelectIcon(
2916
3162
  });
2917
3163
  if (process.env.NODE_ENV !== "production") SelectIcon.displayName = "SelectIcon";
2918
3164
 
2919
- const stateAttributesMapping$1 = {
3165
+ const stateAttributesMapping = {
2920
3166
  ...useScrollLock.popupStateMapping,
2921
3167
  ...useScrollLock.transitionStatusMapping
2922
3168
  };
@@ -2955,17 +3201,12 @@ const SelectBackdrop = /*#__PURE__*/React__namespace.forwardRef(function SelectB
2955
3201
  WebkitUserSelect: 'none'
2956
3202
  }
2957
3203
  }, elementProps],
2958
- stateAttributesMapping: stateAttributesMapping$1
3204
+ stateAttributesMapping
2959
3205
  });
2960
3206
  return element;
2961
3207
  });
2962
3208
  if (process.env.NODE_ENV !== "production") SelectBackdrop.displayName = "SelectBackdrop";
2963
3209
 
2964
- const stateAttributesMapping = {
2965
- ...useScrollLock.popupStateMapping,
2966
- ...useScrollLock.transitionStatusMapping
2967
- };
2968
-
2969
3210
  /**
2970
3211
  * Displays an element positioned against the select popup anchor.
2971
3212
  * Renders a `<div>` element.
@@ -3004,7 +3245,7 @@ const SelectArrow = /*#__PURE__*/React__namespace.forwardRef(function SelectArro
3004
3245
  style: arrowStyles,
3005
3246
  'aria-hidden': true
3006
3247
  }, elementProps],
3007
- stateAttributesMapping
3248
+ stateAttributesMapping: useScrollLock.popupTransitionStateMapping
3008
3249
  });
3009
3250
  if (alignItemWithTriggerActive) {
3010
3251
  return null;
@@ -3022,7 +3263,7 @@ const SelectScrollArrow = /*#__PURE__*/React__namespace.forwardRef(function Sele
3022
3263
  className,
3023
3264
  style,
3024
3265
  direction,
3025
- keepMounted = false,
3266
+ keepMounted,
3026
3267
  ...elementProps
3027
3268
  } = componentProps;
3028
3269
  const isUp = direction === 'up';
@@ -3053,12 +3294,10 @@ const SelectScrollArrow = /*#__PURE__*/React__namespace.forwardRef(function Sele
3053
3294
  } = useScrollLock.useTransitionStatus(visible);
3054
3295
  useScrollLock.useIsoLayoutEffect(() => {
3055
3296
  scrollArrowsMountedCountRef.current += 1;
3056
- if (!store.state.hasScrollArrows) {
3057
- store.set('hasScrollArrows', true);
3058
- }
3297
+ store.set('hasScrollArrows', true);
3059
3298
  return () => {
3060
3299
  scrollArrowsMountedCountRef.current = Math.max(0, scrollArrowsMountedCountRef.current - 1);
3061
- if (scrollArrowsMountedCountRef.current === 0 && store.state.hasScrollArrows) {
3300
+ if (scrollArrowsMountedCountRef.current === 0) {
3062
3301
  store.set('hasScrollArrows', false);
3063
3302
  }
3064
3303
  };
@@ -3095,7 +3334,7 @@ const SelectScrollArrow = /*#__PURE__*/React__namespace.forwardRef(function Sele
3095
3334
  return;
3096
3335
  }
3097
3336
  store.set('activeIndex', null);
3098
- handleScrollArrowVisibility();
3337
+ handleScrollArrowVisibility(scroller);
3099
3338
  const maxScrollTop = scrollEdges.getMaxScrollOffset(scroller.scrollHeight, scroller.clientHeight);
3100
3339
  const scrollTop = scrollEdges.normalizeScrollOffset(scroller.scrollTop, maxScrollTop);
3101
3340
  const isScrolledToEdge = scrollTop === (isUp ? 0 : maxScrollTop);
@@ -3103,11 +3342,6 @@ const SelectScrollArrow = /*#__PURE__*/React__namespace.forwardRef(function Sele
3103
3342
  if (scrollTop !== scroller.scrollTop) {
3104
3343
  scroller.scrollTop = scrollTop;
3105
3344
  }
3106
-
3107
- // Fallback when there are no items registered yet.
3108
- if (items.length === 0) {
3109
- store.set(isUp ? 'scrollUpArrowVisible' : 'scrollDownArrowVisible', !isScrolledToEdge);
3110
- }
3111
3345
  if (isScrolledToEdge) {
3112
3346
  timeout.clear();
3113
3347
  return;
@@ -3196,6 +3430,14 @@ const SelectScrollUpArrow = /*#__PURE__*/React__namespace.forwardRef(function Se
3196
3430
  });
3197
3431
  if (process.env.NODE_ENV !== "production") SelectScrollUpArrow.displayName = "SelectScrollUpArrow";
3198
3432
 
3433
+ /**
3434
+ * A visual separator between items or groups.
3435
+ * Renders a `<div>` element.
3436
+ *
3437
+ * Documentation: [Base UI Select](https://base-ui.com/react/components/select)
3438
+ */
3439
+ const SelectSeparator = ComboboxSeparator.ListboxSeparator;
3440
+
3199
3441
  var index_parts = /*#__PURE__*/Object.freeze({
3200
3442
  __proto__: null,
3201
3443
  Arrow: SelectArrow,
@@ -3214,7 +3456,7 @@ var index_parts = /*#__PURE__*/Object.freeze({
3214
3456
  Root: SelectGroupLabel.SelectRoot,
3215
3457
  ScrollDownArrow: SelectScrollDownArrow,
3216
3458
  ScrollUpArrow: SelectScrollUpArrow,
3217
- Separator: Separator.Separator,
3459
+ Separator: SelectSeparator,
3218
3460
  Trigger: SelectGroupLabel.SelectTrigger,
3219
3461
  Value: SelectGroupLabel.SelectValue
3220
3462
  });