@surdeddd/bottom-sheet 0.8.2 → 0.9.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 (46) hide show
  1. package/dist/{BottomSheetEngine-7BDK2OWX.d.cts → BottomSheetEngine-BcyruNP1.d.cts} +12 -6
  2. package/dist/{BottomSheetEngine-BFTb0-4Z.d.ts → BottomSheetEngine-CBRcBwV-.d.ts} +12 -6
  3. package/dist/element.cjs +530 -349
  4. package/dist/element.d.cts +2 -1
  5. package/dist/element.d.ts +2 -1
  6. package/dist/element.iife.global.js +2 -2
  7. package/dist/element.js +530 -349
  8. package/dist/index.cjs +438 -294
  9. package/dist/index.d.cts +3 -3
  10. package/dist/index.d.ts +3 -3
  11. package/dist/index.js +438 -294
  12. package/dist/integrations/formik.d.cts +1 -1
  13. package/dist/integrations/formik.d.ts +1 -1
  14. package/dist/integrations/react-hook-form.d.cts +1 -1
  15. package/dist/integrations/react-hook-form.d.ts +1 -1
  16. package/dist/overlay.cjs +160 -96
  17. package/dist/overlay.d.cts +2 -0
  18. package/dist/overlay.d.ts +2 -0
  19. package/dist/overlay.js +160 -96
  20. package/dist/preact.d.cts +1 -1
  21. package/dist/preact.d.ts +1 -1
  22. package/dist/qwik.cjs +387 -211
  23. package/dist/qwik.d.cts +4 -3
  24. package/dist/qwik.d.ts +4 -3
  25. package/dist/qwik.js +387 -211
  26. package/dist/react.cjs +472 -296
  27. package/dist/react.d.cts +3 -2
  28. package/dist/react.d.ts +3 -2
  29. package/dist/react.js +472 -296
  30. package/dist/solid.cjs +417 -215
  31. package/dist/solid.d.cts +21 -7
  32. package/dist/solid.d.ts +21 -7
  33. package/dist/solid.js +417 -215
  34. package/dist/solid.source.jsx +412 -209
  35. package/dist/svelte-core.cjs +328 -209
  36. package/dist/svelte-core.js +328 -209
  37. package/dist/svelte-src/BottomSheet.svelte +56 -0
  38. package/dist/svelte.cjs +365 -210
  39. package/dist/svelte.d.cts +4 -0
  40. package/dist/svelte.d.ts +4 -0
  41. package/dist/svelte.js +365 -210
  42. package/dist/vue.cjs +358 -211
  43. package/dist/vue.d.cts +6 -0
  44. package/dist/vue.d.ts +6 -0
  45. package/dist/vue.js +358 -211
  46. package/package.json +6 -6
package/dist/react.js CHANGED
@@ -272,18 +272,25 @@ var SnapResolver = class {
272
272
  __publicField(this, "resolved", []);
273
273
  __publicField(this, "maxAxisSize", 0);
274
274
  __publicField(this, "rangeCache", null);
275
+ __publicField(this, "notified", false);
275
276
  this.raw = raw;
276
277
  this.allowedIds = allowed ?? raw.map((p) => p.id);
277
278
  this.recompute();
278
279
  }
279
280
  recompute() {
280
- this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
281
- this.maxAxisSize = this.resolved.reduce(
281
+ const measure = this.measureFit;
282
+ let measured;
283
+ const measureOnce = measure ? () => measured ?? (measured = measure()) : void 0;
284
+ this.resolved = resolveSnapList(this.raw, this.mode, measureOnce);
285
+ const nextMax = this.resolved.reduce(
282
286
  (m, s) => s.size > m ? s.size : m,
283
287
  0
284
288
  );
285
289
  this.rangeCache = null;
286
- this.onMaxAxisSizeChange?.(this.maxAxisSize);
290
+ const changed = nextMax !== this.maxAxisSize || !this.notified;
291
+ this.maxAxisSize = nextMax;
292
+ this.notified = true;
293
+ if (changed) this.onMaxAxisSizeChange?.(this.maxAxisSize);
287
294
  }
288
295
  setRaw(raw) {
289
296
  this.raw = raw;
@@ -436,7 +443,10 @@ var ensureHandler = () => {
436
443
  target.consumed = true;
437
444
  const idx = closers.indexOf(target);
438
445
  if (idx !== -1) closers.splice(idx, 1);
439
- target.close();
446
+ const settled = target;
447
+ void Promise.resolve(settled.close()).then(() => {
448
+ if (settled.isOpen()) settled.restore();
449
+ });
440
450
  });
441
451
  };
442
452
  var programmaticBack = () => {
@@ -472,16 +482,7 @@ function installRoute(deps) {
472
482
  pushed = false;
473
483
  }
474
484
  };
475
- const onOpen = () => {
476
- if (closer) return;
477
- closer = {
478
- isOpen: () => !deps.isDestroyed() && deps.getSize() > 0,
479
- isTop: () => deps.isTopSheet(),
480
- close: () => void deps.close(),
481
- consumed: false
482
- };
483
- ensureHandler();
484
- closers.push(closer);
485
+ const pushEntry = () => {
485
486
  try {
486
487
  if (deps.routedTo !== void 0) {
487
488
  history.pushState({ __bsRouted: deps.sheetId }, "", deps.routedTo);
@@ -493,6 +494,25 @@ function installRoute(deps) {
493
494
  pushed = false;
494
495
  }
495
496
  };
497
+ const onOpen = () => {
498
+ if (closer) return;
499
+ const self = {
500
+ isOpen: () => !deps.isDestroyed() && deps.getSize() > 0,
501
+ isTop: () => deps.isTopSheet(),
502
+ close: () => deps.close(),
503
+ restore: () => {
504
+ if (deps.isDestroyed() || closer !== self) return;
505
+ self.consumed = false;
506
+ if (!closers.includes(self)) closers.push(self);
507
+ pushEntry();
508
+ },
509
+ consumed: false
510
+ };
511
+ closer = self;
512
+ ensureHandler();
513
+ closers.push(self);
514
+ pushEntry();
515
+ };
496
516
  const unsubscribeOpen = deps.on("open", onOpen);
497
517
  const unsubscribeClose = deps.on("close", teardownEntry);
498
518
  if (deps.getSize() > 0) onOpen();
@@ -539,11 +559,13 @@ var CONTENT_PULL_THRESHOLD = 60;
539
559
  function installContentSwipe(deps) {
540
560
  const { container } = deps;
541
561
  let startY = 0;
562
+ let startScrollTop = 0;
542
563
  let moved = false;
543
564
  const onTouchStart = (e) => {
544
565
  const t = e.touches[0];
545
566
  if (!t) return;
546
567
  startY = t.clientY;
568
+ startScrollTop = container.scrollTop;
547
569
  moved = false;
548
570
  };
549
571
  const onTouchMove = (e) => {
@@ -554,6 +576,7 @@ function installContentSwipe(deps) {
554
576
  const onTouchEnd = (e) => {
555
577
  if (!moved) return;
556
578
  if (deps.isDragging() || deps.isAnimating()) return;
579
+ if (container.scrollTop !== startScrollTop) return;
557
580
  const t = e.changedTouches[0];
558
581
  if (!t) return;
559
582
  const deltaY = t.clientY - startY;
@@ -665,6 +688,131 @@ function installResizeObserver(deps) {
665
688
  };
666
689
  }
667
690
 
691
+ // src/core/features/fit-observer.ts
692
+ var contentTargets = (scroller) => {
693
+ if (!scroller) return [];
694
+ const hasSlot = typeof HTMLSlotElement !== "undefined";
695
+ const out = [];
696
+ for (const child of Array.from(scroller.children)) {
697
+ if (hasSlot && child instanceof HTMLSlotElement) {
698
+ for (const el of child.assignedElements()) out.push(el);
699
+ } else {
700
+ out.push(child);
701
+ }
702
+ }
703
+ return out;
704
+ };
705
+ function installFitObserver(deps) {
706
+ if (typeof ResizeObserver === "undefined") return () => {
707
+ };
708
+ if (!deps.hasFitSnap()) return () => {
709
+ };
710
+ let raf = 0;
711
+ const schedule = () => {
712
+ if (raf) return;
713
+ raf = requestAnimationFrame(() => {
714
+ raf = 0;
715
+ if (deps.isDestroyed() || deps.isDragging()) return;
716
+ deps.recompute();
717
+ });
718
+ };
719
+ const ro = new ResizeObserver(schedule);
720
+ ro.observe(deps.handle);
721
+ const scroller = deps.scrollContainer;
722
+ const observed = /* @__PURE__ */ new Set();
723
+ const slots = /* @__PURE__ */ new Set();
724
+ const hasSlot = typeof HTMLSlotElement !== "undefined";
725
+ const onSlotChange = () => resync();
726
+ const trackSlots = () => {
727
+ if (!scroller || !hasSlot) return;
728
+ for (const child of Array.from(scroller.children)) {
729
+ if (child instanceof HTMLSlotElement && !slots.has(child)) {
730
+ child.addEventListener("slotchange", onSlotChange);
731
+ slots.add(child);
732
+ }
733
+ }
734
+ };
735
+ const resync = () => {
736
+ trackSlots();
737
+ const targets = contentTargets(scroller);
738
+ for (const el of observed) {
739
+ if (!targets.includes(el)) {
740
+ ro.unobserve(el);
741
+ observed.delete(el);
742
+ }
743
+ }
744
+ for (const el of targets) {
745
+ if (!observed.has(el)) {
746
+ ro.observe(el);
747
+ observed.add(el);
748
+ }
749
+ }
750
+ schedule();
751
+ };
752
+ let mo;
753
+ if (scroller) {
754
+ resync();
755
+ if (typeof MutationObserver !== "undefined") {
756
+ mo = new MutationObserver(resync);
757
+ mo.observe(scroller, { childList: true });
758
+ }
759
+ }
760
+ return () => {
761
+ ro.disconnect();
762
+ mo?.disconnect();
763
+ for (const slot of slots) {
764
+ slot.removeEventListener("slotchange", onSlotChange);
765
+ }
766
+ if (raf) cancelAnimationFrame(raf);
767
+ };
768
+ }
769
+
770
+ // src/core/features/fit-measurement.ts
771
+ var measureSheetNatural = (element, content, vertical) => {
772
+ if (!content) {
773
+ return vertical ? element.offsetHeight : element.offsetWidth;
774
+ }
775
+ const axis = vertical ? "height" : "width";
776
+ const sheetStyle = element.style;
777
+ const cs = content.style;
778
+ const prevSheet = sheetStyle[axis];
779
+ const prevFlex = cs.flex;
780
+ const prevContent = vertical ? cs.height : cs.width;
781
+ sheetStyle[axis] = "auto";
782
+ cs.flex = "none";
783
+ if (vertical) cs.height = "auto";
784
+ else cs.width = "auto";
785
+ const poked = vertical ? element.offsetHeight : element.offsetWidth;
786
+ const contentBox = vertical ? content.clientHeight : content.clientWidth;
787
+ const contentScroll = vertical ? content.scrollHeight : content.scrollWidth;
788
+ const natural = Math.max(poked, poked - contentBox + contentScroll);
789
+ sheetStyle[axis] = prevSheet;
790
+ cs.flex = prevFlex;
791
+ if (vertical) cs.height = prevContent;
792
+ else cs.width = prevContent;
793
+ return natural;
794
+ };
795
+ var containingExtent = (element, vertical) => {
796
+ const op = element.offsetParent;
797
+ if (op && op !== document.body && op !== document.documentElement) {
798
+ return vertical ? op.clientHeight : op.clientWidth;
799
+ }
800
+ return vertical ? window.innerHeight : window.innerWidth;
801
+ };
802
+ var measureFitSize = (deps) => {
803
+ const vertical = layoutAxis(deps.mode) === "height";
804
+ const natural = measureSheetNatural(
805
+ deps.element,
806
+ deps.scrollContainer,
807
+ vertical
808
+ );
809
+ const cap = deps.getMaxHeightCap();
810
+ const capped = cap !== void 0 ? Math.min(natural, cap) : natural;
811
+ if (typeof window === "undefined") return capped;
812
+ const viewport = containingExtent(deps.element, vertical);
813
+ return viewport > 0 ? Math.min(capped, viewport) : capped;
814
+ };
815
+
668
816
  // src/core/features/slider-keyboard.ts
669
817
  function installSliderKeyboard(deps) {
670
818
  const isVerticalAxis = deps.mode === "bottom" || deps.mode === "top";
@@ -1151,6 +1299,9 @@ var ScrimController = class {
1151
1299
  const s = this.screenComponent.style;
1152
1300
  s.opacity = "";
1153
1301
  s.display = "";
1302
+ s.position = "";
1303
+ s.inset = "";
1304
+ s.pointerEvents = "";
1154
1305
  }
1155
1306
  if (this.backdrop) {
1156
1307
  this.backdrop.style.opacity = "";
@@ -1399,7 +1550,12 @@ var AnimationRunner = class {
1399
1550
  this.currentSpring?.cancel();
1400
1551
  }
1401
1552
  async animateTo(target, velocityPxPerMs) {
1402
- if (target === this.getSize()) return;
1553
+ if (target === this.getSize()) {
1554
+ if (!this.isAnimating && !this.isDragging()) {
1555
+ this.element.style.willChange = "";
1556
+ }
1557
+ return;
1558
+ }
1403
1559
  this.currentTween?.cancel();
1404
1560
  this.currentSpring?.cancel();
1405
1561
  this.getRootEl()?.setAttribute("data-animating", "true");
@@ -1407,7 +1563,7 @@ var AnimationRunner = class {
1407
1563
  if (this.reducedMotion) {
1408
1564
  this.applySizeFn(target);
1409
1565
  this.getRootEl()?.removeAttribute("data-animating");
1410
- if (!this.isDragging()) this.element.style.willChange = "auto";
1566
+ if (!this.isDragging()) this.element.style.willChange = "";
1411
1567
  return;
1412
1568
  }
1413
1569
  if (this.animationKind === "spring") {
@@ -1435,7 +1591,7 @@ var AnimationRunner = class {
1435
1591
  }
1436
1592
  if (!this.isAnimating) {
1437
1593
  this.getRootEl()?.removeAttribute("data-animating");
1438
- if (!this.isDragging()) this.element.style.willChange = "auto";
1594
+ if (!this.isDragging()) this.element.style.willChange = "";
1439
1595
  }
1440
1596
  }
1441
1597
  destroy() {
@@ -1466,9 +1622,35 @@ var isVisible = (el) => {
1466
1622
  }
1467
1623
  return el.offsetParent !== null;
1468
1624
  };
1469
- var focusables = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter(
1625
+ var collectFocusables = (root) => {
1626
+ const out = Array.from(root.querySelectorAll(FOCUSABLE));
1627
+ if (typeof HTMLSlotElement !== "undefined") {
1628
+ for (const slot of Array.from(root.querySelectorAll("slot"))) {
1629
+ for (const assigned of slot.assignedElements()) {
1630
+ if (assigned.matches(FOCUSABLE)) out.push(assigned);
1631
+ out.push(...collectFocusables(assigned));
1632
+ }
1633
+ }
1634
+ }
1635
+ return out;
1636
+ };
1637
+ var focusables = (root) => collectFocusables(root).filter(
1470
1638
  (el) => !el.hasAttribute("inert") && isVisible(el) && !el.matches("[aria-hidden='true']")
1471
1639
  );
1640
+ var containsComposed = (container, node) => {
1641
+ let n = node;
1642
+ while (n) {
1643
+ if (n === container) return true;
1644
+ const slot = n.assignedSlot;
1645
+ if (slot) {
1646
+ n = slot;
1647
+ continue;
1648
+ }
1649
+ const parent = n.parentNode;
1650
+ n = parent instanceof ShadowRoot ? parent.host : parent;
1651
+ }
1652
+ return false;
1653
+ };
1472
1654
  var trapStack = [];
1473
1655
  var installFocusTrap = (container, options = {}) => {
1474
1656
  if (typeof document === "undefined") return () => {
@@ -1478,7 +1660,13 @@ var installFocusTrap = (container, options = {}) => {
1478
1660
  const isActive = () => trapStack[trapStack.length - 1] === token;
1479
1661
  const previouslyFocused = document.activeElement;
1480
1662
  const focusInitial = () => {
1481
- const initial = typeof options.initialFocus === "string" ? container.querySelector(options.initialFocus) : options.initialFocus ?? focusables(container)[0] ?? container;
1663
+ const spec = options.initialFocus;
1664
+ if (spec === false) {
1665
+ if (!container.hasAttribute("tabindex")) container.tabIndex = -1;
1666
+ container.focus({ preventScroll: true });
1667
+ return;
1668
+ }
1669
+ const initial = typeof spec === "string" ? container.querySelector(spec) : spec ?? focusables(container)[0] ?? container;
1482
1670
  initial?.focus({ preventScroll: true });
1483
1671
  };
1484
1672
  focusInitial();
@@ -1508,7 +1696,7 @@ var installFocusTrap = (container, options = {}) => {
1508
1696
  const handleFocusIn = (e) => {
1509
1697
  if (!isActive()) return;
1510
1698
  const target = e.target;
1511
- if (target && !container.contains(target)) {
1699
+ if (target && !containsComposed(container, target)) {
1512
1700
  const list = focusables(container);
1513
1701
  list[0]?.focus({ preventScroll: true });
1514
1702
  }
@@ -1545,8 +1733,14 @@ var lockBodyScroll = () => {
1545
1733
  position: body.style.position,
1546
1734
  top: body.style.top,
1547
1735
  width: body.style.width,
1736
+ paddingRight: body.style.paddingRight,
1548
1737
  scrollY: window.scrollY
1549
1738
  };
1739
+ const scrollbarGap = window.innerWidth - document.documentElement.clientWidth;
1740
+ if (scrollbarGap > 0) {
1741
+ const current = parseFloat(getComputedStyle(body).paddingRight) || 0;
1742
+ body.style.paddingRight = `${current + scrollbarGap}px`;
1743
+ }
1550
1744
  body.style.overflow = "hidden";
1551
1745
  body.style.position = "fixed";
1552
1746
  body.style.top = `-${savedStyles.scrollY}px`;
@@ -1564,6 +1758,7 @@ var lockBodyScroll = () => {
1564
1758
  body.style.position = savedStyles.position;
1565
1759
  body.style.top = savedStyles.top;
1566
1760
  body.style.width = savedStyles.width;
1761
+ body.style.paddingRight = savedStyles.paddingRight;
1567
1762
  window.scrollTo(0, savedStyles.scrollY);
1568
1763
  savedStyles = null;
1569
1764
  }
@@ -1776,12 +1971,14 @@ var installGestures = (handle, mode, callbacks) => {
1776
1971
  handle.addEventListener("pointermove", onPointerMove);
1777
1972
  handle.addEventListener("pointerup", finishGesture);
1778
1973
  handle.addEventListener("pointercancel", onPointerCancel);
1974
+ const prevTouchAction = handle.style.touchAction;
1779
1975
  handle.style.touchAction = isAxisVertical(mode) ? "pan-x" : "pan-y";
1780
1976
  return () => {
1781
1977
  handle.removeEventListener("pointerdown", onPointerDown);
1782
1978
  handle.removeEventListener("pointermove", onPointerMove);
1783
1979
  handle.removeEventListener("pointerup", finishGesture);
1784
1980
  handle.removeEventListener("pointercancel", onPointerCancel);
1981
+ handle.style.touchAction = prevTouchAction;
1785
1982
  if (activePointerId !== null && handle.hasPointerCapture(activePointerId)) {
1786
1983
  handle.releasePointerCapture(activePointerId);
1787
1984
  }
@@ -1908,7 +2105,7 @@ var GestureController = class {
1908
2105
  this.isDragging_ = false;
1909
2106
  this.keyboardDismissed = false;
1910
2107
  this.deps.getRoot()?.removeAttribute("data-dragging");
1911
- this.deps.element.style.willChange = "auto";
2108
+ this.deps.element.style.willChange = "";
1912
2109
  }
1913
2110
  };
1914
2111
 
@@ -2307,8 +2504,7 @@ function resolveEngineOptions(opts, extras) {
2307
2504
  };
2308
2505
  }
2309
2506
 
2310
- // src/core/BottomSheetEngine.ts
2311
- var HAPTIC_DURATION_MS = 8;
2507
+ // src/core/primitives/teardown-stack.ts
2312
2508
  var TeardownStack = class {
2313
2509
  constructor() {
2314
2510
  __publicField(this, "fns", []);
@@ -2328,6 +2524,9 @@ var TeardownStack = class {
2328
2524
  }
2329
2525
  }
2330
2526
  };
2527
+
2528
+ // src/core/BottomSheetEngine.ts
2529
+ var HAPTIC_DURATION_MS = 8;
2331
2530
  var BottomSheetEngine = class {
2332
2531
  constructor(opts) {
2333
2532
  __publicField(this, "id", nextInstanceId("bs"));
@@ -2352,6 +2551,7 @@ var BottomSheetEngine = class {
2352
2551
  __publicField(this, "disableDragFlag");
2353
2552
  __publicField(this, "closeOnRouteChange");
2354
2553
  __publicField(this, "maxHeightCap");
2554
+ __publicField(this, "maxHeightRaw");
2355
2555
  __publicField(this, "snapPointsRaw");
2356
2556
  __publicField(this, "snaps");
2357
2557
  __publicField(this, "activeId");
@@ -2387,6 +2587,7 @@ var BottomSheetEngine = class {
2387
2587
  __publicField(this, "detachScrimStages", null);
2388
2588
  __publicField(this, "stackEffectEnabled", false);
2389
2589
  __publicField(this, "stackEffectPrimed", false);
2590
+ __publicField(this, "fitDeps");
2390
2591
  __publicField(this, "transformTemplate");
2391
2592
  const resolved = resolveEngineOptions(opts, {
2392
2593
  shouldApplyInertSiblings: () => {
@@ -2459,12 +2660,19 @@ var BottomSheetEngine = class {
2459
2660
  }
2460
2661
  this.activeId = resolved.initialId;
2461
2662
  auditVhUsage(this.snapPointsRaw);
2663
+ this.auditDuplicateSnapIds(this.snapPointsRaw);
2462
2664
  this.rootEl = this.element.closest(".bs-root");
2665
+ this.fitDeps = {
2666
+ element: this.element,
2667
+ scrollContainer: this.scrollContainer,
2668
+ mode: this.mode,
2669
+ getMaxHeightCap: () => this.maxHeightCap
2670
+ };
2463
2671
  this.snaps = new SnapResolver(
2464
2672
  this.snapPointsRaw,
2465
2673
  resolved.initialAllowed,
2466
2674
  this.mode,
2467
- () => this.measureFitSize(),
2675
+ () => measureFitSize(this.fitDeps),
2468
2676
  (maxAxisSize) => {
2469
2677
  this.element.style[layoutAxis(this.mode)] = `${maxAxisSize}px`;
2470
2678
  }
@@ -2498,7 +2706,18 @@ var BottomSheetEngine = class {
2498
2706
  io.observe(this.element);
2499
2707
  this.teardowns.add(() => io.disconnect());
2500
2708
  }
2501
- this.installFitObserver();
2709
+ this.teardowns.add(
2710
+ installFitObserver({
2711
+ handle: this.handle,
2712
+ scrollContainer: this.scrollContainer,
2713
+ hasFitSnap: () => this.snapPointsRaw.some(
2714
+ (p) => p.size === "fit" || p.size === "content"
2715
+ ),
2716
+ isDestroyed: () => this.destroyed,
2717
+ isDragging: () => this.gesture?.isDragging ?? false,
2718
+ recompute: () => this.recompute()
2719
+ })
2720
+ );
2502
2721
  if (resolved.radius !== void 0) this.setRadius(resolved.radius);
2503
2722
  if (resolved.maxHeight !== void 0) this.setMaxHeight(resolved.maxHeight);
2504
2723
  this.applyAutoAriaLabelledBy();
@@ -2649,27 +2868,33 @@ var BottomSheetEngine = class {
2649
2868
  } else {
2650
2869
  await this.animation.animateTo(target.size, velocityPxPerMs);
2651
2870
  }
2652
- this.opening = false;
2653
2871
  externalSignal?.removeEventListener("abort", onExternalAbort);
2654
2872
  if (signal.aborted) return;
2655
- this.scrollCache.restore(id, previousSize, target.size);
2873
+ this.opening = false;
2874
+ this.completeSnap(id, target.size, previousSize, wasClosed, false);
2875
+ }
2876
+ completeSnap(id, targetSize, previousSize, wasClosed, withHaptic) {
2877
+ this.scrollCache.restore(id, previousSize, targetSize);
2656
2878
  this.updateAriaSlider();
2657
2879
  this.emit("snap", {
2658
2880
  id,
2659
2881
  size: this.size,
2660
2882
  progress: this.computeProgress(this.size)
2661
2883
  });
2662
- if (wasClosed && target.size > 0) {
2663
- this.emit("open", { id });
2664
- this.handleOpen();
2665
- notifyLinkedSheets(this.linkedSheets, this);
2666
- this.emit("opened", { id });
2667
- }
2668
- if (target.size === 0) {
2669
- this.emit("close", void 0);
2670
- this.handleClose();
2671
- this.emit("closed", void 0);
2672
- }
2884
+ if (withHaptic) this.haptic();
2885
+ if (wasClosed && targetSize > 0) this.emitOpenSequence(id);
2886
+ if (targetSize === 0) this.emitCloseSequence();
2887
+ }
2888
+ emitOpenSequence(id) {
2889
+ this.emit("open", { id });
2890
+ this.handleOpen();
2891
+ notifyLinkedSheets(this.linkedSheets, this);
2892
+ this.emit("opened", { id });
2893
+ }
2894
+ emitCloseSequence() {
2895
+ this.emit("close", void 0);
2896
+ this.handleClose();
2897
+ this.emit("closed", void 0);
2673
2898
  }
2674
2899
  async dragTo(targetSize, velocityOrOpts = 0) {
2675
2900
  if (this.destroyed) return;
@@ -2690,19 +2915,31 @@ var BottomSheetEngine = class {
2690
2915
  }
2691
2916
  open(id) {
2692
2917
  if (this.destroyed) return Promise.resolve();
2693
- const target = id ?? this.snaps.getAllowedIds().find((a) => a !== "closed") ?? this.activeId;
2918
+ const target = id ?? this.snaps.getAllowedIds().find((a) => (this.snaps.findById(a)?.size ?? 0) > 0) ?? this.activeId;
2694
2919
  return this.snapTo(target);
2695
2920
  }
2696
2921
  close(reason = "programmatic") {
2697
2922
  if (this.destroyed) return Promise.resolve();
2698
2923
  if (this.disableCloseFlag) return Promise.resolve();
2699
2924
  if (this.size > 0 && this.emitBeforeClose(reason)) return Promise.resolve();
2700
- const closedId = this.snapPointsRaw.find((p) => p.id === "closed")?.id ?? this.snaps.getAllowedIds()[0];
2925
+ const closedId = this.snapPointsRaw.find((p) => p.id === "closed")?.id ?? this.snaps.getAllowedIds().find((a) => this.snaps.findById(a)?.size === 0) ?? this.snaps.getAllowedIds()[0];
2701
2926
  return this.snapTo(closedId ?? this.activeId);
2702
2927
  }
2703
2928
  canDismiss() {
2704
2929
  return !this.persistent && !this.disableCloseFlag && !this.destroyed;
2705
2930
  }
2931
+ setPersistent(value) {
2932
+ if (this.destroyed) return;
2933
+ this.persistent = value;
2934
+ }
2935
+ setDisableClose(value) {
2936
+ if (this.destroyed) return;
2937
+ this.disableCloseFlag = value;
2938
+ }
2939
+ setDisableDrag(value) {
2940
+ if (this.destroyed) return;
2941
+ this.disableDragFlag = value;
2942
+ }
2706
2943
  isTop() {
2707
2944
  return this.isTopSheet;
2708
2945
  }
@@ -2739,18 +2976,23 @@ var BottomSheetEngine = class {
2739
2976
  axis === "height" ? "max-height" : "max-width",
2740
2977
  value
2741
2978
  );
2742
- if (typeof h === "number") {
2743
- this.maxHeightCap = h;
2744
- this.recompute();
2745
- } else if (typeof window !== "undefined") {
2746
- const probe2 = document.createElement("div");
2747
- probe2.style.cssText = `position:absolute;visibility:hidden;pointer-events:none;${axis === "height" ? "height" : "width"}:${value};`;
2748
- document.body.appendChild(probe2);
2749
- const measured = axis === "height" ? probe2.offsetHeight : probe2.offsetWidth;
2750
- probe2.remove();
2751
- this.maxHeightCap = measured > 0 ? measured : void 0;
2752
- this.recompute();
2979
+ this.maxHeightRaw = h;
2980
+ this.resolveMaxHeightCap();
2981
+ this.recompute();
2982
+ }
2983
+ resolveMaxHeightCap() {
2984
+ const raw = this.maxHeightRaw;
2985
+ if (raw === void 0) {
2986
+ this.maxHeightCap = void 0;
2987
+ return;
2753
2988
  }
2989
+ if (typeof raw === "number") {
2990
+ this.maxHeightCap = raw;
2991
+ return;
2992
+ }
2993
+ if (typeof window === "undefined") return;
2994
+ const measured = resolveSnap(raw, this.mode);
2995
+ this.maxHeightCap = measured > 0 ? measured : void 0;
2754
2996
  }
2755
2997
  clampToMaxHeight() {
2756
2998
  if (this.maxHeightCap === void 0) return;
@@ -2881,6 +3123,7 @@ var BottomSheetEngine = class {
2881
3123
  this.newCycle();
2882
3124
  this.snapPointsRaw = points;
2883
3125
  auditVhUsage(points);
3126
+ this.auditDuplicateSnapIds(points);
2884
3127
  this.snaps.setRaw(points);
2885
3128
  if (allowed) this.snaps.setAllowedIds(allowed);
2886
3129
  this.scrollCache.clear();
@@ -2893,6 +3136,7 @@ var BottomSheetEngine = class {
2893
3136
  }
2894
3137
  recompute() {
2895
3138
  if (this.destroyed) return;
3139
+ this.resolveMaxHeightCap();
2896
3140
  this.snaps.recompute();
2897
3141
  this.clampToMaxHeight();
2898
3142
  this.scrim.invalidateOpacityCache();
@@ -2904,129 +3148,6 @@ var BottomSheetEngine = class {
2904
3148
  this.updateAriaSlider();
2905
3149
  }
2906
3150
  }
2907
- contentTargets() {
2908
- const scroller = this.scrollContainer;
2909
- if (!scroller) return [];
2910
- const hasSlot = typeof HTMLSlotElement !== "undefined";
2911
- const out = [];
2912
- for (const child of Array.from(scroller.children)) {
2913
- if (hasSlot && child instanceof HTMLSlotElement) {
2914
- for (const el of child.assignedElements()) out.push(el);
2915
- } else {
2916
- out.push(child);
2917
- }
2918
- }
2919
- return out;
2920
- }
2921
- measureSheetNatural(vertical) {
2922
- const el = this.element;
2923
- const content = this.scrollContainer;
2924
- if (!content) {
2925
- return vertical ? el.offsetHeight : el.offsetWidth;
2926
- }
2927
- const axis = vertical ? "height" : "width";
2928
- const sheetStyle = el.style;
2929
- const cs = content.style;
2930
- const prevSheet = sheetStyle[axis];
2931
- const prevFlex = cs.flex;
2932
- const prevContent = vertical ? cs.height : cs.width;
2933
- sheetStyle[axis] = "auto";
2934
- cs.flex = "none";
2935
- if (vertical) cs.height = "auto";
2936
- else cs.width = "auto";
2937
- const poked = vertical ? el.offsetHeight : el.offsetWidth;
2938
- const contentBox = vertical ? content.clientHeight : content.clientWidth;
2939
- const contentScroll = vertical ? content.scrollHeight : content.scrollWidth;
2940
- const natural = Math.max(poked, poked - contentBox + contentScroll);
2941
- sheetStyle[axis] = prevSheet;
2942
- cs.flex = prevFlex;
2943
- if (vertical) cs.height = prevContent;
2944
- else cs.width = prevContent;
2945
- return natural;
2946
- }
2947
- measureFitSize() {
2948
- const vertical = layoutAxis(this.mode) === "height";
2949
- const natural = this.measureSheetNatural(vertical);
2950
- const capped = this.maxHeightCap !== void 0 ? Math.min(natural, this.maxHeightCap) : natural;
2951
- if (typeof window === "undefined") return capped;
2952
- const viewport = this.containingExtent(vertical);
2953
- return viewport > 0 ? Math.min(capped, viewport) : capped;
2954
- }
2955
- // The fit cap is the sheet's containing block, not always the window: a
2956
- // position:fixed sheet nested under a transform/filter/will-change ancestor
2957
- // is clipped to that ancestor, so capping by window.innerHeight oversizes the
2958
- // sheet and the content/footer get clipped. offsetParent resolves to that
2959
- // ancestor when present, and is null (→ viewport) for a plain fixed sheet.
2960
- containingExtent(vertical) {
2961
- const op = this.element.offsetParent;
2962
- if (op && op !== document.body && op !== document.documentElement) {
2963
- return vertical ? op.clientHeight : op.clientWidth;
2964
- }
2965
- return vertical ? window.innerHeight : window.innerWidth;
2966
- }
2967
- installFitObserver() {
2968
- if (typeof ResizeObserver === "undefined") return;
2969
- if (!this.snapPointsRaw.some((p) => p.size === "fit" || p.size === "content"))
2970
- return;
2971
- let raf = 0;
2972
- const schedule = () => {
2973
- if (raf) return;
2974
- raf = requestAnimationFrame(() => {
2975
- raf = 0;
2976
- if (this.destroyed || this.gesture?.isDragging) return;
2977
- this.recompute();
2978
- });
2979
- };
2980
- const ro = new ResizeObserver(schedule);
2981
- ro.observe(this.handle);
2982
- const scroller = this.scrollContainer;
2983
- const observed = /* @__PURE__ */ new Set();
2984
- const slots = /* @__PURE__ */ new Set();
2985
- const hasSlot = typeof HTMLSlotElement !== "undefined";
2986
- const onSlotChange = () => resync();
2987
- const trackSlots = () => {
2988
- if (!scroller || !hasSlot) return;
2989
- for (const child of Array.from(scroller.children)) {
2990
- if (child instanceof HTMLSlotElement && !slots.has(child)) {
2991
- child.addEventListener("slotchange", onSlotChange);
2992
- slots.add(child);
2993
- }
2994
- }
2995
- };
2996
- const resync = () => {
2997
- trackSlots();
2998
- const targets = this.contentTargets();
2999
- for (const el of observed) {
3000
- if (!targets.includes(el)) {
3001
- ro.unobserve(el);
3002
- observed.delete(el);
3003
- }
3004
- }
3005
- for (const el of targets) {
3006
- if (!observed.has(el)) {
3007
- ro.observe(el);
3008
- observed.add(el);
3009
- }
3010
- }
3011
- schedule();
3012
- };
3013
- let mo;
3014
- if (scroller) {
3015
- resync();
3016
- if (typeof MutationObserver !== "undefined") {
3017
- mo = new MutationObserver(resync);
3018
- mo.observe(scroller, { childList: true });
3019
- }
3020
- }
3021
- this.teardowns.add(() => {
3022
- ro.disconnect();
3023
- mo?.disconnect();
3024
- for (const slot of slots) {
3025
- slot.removeEventListener("slotchange", onSlotChange);
3026
- }
3027
- if (raf) cancelAnimationFrame(raf);
3028
- });
3029
- }
3030
3151
  destroy() {
3031
3152
  this.destroyed = true;
3032
3153
  this.opening = false;
@@ -3044,9 +3165,10 @@ var BottomSheetEngine = class {
3044
3165
  this.backdrop.style.opacity = "";
3045
3166
  this.backdrop.style.pointerEvents = "";
3046
3167
  }
3047
- if (this.anchorHost) {
3048
- this.anchorHost.style.removeProperty("--bs-size");
3049
- this.anchorHost.style.removeProperty("--bs-progress");
3168
+ for (const host of [this.anchorHost, this.scrimParent, this.rootEl]) {
3169
+ if (!host) continue;
3170
+ host.style.removeProperty("--bs-size");
3171
+ host.style.removeProperty("--bs-progress");
3050
3172
  }
3051
3173
  if (this.screenComponent) {
3052
3174
  this.screenComponent.style.opacity = "";
@@ -3060,6 +3182,7 @@ var BottomSheetEngine = class {
3060
3182
  }
3061
3183
  newCycle() {
3062
3184
  this.allowOvershoot = false;
3185
+ this.opening = false;
3063
3186
  this.currentAbort.abort();
3064
3187
  this.currentAbort = new AbortController();
3065
3188
  return this.currentAbort.signal;
@@ -3115,8 +3238,14 @@ var BottomSheetEngine = class {
3115
3238
  this.element.setAttribute("aria-labelledby", titled.id);
3116
3239
  }
3117
3240
  recomputeSnaps() {
3241
+ this.resolveMaxHeightCap();
3242
+ this.snaps.setRaw(this.snapPointsRaw);
3243
+ this.clampToMaxHeight();
3244
+ this.scrim.invalidateOpacityCache();
3245
+ }
3246
+ auditDuplicateSnapIds(points) {
3118
3247
  const seen = /* @__PURE__ */ new Set();
3119
- for (const p of this.snapPointsRaw) {
3248
+ for (const p of points) {
3120
3249
  if (seen.has(p.id)) {
3121
3250
  console.warn(
3122
3251
  `[BottomSheet] duplicate snap id "${p.id}" \u2014 only the first occurrence is reachable.`
@@ -3124,9 +3253,6 @@ var BottomSheetEngine = class {
3124
3253
  }
3125
3254
  seen.add(p.id);
3126
3255
  }
3127
- this.snaps.setRaw(this.snapPointsRaw);
3128
- this.clampToMaxHeight();
3129
- this.scrim.invalidateOpacityCache();
3130
3256
  }
3131
3257
  getAllowedRange() {
3132
3258
  return this.snaps.getAllowedRange();
@@ -3314,9 +3440,15 @@ var BottomSheetEngine = class {
3314
3440
  flickVelocity: this.flickVelocity,
3315
3441
  dragThreshold: this.dragThreshold
3316
3442
  });
3317
- if (!target) return;
3443
+ if (!target) {
3444
+ this.clearWillChangeIfIdle();
3445
+ return;
3446
+ }
3318
3447
  const previousId = this.activeId;
3319
- if (target.id === previousId && target.size === this.size) return;
3448
+ if (target.id === previousId && target.size === this.size) {
3449
+ this.clearWillChangeIfIdle();
3450
+ return;
3451
+ }
3320
3452
  if (this.emitBeforeSnap(target, previousId)) {
3321
3453
  const restore = this.snaps.findById(previousId);
3322
3454
  if (restore) {
@@ -3339,27 +3471,19 @@ var BottomSheetEngine = class {
3339
3471
  if (signal.aborted) return;
3340
3472
  this.allowOvershoot = false;
3341
3473
  this.opening = false;
3342
- this.scrollCache.restore(target.id, previousSize, target.size);
3343
- this.updateAriaSlider();
3344
- this.emit("snap", {
3345
- id: target.id,
3346
- size: this.size,
3347
- progress: this.computeProgress(this.size)
3348
- });
3349
- this.haptic();
3350
- if (previousSnapSize === 0 && target.size > 0) {
3351
- this.emit("open", { id: target.id });
3352
- this.handleOpen();
3353
- notifyLinkedSheets(this.linkedSheets, this);
3354
- this.emit("opened", { id: target.id });
3355
- }
3356
- if (target.size === 0) {
3357
- this.emit("close", void 0);
3358
- this.handleClose();
3359
- this.emit("closed", void 0);
3360
- }
3474
+ this.completeSnap(
3475
+ target.id,
3476
+ target.size,
3477
+ previousSize,
3478
+ previousSnapSize === 0,
3479
+ true
3480
+ );
3361
3481
  });
3362
3482
  }
3483
+ clearWillChangeIfIdle() {
3484
+ if (this.animation.isAnimating || this.gesture?.isDragging) return;
3485
+ this.element.style.willChange = "";
3486
+ }
3363
3487
  haptic() {
3364
3488
  if (typeof navigator !== "undefined" && "vibrate" in navigator) {
3365
3489
  try {
@@ -3440,14 +3564,9 @@ var BottomSheetEngine = class {
3440
3564
  progress: this.computeProgress(this.size)
3441
3565
  });
3442
3566
  if (target.size > 0 && !this.lifecycle.isInstalled) {
3443
- this.emit("open", { id: target.id });
3444
- this.handleOpen();
3445
- notifyLinkedSheets(this.linkedSheets, this);
3446
- this.emit("opened", { id: target.id });
3567
+ this.emitOpenSequence(target.id);
3447
3568
  } else if (target.size === 0 && this.lifecycle.isInstalled) {
3448
- this.emit("close", void 0);
3449
- this.handleClose();
3450
- this.emit("closed", void 0);
3569
+ this.emitCloseSequence();
3451
3570
  }
3452
3571
  }
3453
3572
  handleClose() {
@@ -3544,6 +3663,9 @@ function useBottomSheet(opts) {
3544
3663
  refresh();
3545
3664
  optsRef.current.onSnap?.(payload.id);
3546
3665
  }),
3666
+ engine.on("before-snap", (payload) => {
3667
+ optsRef.current.onBeforeSnap?.(payload);
3668
+ }),
3547
3669
  engine.on("before-close", (payload) => {
3548
3670
  optsRef.current.onBeforeClose?.(payload);
3549
3671
  }),
@@ -3574,12 +3696,26 @@ function useBottomSheet(opts) {
3574
3696
  const allowedKey = (opts.allowed ?? opts.snapPoints.map((p) => p.id)).join(
3575
3697
  "\0"
3576
3698
  );
3577
- const snapKey = opts.snapPoints.map((p) => p.id).join("\0");
3699
+ const defKey = opts.snapPoints.map((p) => `${p.id}${String(p.size)}`).join("\0");
3700
+ const defApplied = useRef(false);
3701
+ useEffect(() => {
3702
+ if (!defApplied.current) {
3703
+ defApplied.current = true;
3704
+ return;
3705
+ }
3706
+ const engine = engineRef.current;
3707
+ if (!engine) return;
3708
+ const current = optsRef.current;
3709
+ engine.setSnapPoints(
3710
+ current.snapPoints,
3711
+ current.allowed ? Array.from(current.allowed) : void 0
3712
+ );
3713
+ }, [defKey]);
3578
3714
  useEffect(() => {
3579
3715
  if (!engineRef.current) return;
3580
3716
  const ids = opts.allowed ? Array.from(opts.allowed) : opts.snapPoints.map((p) => p.id);
3581
3717
  engineRef.current.setAllowed(ids);
3582
- }, [allowedKey, snapKey]);
3718
+ }, [allowedKey]);
3583
3719
  useEffect(() => {
3584
3720
  if (!engineRef.current) return;
3585
3721
  if (opts.backdropColor !== void 0) {
@@ -3604,6 +3740,21 @@ function useBottomSheet(opts) {
3604
3740
  engineRef.current.setMaxHeight(opts.maxHeight);
3605
3741
  }
3606
3742
  }, [opts.maxHeight]);
3743
+ useEffect(() => {
3744
+ if (opts.persistent !== void 0) {
3745
+ engineRef.current?.setPersistent(opts.persistent);
3746
+ }
3747
+ }, [opts.persistent]);
3748
+ useEffect(() => {
3749
+ if (opts.disableClose !== void 0) {
3750
+ engineRef.current?.setDisableClose(opts.disableClose);
3751
+ }
3752
+ }, [opts.disableClose]);
3753
+ useEffect(() => {
3754
+ if (opts.disableDrag !== void 0) {
3755
+ engineRef.current?.setDisableDrag(opts.disableDrag);
3756
+ }
3757
+ }, [opts.disableDrag]);
3607
3758
  const subscribe = useCallback((fn) => {
3608
3759
  subscribersRef.current.add(fn);
3609
3760
  return () => subscribersRef.current.delete(fn);
@@ -3939,6 +4090,95 @@ var BottomSheet = forwardRef(
3939
4090
  }
3940
4091
  );
3941
4092
 
4093
+ // src/core/primitives/overlay-transforms.ts
4094
+ var closedTransform = (edge) => CLOSED_TRANSFORM[edge];
4095
+ var closedTransformFor = (edge, animation, peekPx) => {
4096
+ switch (animation) {
4097
+ case "fade":
4098
+ return "translate3d(0, 0, 0)";
4099
+ case "scale":
4100
+ return "translate3d(0, 0, 0) scale(0.94)";
4101
+ case "slide":
4102
+ default:
4103
+ if (peekPx !== void 0 && edge === "bottom") {
4104
+ return `translate3d(0, calc(100% - ${peekPx}px), 0)`;
4105
+ }
4106
+ return closedTransform(edge);
4107
+ }
4108
+ };
4109
+ var openTransformFor = (animation) => animation === "scale" ? "translate3d(0, 0, 0) scale(1)" : "translate3d(0, 0, 0)";
4110
+
4111
+ // src/core/features/overlay-swipe.ts
4112
+ function installOverlaySwipe(deps) {
4113
+ const el = deps.element;
4114
+ const edge = deps.edge;
4115
+ const isVertical = edge === "bottom" || edge === "top";
4116
+ const sign = edge === "bottom" || edge === "right" ? 1 : -1;
4117
+ const SLOP = 8;
4118
+ let startX = 0;
4119
+ let startY = 0;
4120
+ let startT = 0;
4121
+ let tracking = false;
4122
+ let dragging = false;
4123
+ let pointerId = null;
4124
+ const previousTouchAction = el.style.touchAction;
4125
+ el.style.touchAction = isVertical ? "pan-x" : "pan-y";
4126
+ const onDown = (e) => {
4127
+ if (e.button !== void 0 && e.button !== 0) return;
4128
+ pointerId = e.pointerId;
4129
+ startX = e.clientX;
4130
+ startY = e.clientY;
4131
+ startT = performance.now();
4132
+ tracking = true;
4133
+ dragging = false;
4134
+ };
4135
+ const onMove = (e) => {
4136
+ if (!tracking || e.pointerId !== pointerId) return;
4137
+ const delta = isVertical ? e.clientY - startY : e.clientX - startX;
4138
+ if (!dragging) {
4139
+ if (Math.abs(delta) < SLOP) return;
4140
+ dragging = true;
4141
+ el.style.transition = "none";
4142
+ try {
4143
+ el.setPointerCapture(e.pointerId);
4144
+ } catch {
4145
+ }
4146
+ }
4147
+ const offset = Math.max(0, sign * delta);
4148
+ el.style.transform = isVertical ? `translate3d(0, ${sign * offset}px, 0)` : `translate3d(${sign * offset}px, 0, 0)`;
4149
+ };
4150
+ const finish = (e) => {
4151
+ if (!tracking || e.pointerId !== pointerId) return;
4152
+ tracking = false;
4153
+ pointerId = null;
4154
+ if (!dragging) return;
4155
+ dragging = false;
4156
+ const dt = performance.now() - startT || 1;
4157
+ const delta = isVertical ? e.clientY - startY : e.clientX - startX;
4158
+ const offset = Math.max(0, sign * delta);
4159
+ const velocity = sign * delta / dt;
4160
+ const size = isVertical ? el.offsetHeight : el.offsetWidth;
4161
+ const past = offset > size * deps.getThreshold() || velocity > deps.getVelocityThreshold();
4162
+ el.style.transition = deps.getExitTransition();
4163
+ if (past) {
4164
+ deps.close();
4165
+ } else {
4166
+ el.style.transform = deps.getOpenTransform();
4167
+ }
4168
+ };
4169
+ el.addEventListener("pointerdown", onDown);
4170
+ el.addEventListener("pointermove", onMove);
4171
+ el.addEventListener("pointerup", finish);
4172
+ el.addEventListener("pointercancel", finish);
4173
+ return () => {
4174
+ el.style.touchAction = previousTouchAction;
4175
+ el.removeEventListener("pointerdown", onDown);
4176
+ el.removeEventListener("pointermove", onMove);
4177
+ el.removeEventListener("pointerup", finish);
4178
+ el.removeEventListener("pointercancel", finish);
4179
+ };
4180
+ }
4181
+
3942
4182
  // src/core/overlay.ts
3943
4183
  var OVERLAY_PRESETS = Object.freeze({
3944
4184
  sheet: Object.freeze({
@@ -3970,22 +4210,6 @@ var OVERLAY_PRESETS = Object.freeze({
3970
4210
  swipeToClose: false
3971
4211
  })
3972
4212
  });
3973
- var closedTransform = (edge) => CLOSED_TRANSFORM[edge];
3974
- var closedTransformFor = (edge, animation, peekPx) => {
3975
- switch (animation) {
3976
- case "fade":
3977
- return "translate3d(0, 0, 0)";
3978
- case "scale":
3979
- return "translate3d(0, 0, 0) scale(0.94)";
3980
- case "slide":
3981
- default:
3982
- if (peekPx !== void 0 && edge === "bottom") {
3983
- return `translate3d(0, calc(100% - ${peekPx}px), 0)`;
3984
- }
3985
- return closedTransform(edge);
3986
- }
3987
- };
3988
- var openTransformFor = (animation) => animation === "scale" ? "translate3d(0, 0, 0) scale(1)" : "translate3d(0, 0, 0)";
3989
4213
  var OverlayEngine = class {
3990
4214
  constructor(opts) {
3991
4215
  __publicField(this, "id", nextInstanceId("ovl"));
@@ -4118,6 +4342,7 @@ var OverlayEngine = class {
4118
4342
  if (this.backdrop) {
4119
4343
  this.backdrop.style.transition = `opacity ${dur}ms ease-out`;
4120
4344
  }
4345
+ this.setWillChange(true);
4121
4346
  const cycle = ++this.cycleNonce;
4122
4347
  return new Promise((resolve) => {
4123
4348
  requestAnimationFrame(() => {
@@ -4140,6 +4365,7 @@ var OverlayEngine = class {
4140
4365
  this.releaseInteractiveListeners();
4141
4366
  this.isOpen_ = false;
4142
4367
  this.isAnimating_ = false;
4368
+ this.setWillChange(false);
4143
4369
  this.element.style.transform = closedTransformFor(
4144
4370
  this.edge,
4145
4371
  this.enterAnimation,
@@ -4170,6 +4396,7 @@ var OverlayEngine = class {
4170
4396
  ).then(() => {
4171
4397
  if (this.destroyed || cycle !== this.cycleNonce) return resolve();
4172
4398
  this.isAnimating_ = false;
4399
+ this.setWillChange(false);
4173
4400
  this.element.setAttribute("data-state", "open");
4174
4401
  this.emit("open", void 0);
4175
4402
  this.onOpenCb?.();
@@ -4193,6 +4420,7 @@ var OverlayEngine = class {
4193
4420
  if (this.backdrop) {
4194
4421
  this.backdrop.style.transition = `opacity ${dur}ms ease-in`;
4195
4422
  }
4423
+ this.setWillChange(true);
4196
4424
  const cycle = ++this.cycleNonce;
4197
4425
  return new Promise((resolve) => {
4198
4426
  requestAnimationFrame(() => {
@@ -4218,6 +4446,7 @@ var OverlayEngine = class {
4218
4446
  ).then(() => {
4219
4447
  if (this.destroyed || cycle !== this.cycleNonce) return resolve();
4220
4448
  this.isAnimating_ = false;
4449
+ this.setWillChange(false);
4221
4450
  if (this.peekPx === void 0) {
4222
4451
  this.element.setAttribute("hidden", "");
4223
4452
  }
@@ -4240,6 +4469,7 @@ var OverlayEngine = class {
4240
4469
  this.destroyed = true;
4241
4470
  this.cycleNonce++;
4242
4471
  this.bus.clear();
4472
+ this.setWillChange(false);
4243
4473
  this.releaseInteractiveListeners();
4244
4474
  this.releaseStackEntry?.();
4245
4475
  this.releaseStackEntry = null;
@@ -4465,7 +4695,6 @@ var OverlayEngine = class {
4465
4695
  this.originalNextSibling = null;
4466
4696
  }
4467
4697
  applyClosedStyles() {
4468
- this.element.style.willChange = "transform, opacity";
4469
4698
  this.element.style.transform = closedTransformFor(
4470
4699
  this.edge,
4471
4700
  this.enterAnimation,
@@ -4479,7 +4708,12 @@ var OverlayEngine = class {
4479
4708
  if (this.backdrop) {
4480
4709
  this.backdrop.style.opacity = "0";
4481
4710
  this.backdrop.style.pointerEvents = "none";
4482
- this.backdrop.style.willChange = "opacity";
4711
+ }
4712
+ }
4713
+ setWillChange(on) {
4714
+ this.element.style.willChange = on ? "transform, opacity" : "";
4715
+ if (this.backdrop) {
4716
+ this.backdrop.style.willChange = on ? "opacity" : "";
4483
4717
  }
4484
4718
  }
4485
4719
  awaitTransition(cycle, property = "transform") {
@@ -4565,73 +4799,15 @@ var OverlayEngine = class {
4565
4799
  this.lifecycle.release();
4566
4800
  }
4567
4801
  installSwipeToClose() {
4568
- const el = this.element;
4569
- const edge = this.edge;
4570
- const isVertical = edge === "bottom" || edge === "top";
4571
- const sign = edge === "bottom" || edge === "right" ? 1 : -1;
4572
- const SLOP = 8;
4573
- let startX = 0;
4574
- let startY = 0;
4575
- let startT = 0;
4576
- let tracking = false;
4577
- let dragging = false;
4578
- let pointerId = null;
4579
- const previousTouchAction = el.style.touchAction;
4580
- el.style.touchAction = isVertical ? "pan-x" : "pan-y";
4581
- const onDown = (e) => {
4582
- if (e.button !== void 0 && e.button !== 0) return;
4583
- pointerId = e.pointerId;
4584
- startX = e.clientX;
4585
- startY = e.clientY;
4586
- startT = performance.now();
4587
- tracking = true;
4588
- dragging = false;
4589
- };
4590
- const onMove = (e) => {
4591
- if (!tracking || e.pointerId !== pointerId) return;
4592
- const delta = isVertical ? e.clientY - startY : e.clientX - startX;
4593
- if (!dragging) {
4594
- if (Math.abs(delta) < SLOP) return;
4595
- dragging = true;
4596
- el.style.transition = "none";
4597
- try {
4598
- el.setPointerCapture(e.pointerId);
4599
- } catch {
4600
- }
4601
- }
4602
- const offset = Math.max(0, sign * delta);
4603
- el.style.transform = isVertical ? `translate3d(0, ${sign * offset}px, 0)` : `translate3d(${sign * offset}px, 0, 0)`;
4604
- };
4605
- const finish = (e) => {
4606
- if (!tracking || e.pointerId !== pointerId) return;
4607
- tracking = false;
4608
- pointerId = null;
4609
- if (!dragging) return;
4610
- dragging = false;
4611
- const dt = performance.now() - startT || 1;
4612
- const delta = isVertical ? e.clientY - startY : e.clientX - startX;
4613
- const offset = Math.max(0, sign * delta);
4614
- const velocity = sign * delta / dt;
4615
- const size = isVertical ? el.offsetHeight : el.offsetWidth;
4616
- const past = offset > size * this.swipeThreshold || velocity > this.swipeVelocityThreshold;
4617
- el.style.transition = `transform ${this.effectiveDuration()}ms ${this.exitEasing}`;
4618
- if (past) {
4619
- void this.close("swipe");
4620
- } else {
4621
- el.style.transform = openTransformFor(this.enterAnimation);
4622
- }
4623
- };
4624
- el.addEventListener("pointerdown", onDown);
4625
- el.addEventListener("pointermove", onMove);
4626
- el.addEventListener("pointerup", finish);
4627
- el.addEventListener("pointercancel", finish);
4628
- return () => {
4629
- el.style.touchAction = previousTouchAction;
4630
- el.removeEventListener("pointerdown", onDown);
4631
- el.removeEventListener("pointermove", onMove);
4632
- el.removeEventListener("pointerup", finish);
4633
- el.removeEventListener("pointercancel", finish);
4634
- };
4802
+ return installOverlaySwipe({
4803
+ element: this.element,
4804
+ edge: this.edge,
4805
+ getThreshold: () => this.swipeThreshold,
4806
+ getVelocityThreshold: () => this.swipeVelocityThreshold,
4807
+ getExitTransition: () => `transform ${this.effectiveDuration()}ms ${this.exitEasing}`,
4808
+ getOpenTransform: () => openTransformFor(this.enterAnimation),
4809
+ close: () => void this.close("swipe")
4810
+ });
4635
4811
  }
4636
4812
  registerInStack() {
4637
4813
  this.releaseStackEntry = sheetStack.push({