@surdeddd/bottom-sheet 0.1.0 → 0.3.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 (57) hide show
  1. package/README.md +57 -21
  2. package/dist/BottomSheetEngine-CAymrH-P.d.cts +285 -0
  3. package/dist/BottomSheetEngine-DcqcuudN.d.ts +285 -0
  4. package/dist/element.cjs +775 -537
  5. package/dist/element.d.cts +9 -57
  6. package/dist/element.d.ts +9 -57
  7. package/dist/element.iife.global.js +2 -2
  8. package/dist/element.js +775 -537
  9. package/dist/index.cjs +760 -600
  10. package/dist/index.d.cts +18 -85
  11. package/dist/index.d.ts +18 -85
  12. package/dist/index.js +758 -601
  13. package/dist/integrations/formik.d.cts +2 -8
  14. package/dist/integrations/formik.d.ts +2 -8
  15. package/dist/integrations/react-hook-form.d.cts +2 -11
  16. package/dist/integrations/react-hook-form.d.ts +2 -11
  17. package/dist/overlay.cjs +168 -122
  18. package/dist/overlay.d.cts +14 -137
  19. package/dist/overlay.d.ts +14 -137
  20. package/dist/overlay.js +168 -122
  21. package/dist/preact.d.cts +2 -2
  22. package/dist/preact.d.ts +2 -2
  23. package/dist/preact.js +0 -1
  24. package/dist/qwik.cjs +667 -519
  25. package/dist/qwik.d.cts +3 -24
  26. package/dist/qwik.d.ts +3 -24
  27. package/dist/qwik.js +668 -520
  28. package/dist/react.cjs +873 -648
  29. package/dist/react.d.cts +34 -159
  30. package/dist/react.d.ts +34 -159
  31. package/dist/react.js +873 -648
  32. package/dist/solid.cjs +703 -560
  33. package/dist/solid.d.cts +264 -32
  34. package/dist/solid.d.ts +264 -32
  35. package/dist/solid.js +704 -561
  36. package/dist/solid.source.jsx +3215 -0
  37. package/dist/styles.css +37 -112
  38. package/dist/svelte-core.cjs +3149 -0
  39. package/dist/svelte-core.js +3147 -0
  40. package/dist/svelte-src/BottomSheet.svelte +210 -0
  41. package/dist/svelte-src/index.js +2 -0
  42. package/dist/svelte.cjs +718 -547
  43. package/dist/svelte.d.cts +100 -0
  44. package/dist/svelte.d.ts +15 -4
  45. package/dist/svelte.js +719 -548
  46. package/dist/themes/ios.css +0 -3
  47. package/dist/themes/material.css +0 -3
  48. package/dist/themes/vercel.css +0 -3
  49. package/dist/{transform-Brkf-wzo.d.cts → transform-jVfAikE9.d.cts} +0 -1
  50. package/dist/{transform-Brkf-wzo.d.ts → transform-jVfAikE9.d.ts} +0 -1
  51. package/dist/vue.cjs +677 -526
  52. package/dist/vue.d.cts +78 -0
  53. package/dist/vue.d.ts +12 -2
  54. package/dist/vue.js +678 -527
  55. package/package.json +105 -45
  56. package/dist/BottomSheetEngine-CvSVbo6c.d.cts +0 -704
  57. package/dist/BottomSheetEngine-DbO8rW_3.d.ts +0 -704
package/dist/vue.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ref, reactive, onMounted, onBeforeUnmount, openBlock, createElementBlock, createCommentVNode, renderSlot, createElementVNode, normalizeClass, toDisplayString, defineComponent, watch, computed, readonly } from 'vue';
1
+ import { ref, reactive, onMounted, onBeforeUnmount, openBlock, createElementBlock, createCommentVNode, createElementVNode, renderSlot, normalizeClass, toDisplayString, defineComponent, watch, computed, readonly } from 'vue';
2
2
 
3
3
  var __defProp = Object.defineProperty;
4
4
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -22,6 +22,18 @@ var SheetStack = class {
22
22
  this.entries.splice(idx, 1);
23
23
  this.recompute();
24
24
  }
25
+ promote(id) {
26
+ const idx = this.entries.findIndex((e) => e.id === id);
27
+ if (idx === -1) return;
28
+ if (idx !== this.entries.length - 1) {
29
+ const [entry] = this.entries.splice(idx, 1);
30
+ this.entries.push(entry);
31
+ }
32
+ this.recompute();
33
+ }
34
+ update() {
35
+ this.recompute();
36
+ }
25
37
  size() {
26
38
  return this.entries.length;
27
39
  }
@@ -29,9 +41,24 @@ var SheetStack = class {
29
41
  this.entries = [];
30
42
  }
31
43
  recompute() {
44
+ let top = null;
45
+ const open = [];
46
+ for (const entry of this.entries) {
47
+ if (!entry.isOpen || entry.isOpen()) {
48
+ top = entry;
49
+ open.push(entry);
50
+ }
51
+ }
52
+ if (!top && this.entries.length > 0) {
53
+ top = this.entries[this.entries.length - 1];
54
+ }
32
55
  this.entries.forEach((entry, i) => {
33
56
  entry.setZIndex(BASE_Z + i * STEP);
34
- entry.setIsTop(i === this.entries.length - 1);
57
+ entry.setIsTop(entry === top);
58
+ if (entry.setDepth) {
59
+ const openIdx = open.indexOf(entry);
60
+ entry.setDepth(openIdx === -1 ? 0 : open.length - 1 - openIdx);
61
+ }
35
62
  });
36
63
  }
37
64
  };
@@ -230,11 +257,6 @@ var SnapResolver = class {
230
257
  this.allowedIds = allowed ?? raw.map((p) => p.id);
231
258
  this.recompute();
232
259
  }
233
- /**
234
- * Re-resolve raw → resolved against the current viewport / fit-measure,
235
- * recompute maxAxisSize, invalidate range cache, notify engine. Called on
236
- * resize, orientationchange, visualViewport changes, and snap-list swaps.
237
- */
238
260
  recompute() {
239
261
  this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
240
262
  this.maxAxisSize = this.resolved.reduce(
@@ -244,15 +266,10 @@ var SnapResolver = class {
244
266
  this.rangeCache = null;
245
267
  this.onMaxAxisSizeChange?.(this.maxAxisSize);
246
268
  }
247
- /** Replace the raw snap list and recompute. Engine calls on `setSnapPoints`. */
248
269
  setRaw(raw) {
249
270
  this.raw = raw;
250
271
  this.recompute();
251
272
  }
252
- /**
253
- * Replace the allow-list. Range cache depends on `(resolvedSnaps ×
254
- * allowedIds)` so flipping ids alone invalidates without a full recompute.
255
- */
256
273
  setAllowedIds(ids) {
257
274
  this.allowedIds = ids;
258
275
  this.rangeCache = null;
@@ -260,11 +277,6 @@ var SnapResolver = class {
260
277
  findById(id) {
261
278
  return findById(id, this.resolved);
262
279
  }
263
- /**
264
- * Pure drag-settle target resolution. Engine passes the gesture-specific
265
- * fields (delta, velocity, pointerKind, flickVelocity, dragThreshold) plus
266
- * `size`/`activeId` since those are still engine-owned.
267
- */
268
280
  findDragSettleTarget(args) {
269
281
  return findDragSettleTarget({
270
282
  ...args,
@@ -273,18 +285,9 @@ var SnapResolver = class {
273
285
  maxAxisSize: this.maxAxisSize
274
286
  });
275
287
  }
276
- /**
277
- * Memoised allowed-range query. Twice-per-drag-frame (gesture onMove +
278
- * applySize→computeProgress); cache collapses 2× iteration of resolvedSnaps
279
- * to one recompute per geometry/allow-list change.
280
- */
281
288
  getAllowedRange() {
282
289
  return this.rangeCache ?? (this.rangeCache = allowedRange(this.resolved, this.allowedIds));
283
290
  }
284
- /**
285
- * Read-only view of resolved snaps. Returned as `readonly` to discourage
286
- * accidental mutation by feature install sites that close over this list.
287
- */
288
291
  getResolvedSnaps() {
289
292
  return this.resolved;
290
293
  }
@@ -294,18 +297,6 @@ var SnapResolver = class {
294
297
  getMaxAxisSize() {
295
298
  return this.maxAxisSize;
296
299
  }
297
- /**
298
- * External clamp override. The resize-observer feature compares
299
- * `getMaxAxisSize()` against the live viewport and shrinks if the largest
300
- * resolved snap exceeds it (mobile address-bar collapse / soft-keyboard
301
- * window resize). The next `recompute()` overwrites this clamp from the
302
- * resolved-snap reduction.
303
- *
304
- * Does NOT invalidate the range cache — viewport clamping doesn't change
305
- * `(resolvedSnaps × allowedIds) → range`. DOES fire `onMaxAxisSizeChange`
306
- * so the resolver owns the layout-axis style write end-to-end (symmetric
307
- * with `recompute()` — feature call sites no longer duplicate the write).
308
- */
309
300
  setMaxAxisSize(n) {
310
301
  this.maxAxisSize = n;
311
302
  this.onMaxAxisSizeChange?.(n);
@@ -352,14 +343,17 @@ function installAutoCollapse(deps) {
352
343
  const fire = () => {
353
344
  if (deps.isDestroyed()) return;
354
345
  if (deps.isDragging()) return;
355
- const allowed = deps.getAllowedIds();
356
- const target = allowed.find((id) => {
346
+ const active = deps.resolveSnap(deps.getActiveId());
347
+ if (!active || active.size === 0) return;
348
+ let target = null;
349
+ for (const id of deps.getAllowedIds()) {
357
350
  const snap = deps.resolveSnap(id);
358
- return snap !== null && snap.size > 0;
359
- });
351
+ if (!snap || snap.size <= 0) continue;
352
+ if (!target || snap.size < target.size) target = snap;
353
+ }
360
354
  if (!target) return;
361
- if (target === deps.getActiveId()) return;
362
- deps.snapTo(target);
355
+ if (target.id === active.id || target.size >= active.size) return;
356
+ deps.snapTo(target.id);
363
357
  };
364
358
  const reset = () => {
365
359
  clearTimer();
@@ -370,6 +364,7 @@ function installAutoCollapse(deps) {
370
364
  }, ms);
371
365
  };
372
366
  const unsubscribe = deps.on("snap", () => reset());
367
+ if ((deps.resolveSnap(deps.getActiveId())?.size ?? 0) > 0) reset();
373
368
  return () => {
374
369
  clearTimer();
375
370
  unsubscribe();
@@ -382,6 +377,7 @@ function notifyLinkedSheets(sheets, self) {
382
377
  for (const linked of sheets) {
383
378
  if (linked === self) continue;
384
379
  try {
380
+ if (linked.state.size === 0) continue;
385
381
  const ids = linked.getAllowedIds();
386
382
  const target = ids.find((id) => id !== "closed");
387
383
  if (target && target !== linked.state.activeId) {
@@ -428,7 +424,7 @@ function installRoute(deps) {
428
424
  const onPop = (event) => {
429
425
  if (deps.isDestroyed()) return;
430
426
  const state = event.state;
431
- if (state && state.__bsRouted === target && deps.getSize() > 0) {
427
+ if ((!state || state.__bsRouted !== target) && deps.getSize() > 0) {
432
428
  routePushed = false;
433
429
  void deps.close();
434
430
  }
@@ -582,13 +578,17 @@ function installResizeObserver(deps) {
582
578
  if (viewportSize2 > 0 && viewportSize2 < max) {
583
579
  deps.setMaxAxisSize(viewportSize2);
584
580
  }
581
+ const wasAnimating = deps.isAnimating?.() ?? false;
585
582
  deps.cancelInFlight();
586
583
  deps.newCycle();
587
584
  const current = deps.resolveActiveSnap();
588
585
  const newMax = deps.getMaxAxisSize();
589
586
  const targetSize = current ? Math.min(current.size, newMax) : Math.min(deps.getSize(), newMax);
590
587
  deps.setSize(targetSize);
591
- if (current && !deps.isDragging()) deps.applySize(targetSize);
588
+ if (current && !deps.isDragging()) {
589
+ deps.applySize(targetSize);
590
+ if (wasAnimating) deps.resyncAfterCancel?.();
591
+ }
592
592
  };
593
593
  let resizeObserver = null;
594
594
  if (typeof ResizeObserver !== "undefined") {
@@ -680,73 +680,130 @@ var WriteSentinel = class {
680
680
  constructor() {
681
681
  __publicField(this, "last", -1);
682
682
  }
683
- /**
684
- * Returns `true` (and updates the cached value) if `next` differs from the
685
- * cached `last` by more than `epsilon`. Returns `false` (and leaves the
686
- * cache untouched) when the delta is within epsilon — caller skips the
687
- * CSSOM write.
688
- */
689
683
  shouldWrite(next, epsilon) {
690
684
  if (Math.abs(next - this.last) <= epsilon) return false;
691
685
  this.last = next;
692
686
  return true;
693
687
  }
694
- /**
695
- * Force the next `shouldWrite` to pass. Resets the cache to `-1` so any
696
- * non-negative `next` exceeds the epsilon comparison. Called from setters
697
- * that mutate state under the cache (range/mode/enabled flips that change
698
- * the progress→opacity mapping but don't move `progress` itself).
699
- */
700
688
  invalidate() {
701
689
  this.last = -1;
702
690
  }
703
- /**
704
- * Record `value` as the last-written without performing a write. Used
705
- * when a setter writes a literal value through a different path (e.g.
706
- * `setScrimMode("off")` writes `opacity: "0"` inline) and wants future
707
- * `shouldWrite` calls to short-circuit against that known cached state.
708
- */
709
691
  setLastWritten(value) {
710
692
  this.last = value;
711
693
  }
712
- /** Read-only accessor for diagnostic / dedup-aware peek. */
713
694
  get value() {
714
695
  return this.last;
715
696
  }
716
697
  };
717
698
 
699
+ // src/core/primitives/devWarn.ts
700
+ function devWarn(...args) {
701
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && typeof console !== "undefined") {
702
+ console.warn(...args);
703
+ }
704
+ }
705
+
706
+ // src/core/primitives/overlay-position.ts
707
+ function resolveSheetAnchoredStyle(mode, position, inset) {
708
+ const sizeOffset = `calc(var(--bs-size, 0px) + ${inset})`;
709
+ switch (mode) {
710
+ case "bottom":
711
+ if (position === "sheet-top-left") return { bottom: sizeOffset, left: inset };
712
+ if (position === "sheet-top-right") return { bottom: sizeOffset, right: inset };
713
+ return { bottom: sizeOffset, left: "50%", transform: "translateX(-50%)" };
714
+ case "top":
715
+ if (position === "sheet-top-left") return { top: sizeOffset, left: inset };
716
+ if (position === "sheet-top-right") return { top: sizeOffset, right: inset };
717
+ return { top: sizeOffset, left: "50%", transform: "translateX(-50%)" };
718
+ case "left":
719
+ if (position === "sheet-top-left") return { left: sizeOffset, top: inset };
720
+ if (position === "sheet-top-right") return { left: sizeOffset, bottom: inset };
721
+ return { left: sizeOffset, top: "50%", transform: "translateY(-50%)" };
722
+ case "right":
723
+ if (position === "sheet-top-left") return { right: sizeOffset, top: inset };
724
+ if (position === "sheet-top-right") return { right: sizeOffset, bottom: inset };
725
+ return { right: sizeOffset, top: "50%", transform: "translateY(-50%)" };
726
+ }
727
+ }
728
+ function applyOverlayPosition(ws, mode, position, inset) {
729
+ switch (position) {
730
+ case "top-left":
731
+ ws.top = inset;
732
+ ws.left = inset;
733
+ break;
734
+ case "top-center":
735
+ ws.top = inset;
736
+ ws.left = "50%";
737
+ ws.transform = "translateX(-50%)";
738
+ break;
739
+ case "top-right":
740
+ ws.top = inset;
741
+ ws.right = inset;
742
+ break;
743
+ case "center-left":
744
+ ws.top = "50%";
745
+ ws.left = inset;
746
+ ws.transform = "translateY(-50%)";
747
+ break;
748
+ case "center":
749
+ ws.top = "50%";
750
+ ws.left = "50%";
751
+ ws.transform = "translate(-50%, -50%)";
752
+ break;
753
+ case "center-right":
754
+ ws.top = "50%";
755
+ ws.right = inset;
756
+ ws.transform = "translateY(-50%)";
757
+ break;
758
+ case "bottom-left":
759
+ ws.bottom = inset;
760
+ ws.left = inset;
761
+ break;
762
+ case "bottom-center":
763
+ ws.bottom = inset;
764
+ ws.left = "50%";
765
+ ws.transform = "translateX(-50%)";
766
+ break;
767
+ case "bottom-right":
768
+ ws.bottom = inset;
769
+ ws.right = inset;
770
+ break;
771
+ case "sheet-top-left":
772
+ case "sheet-top-center":
773
+ case "sheet-top-right": {
774
+ const style = resolveSheetAnchoredStyle(mode, position, inset);
775
+ if (style.top !== void 0) ws.top = style.top;
776
+ if (style.bottom !== void 0) ws.bottom = style.bottom;
777
+ if (style.left !== void 0) ws.left = style.left;
778
+ if (style.right !== void 0) ws.right = style.right;
779
+ if (style.transform !== void 0) ws.transform = style.transform;
780
+ break;
781
+ }
782
+ }
783
+ }
784
+
718
785
  // src/core/controllers/scrim-controller.ts
786
+ var VALID_CSS_LENGTH = /^-?\d+(?:\.\d+)?(?:px|em|rem|%)$/;
719
787
  var ScrimController = class {
720
788
  constructor(deps, opts) {
721
- // ── DOM refs (mirror engine's; cached for hot path) ─────────────────
722
789
  __publicField(this, "screenComponent");
723
790
  __publicField(this, "backdrop");
724
791
  __publicField(this, "mode");
725
- // ── Engine callbacks ────────────────────────────────────────────────
726
792
  __publicField(this, "isDestroyed");
727
793
  __publicField(this, "isTopSheet");
728
794
  __publicField(this, "getAllowedIds");
729
795
  __publicField(this, "getResolvedSnaps");
730
796
  __publicField(this, "snapToFn");
731
- // ── Mode + state ────────────────────────────────────────────────────
797
+ __publicField(this, "closeFn");
798
+ __publicField(this, "scrimInteractive", false);
732
799
  __publicField(this, "scrimMode");
733
800
  __publicField(this, "backdropRange");
734
801
  __publicField(this, "screenRange");
735
- // setScrimEnabled toggle — gated through `scrimEnabled` rather than nuking
736
- // ranges so re-enable can restore prior values without a remount. Public
737
- // so tests / introspection can read it without an `as any` cast.
738
802
  __publicField(this, "scrimEnabled", true);
739
803
  __publicField(this, "savedScrimRanges", null);
740
- // ── Tap-to-close ────────────────────────────────────────────────────
741
804
  __publicField(this, "scrimTapToCloseEnabled", false);
742
805
  __publicField(this, "detachScrimTap", null);
743
- // ── Scrim overlay slot (positioned floating element) ────────────────
744
806
  __publicField(this, "scrimOverlayTeardown", null);
745
- // ── Hot-path opacity dedup caches (per-frame writes) ────────────────
746
- // Numeric sentinels migrated to WriteSentinel primitive — single
747
- // invalidation surface, shared with the engine's size/progress sentinels.
748
- // The two string-typed sentinels (`Pointer`, `Display`) keep their inline
749
- // pattern: equality on string union, no epsilon, different reset semantics.
750
807
  __publicField(this, "backdropOpacitySentinel", new WriteSentinel());
751
808
  __publicField(this, "screenOpacitySentinel", new WriteSentinel());
752
809
  __publicField(this, "lastBackdropPointer", "");
@@ -759,6 +816,7 @@ var ScrimController = class {
759
816
  this.getAllowedIds = deps.getAllowedIds;
760
817
  this.getResolvedSnaps = deps.getResolvedSnaps;
761
818
  this.snapToFn = deps.snapTo;
819
+ this.closeFn = deps.close;
762
820
  this.scrimMode = opts.scrimMode ?? "full";
763
821
  const preset = opts.scrimPreset ? SCRIM_PRESETS[opts.scrimPreset] : null;
764
822
  this.backdropRange = opts.backdropRange ?? preset?.range ?? [0, 1];
@@ -781,41 +839,15 @@ var ScrimController = class {
781
839
  }
782
840
  }
783
841
  }
784
- /**
785
- * Invalidate the opacity dedup caches. Called from any setter that mutates
786
- * range/mode/enabled — opacity is a pure function of (progress, range,
787
- * enabled), so a config change makes the cached values stale even if
788
- * `progress` itself didn't move. Next `applyOpacity(progress, false)` call
789
- * sees the -1 sentinel and re-runs the math instead of short-circuiting.
790
- *
791
- * Public for the engine: `setSnapPoints` mutates resolved geometry which
792
- * shifts `progress→opacity` mapping under the cache; engine calls this to
793
- * keep the next render frame correct on a static sheet.
794
- * @internal
795
- */
796
842
  invalidateOpacityCache() {
797
843
  this.backdropOpacitySentinel.invalidate();
798
844
  this.screenOpacitySentinel.invalidate();
799
845
  }
800
- /** Engine calls this when its `attach()` runs — installs scrim-tap-click if requested. */
801
846
  attach() {
802
847
  if (this.scrimTapToCloseEnabled) {
803
848
  this.setScrimTapToClose(true);
804
849
  }
805
850
  }
806
- // ── Hot-path: opacity computation called per-frame from engine.applySize ──
807
- /**
808
- * Per-frame opacity writer. Computes backdrop + screen opacity from the
809
- * sheet's progress (0..1 within allowed range), gated by sub-pixel dedup
810
- * caches so steady-state frames don't trigger style recalc.
811
- *
812
- * `progressChanged` is the engine's already-computed dedup gate — when
813
- * `false`, opacity is by construction a pure function of `progress` so
814
- * cached values are still valid; we short-circuit the Math.min/max +
815
- * division work entirely. Setters that mutate ranges/mode/enabled
816
- * invalidate the cache by writing -1 sentinels so the next call re-runs
817
- * the math.
818
- */
819
851
  applyOpacity(progress, progressChanged) {
820
852
  if (!progressChanged && this.backdropOpacitySentinel.value !== -1 && this.screenOpacitySentinel.value !== -1) {
821
853
  return;
@@ -861,80 +893,30 @@ var ScrimController = class {
861
893
  }
862
894
  }
863
895
  }
864
- // ── Public setters (engine forwards these 1:1) ─────────────────────
865
- /**
866
- * Replace the [start, end] progress window in which the backdrop fades 0→1.
867
- * @param range - Tuple of two progress values in 0..1 (start <= end).
868
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
869
- * @example
870
- * // Backdrop only starts darkening after the sheet crosses 30% open.
871
- * engine.setBackdropRange([0.3, 1]);
872
- */
873
896
  setBackdropRange(range, applySize) {
874
897
  if (this.isDestroyed()) return;
875
898
  this.backdropRange = range;
876
899
  this.invalidateOpacityCache();
877
900
  applySize();
878
901
  }
879
- /**
880
- * Replace the [start, end] progress window in which the screen scrim fades 0→1.
881
- * @param range - Tuple of two progress values in 0..1 (start <= end).
882
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
883
- * @example
884
- * // Scrim is fully opaque only when the sheet is past 50% — softer feel.
885
- * engine.setScreenRange([0, 0.5]);
886
- */
887
902
  setScreenRange(range, applySize) {
888
903
  if (this.isDestroyed()) return;
889
904
  this.screenRange = range;
890
905
  this.invalidateOpacityCache();
891
906
  applySize();
892
907
  }
893
- /**
894
- * Set the scrim background color. Pass `null`/`undefined` to clear the
895
- * inline write and let consumer CSS take over.
896
- * @param color - Any valid CSS color string, or `null`/`undefined` to clear.
897
- * @example
898
- * engine.setScrimColor("rgba(0, 0, 0, 0.6)");
899
- * engine.setScrimColor(null); // hand back to consumer CSS
900
- */
901
908
  setScrimColor(color) {
902
909
  if (this.isDestroyed()) return;
903
910
  this.applyScrimStyles({ color: color === void 0 ? null : color });
904
911
  }
905
- /**
906
- * Set the scrim backdrop-filter blur radius. Pass `null`/`undefined` to clear.
907
- * @param blur - CSS length (e.g. `"8px"`) consumed as `blur(<value>)`, or
908
- * `null`/`undefined` to remove the filter.
909
- * @example
910
- * engine.setScrimBlur("12px"); // frosted-glass effect
911
- * engine.setScrimBlur(null); // disable blur
912
- */
913
912
  setScrimBlur(blur) {
914
913
  if (this.isDestroyed()) return;
915
914
  this.applyScrimStyles({ blur: blur === void 0 ? null : blur });
916
915
  }
917
- /**
918
- * Toggle whether the scrim layer captures pointer events.
919
- * @param interactive - `true` → `pointer-events: auto`; `false` → `none`.
920
- * @example
921
- * engine.setScrimInteractive(true); // scrim swallows clicks
922
- * engine.setScrimInteractive(false); // clicks pass through to content below
923
- */
924
916
  setScrimInteractive(interactive) {
925
917
  if (this.isDestroyed()) return;
926
918
  this.applyScrimStyles({ interactive });
927
919
  }
928
- /**
929
- * Switch scrim positioning at runtime.
930
- * @param mode - `"full"` (consumer CSS), `"above-sheet"` (engine-pinned inset
931
- * so only the area NOT covered by the sheet is dimmed), or
932
- * `"off"` (display:none + opacity:0 — no remount needed).
933
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
934
- * @example
935
- * engine.setScrimMode("above-sheet"); // dim only the area above the sheet
936
- * engine.setScrimMode("off"); // disable scrim layer
937
- */
938
920
  setScrimMode(mode, applySize) {
939
921
  if (this.isDestroyed()) return;
940
922
  if (mode === this.scrimMode) return;
@@ -964,15 +946,6 @@ var ScrimController = class {
964
946
  }
965
947
  applySize();
966
948
  }
967
- /**
968
- * Enable or disable click-on-scrim → snap to first non-zero allowed snap.
969
- * Auto-promotes `pointer-events: auto` when scrim is non-interactive so the
970
- * click listener actually fires.
971
- * @param enabled - `true` installs the click listener; `false` removes it.
972
- * @example
973
- * engine.setScrimTapToClose(true); // tap outside sheet collapses it
974
- * engine.setScrimTapToClose(false); // require explicit close button
975
- */
976
949
  setScrimTapToClose(enabled) {
977
950
  if (this.isDestroyed()) return;
978
951
  if (!this.screenComponent) return;
@@ -985,11 +958,15 @@ var ScrimController = class {
985
958
  }
986
959
  const onClick = () => {
987
960
  if (this.isDestroyed()) return;
961
+ if (this.closeFn) {
962
+ this.closeFn();
963
+ return;
964
+ }
988
965
  const allowed = this.getAllowedIds();
989
966
  const snaps = this.getResolvedSnaps();
990
967
  const fallback = allowed.find((id) => {
991
968
  const snap = findById(id, snaps);
992
- return snap !== null && snap.size > 0;
969
+ return snap !== null && snap.size === 0;
993
970
  });
994
971
  if (fallback) this.snapToFn(fallback);
995
972
  };
@@ -1002,20 +979,11 @@ var ScrimController = class {
1002
979
  this.scrimTapToCloseEnabled = false;
1003
980
  }
1004
981
  }
1005
- /**
1006
- * Toggle the scrim layer without remounting. Disabling stashes current
1007
- * `screenRange`/`backdropRange` and degenerates them to `[1, 1]` (opacity
1008
- * formula → 0 across the whole range); re-enabling restores the saved values.
1009
- * @param enabled - `false` hides scrim, `true` restores prior ranges.
1010
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
1011
- * @example
1012
- * engine.setScrimEnabled(false); // hide scrim, ranges remembered
1013
- * engine.setScrimEnabled(true); // restore prior ranges (no remount)
1014
- */
1015
982
  setScrimEnabled(enabled, applySize) {
1016
983
  if (this.isDestroyed()) return;
1017
984
  if (enabled === this.scrimEnabled) return;
1018
985
  this.scrimEnabled = enabled;
986
+ this.invalidateOpacityCache();
1019
987
  if (!enabled) {
1020
988
  this.savedScrimRanges = {
1021
989
  screen: this.screenRange,
@@ -1030,20 +998,6 @@ var ScrimController = class {
1030
998
  }
1031
999
  applySize();
1032
1000
  }
1033
- /**
1034
- * Batch-update multiple scrim fields with a single applySize() at the end.
1035
- * If `preset` is set, it seeds color/blur/interactive/range first; explicit
1036
- * fields override.
1037
- * @param opts - Partial scrim config: preset, mode, enabled, tapToClose,
1038
- * color, blur, interactive, range.
1039
- * @param applySize - Engine-supplied recompute hook; runs ONCE per call.
1040
- * @example
1041
- * engine.setScrim({
1042
- * preset: "frosted",
1043
- * mode: "above-sheet",
1044
- * tapToClose: true,
1045
- * });
1046
- */
1047
1001
  setScrim(opts, applySize) {
1048
1002
  if (this.isDestroyed()) return;
1049
1003
  if (opts.preset) {
@@ -1053,7 +1007,12 @@ var ScrimController = class {
1053
1007
  blur: cfg.blur ?? null,
1054
1008
  interactive: cfg.interactive
1055
1009
  });
1056
- this.screenRange = cfg.range;
1010
+ if (!this.scrimEnabled && this.savedScrimRanges) {
1011
+ this.savedScrimRanges = { screen: cfg.range, backdrop: cfg.range };
1012
+ } else {
1013
+ this.screenRange = cfg.range;
1014
+ this.backdropRange = cfg.range;
1015
+ }
1057
1016
  }
1058
1017
  const noop = () => {
1059
1018
  };
@@ -1066,27 +1025,18 @@ var ScrimController = class {
1066
1025
  interactive: opts.interactive
1067
1026
  });
1068
1027
  if (opts.range !== void 0) {
1069
- this.screenRange = opts.range;
1028
+ if (!this.scrimEnabled && this.savedScrimRanges) {
1029
+ this.savedScrimRanges = {
1030
+ screen: opts.range,
1031
+ backdrop: this.savedScrimRanges.backdrop
1032
+ };
1033
+ } else {
1034
+ this.screenRange = opts.range;
1035
+ }
1070
1036
  }
1071
1037
  this.invalidateOpacityCache();
1072
1038
  applySize();
1073
1039
  }
1074
- /**
1075
- * Inject a positioned floating element into the scrim layer (e.g. a close
1076
- * button anchored top-right). Replaces any prior overlay so repeated calls
1077
- * (reactive effects re-running) don't accumulate wrappers.
1078
- * @param opts - Children element, optional `inset` (default `"16px"`),
1079
- * `position` (9-way grid, default `"top-right"`), and
1080
- * `interactive` flag (default `true`).
1081
- * @returns Teardown function that removes the wrapper from the DOM.
1082
- * @example
1083
- * const teardown = engine.setScrimOverlay({
1084
- * children: closeBtn,
1085
- * position: "top-right",
1086
- * inset: "20px",
1087
- * });
1088
- * // later: teardown(); // remove the floating button
1089
- */
1090
1040
  setScrimOverlay(opts) {
1091
1041
  if (this.isDestroyed()) return () => {
1092
1042
  };
@@ -1094,61 +1044,38 @@ var ScrimController = class {
1094
1044
  };
1095
1045
  this.scrimOverlayTeardown?.();
1096
1046
  const screen = this.screenComponent;
1097
- const inset = opts.inset ?? "16px";
1047
+ const insetRaw = opts.inset ?? "16px";
1048
+ const inset = VALID_CSS_LENGTH.test(insetRaw) ? insetRaw : "16px";
1049
+ if (inset !== insetRaw) {
1050
+ devWarn(
1051
+ `[bottom-sheet] setScrimOverlay: invalid inset ${JSON.stringify(insetRaw)}; expected a CSS length like "16px" / "1rem" / "5%". Falling back to "16px".`
1052
+ );
1053
+ }
1054
+ if (opts.children.ownerDocument && opts.children.ownerDocument !== screen.ownerDocument) {
1055
+ devWarn(
1056
+ "[bottom-sheet] setScrimOverlay: children belong to a different document than the scrim. Cross-document adoption can drop event listeners; construct children via the same document as the sheet root."
1057
+ );
1058
+ }
1098
1059
  const position = opts.position ?? "top-right";
1099
1060
  const interactive = opts.interactive ?? true;
1100
1061
  const wrapper = screen.ownerDocument.createElement("div");
1101
1062
  wrapper.className = "bs-scrim-overlay";
1102
1063
  const ws = wrapper.style;
1103
1064
  ws.position = "absolute";
1104
- switch (position) {
1105
- case "top-left":
1106
- ws.top = inset;
1107
- ws.left = inset;
1108
- break;
1109
- case "top-center":
1110
- ws.top = inset;
1111
- ws.left = "50%";
1112
- ws.transform = "translateX(-50%)";
1113
- break;
1114
- case "top-right":
1115
- ws.top = inset;
1116
- ws.right = inset;
1117
- break;
1118
- case "center-left":
1119
- ws.top = "50%";
1120
- ws.left = inset;
1121
- ws.transform = "translateY(-50%)";
1122
- break;
1123
- case "center":
1124
- ws.top = "50%";
1125
- ws.left = "50%";
1126
- ws.transform = "translate(-50%, -50%)";
1127
- break;
1128
- case "center-right":
1129
- ws.top = "50%";
1130
- ws.right = inset;
1131
- ws.transform = "translateY(-50%)";
1132
- break;
1133
- case "bottom-left":
1134
- ws.bottom = inset;
1135
- ws.left = inset;
1136
- break;
1137
- case "bottom-center":
1138
- ws.bottom = inset;
1139
- ws.left = "50%";
1140
- ws.transform = "translateX(-50%)";
1141
- break;
1142
- case "bottom-right":
1143
- ws.bottom = inset;
1144
- ws.right = inset;
1145
- break;
1146
- }
1065
+ applyOverlayPosition(ws, this.mode, position, inset);
1147
1066
  if (interactive) {
1148
1067
  ws.pointerEvents = "auto";
1149
1068
  }
1150
1069
  wrapper.appendChild(opts.children);
1151
- screen.appendChild(wrapper);
1070
+ const host = screen.parentElement;
1071
+ if (!host) {
1072
+ devWarn(
1073
+ "[bottom-sheet] setScrimOverlay: scrim element has no parent, skipping overlay injection. Mount the sheet root before calling setScrimOverlay."
1074
+ );
1075
+ return () => {
1076
+ };
1077
+ }
1078
+ host.appendChild(wrapper);
1152
1079
  const teardown = () => {
1153
1080
  if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
1154
1081
  if (this.scrimOverlayTeardown === teardown) {
@@ -1158,7 +1085,6 @@ var ScrimController = class {
1158
1085
  this.scrimOverlayTeardown = teardown;
1159
1086
  return teardown;
1160
1087
  }
1161
- /** Tear down toggle-state listeners + remove any injected overlay wrapper. */
1162
1088
  destroy() {
1163
1089
  this.detachScrimTap?.();
1164
1090
  this.detachScrimTap = null;
@@ -1177,8 +1103,6 @@ var ScrimController = class {
1177
1103
  this.lastBackdropPointer = "";
1178
1104
  this.lastScreenDisplay = "";
1179
1105
  }
1180
- // ── Private helpers ────────────────────────────────────────────────
1181
- /** Centralized scrim style writer — atomic 3-property write across all setters. */
1182
1106
  applyScrimStyles(opts) {
1183
1107
  if (!this.screenComponent) return;
1184
1108
  const s = this.screenComponent.style;
@@ -1197,10 +1121,10 @@ var ScrimController = class {
1197
1121
  }
1198
1122
  }
1199
1123
  if (opts.interactive !== void 0) {
1124
+ this.scrimInteractive = opts.interactive;
1200
1125
  s.pointerEvents = opts.interactive ? "auto" : "none";
1201
1126
  }
1202
1127
  }
1203
- /** Pin the screen overlay to the area NOT occupied by the sheet. */
1204
1128
  applyAboveSheetInset() {
1205
1129
  if (!this.screenComponent) return;
1206
1130
  const insetByMode = {
@@ -1212,7 +1136,7 @@ var ScrimController = class {
1212
1136
  const s = this.screenComponent.style;
1213
1137
  s.position = "fixed";
1214
1138
  s.inset = insetByMode[this.mode];
1215
- s.pointerEvents = "none";
1139
+ s.pointerEvents = this.scrimInteractive || this.scrimTapToCloseEnabled ? "auto" : "none";
1216
1140
  }
1217
1141
  };
1218
1142
 
@@ -1266,6 +1190,10 @@ var tween = ({
1266
1190
  promise
1267
1191
  };
1268
1192
  };
1193
+ var prefersReducedMotion = () => {
1194
+ if (typeof window === "undefined" || !window.matchMedia) return false;
1195
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
1196
+ };
1269
1197
 
1270
1198
  // src/core/animation/spring.ts
1271
1199
  var DEFAULT_SPRING = {
@@ -1296,14 +1224,20 @@ var runSpring = ({
1296
1224
  if (cancelled) return;
1297
1225
  const dt = Math.min((now - lastTime) / 1e3, MAX_DT);
1298
1226
  lastTime = now;
1299
- const subDt = Math.min(dt, 1 / 240);
1300
- let stepsLeft = Math.max(1, Math.ceil(dt / subDt));
1227
+ const steps = Math.max(1, Math.ceil(dt * 240));
1228
+ const subDt = dt / steps;
1229
+ let stepsLeft = steps;
1301
1230
  while (stepsLeft-- > 0) {
1302
1231
  const force = -cfg.stiffness * (x - to) - cfg.damping * v;
1303
1232
  const accel = force / cfg.mass;
1304
1233
  v += accel * subDt;
1305
1234
  x += v * subDt;
1306
1235
  }
1236
+ if (!Number.isFinite(x) || !Number.isFinite(v)) {
1237
+ onUpdate(to, 0);
1238
+ resolve();
1239
+ return;
1240
+ }
1307
1241
  onUpdate(x, v);
1308
1242
  const isAtRest = Math.abs(to - x) < (cfg.restDelta ?? 0.5) && Math.abs(v) < (cfg.restSpeed ?? 0.5);
1309
1243
  if (isAtRest) {
@@ -1367,13 +1301,9 @@ var AnimationRunner = class {
1367
1301
  __publicField(this, "duration");
1368
1302
  __publicField(this, "easing");
1369
1303
  __publicField(this, "respectReducedMotion");
1370
- /** Live-tracked via mq listener so animateTo skips matchMedia per snap. */
1371
1304
  __publicField(this, "reducedMotion", false);
1372
- /** Disposes the matchMedia change listener — drained from destroy(). */
1373
1305
  __publicField(this, "detachReducedMotion", null);
1374
- /** Computed once — both inputs (option + API presence) stable for life. */
1375
1306
  __publicField(this, "viewTransitionsAvailable");
1376
- /** In-flight handles. Public for tests / debugging only — engine drives via cancel(). */
1377
1307
  __publicField(this, "currentTween", null);
1378
1308
  __publicField(this, "currentSpring", null);
1379
1309
  this.element = deps.element;
@@ -1404,26 +1334,13 @@ var AnimationRunner = class {
1404
1334
  }
1405
1335
  }
1406
1336
  }
1407
- /**
1408
- * Live derived flag — `true` whenever a spring/tween handle is in flight.
1409
- * Reading the canonical handles (instead of mirroring into a separate
1410
- * boolean) eliminates the reduced-motion early-return latent bug where
1411
- * the flag stayed true forever, and removes the cross-controller
1412
- * setIsAnimating callback.
1413
- */
1414
1337
  get isAnimating() {
1415
1338
  return this.currentTween !== null || this.currentSpring !== null;
1416
1339
  }
1417
- /** Cancel any in-flight tween/spring without touching engine state. */
1418
1340
  cancel() {
1419
1341
  this.currentTween?.cancel();
1420
1342
  this.currentSpring?.cancel();
1421
1343
  }
1422
- /**
1423
- * Animate from current size to `target`. Resolves when the animation
1424
- * completes; resolves early if cancelled mid-flight (caller checks an
1425
- * abort signal post-await for cycle-invariant correctness).
1426
- */
1427
1344
  async animateTo(target, velocityPxPerMs) {
1428
1345
  if (target === this.getSize()) return;
1429
1346
  this.currentTween?.cancel();
@@ -1464,7 +1381,6 @@ var AnimationRunner = class {
1464
1381
  if (!this.isDragging()) this.element.style.willChange = "auto";
1465
1382
  }
1466
1383
  }
1467
- /** Tear down the matchMedia listener. Engine calls from destroy(). */
1468
1384
  destroy() {
1469
1385
  this.detachReducedMotion?.();
1470
1386
  this.detachReducedMotion = null;
@@ -1496,9 +1412,13 @@ var isVisible = (el) => {
1496
1412
  var focusables = (root) => Array.from(root.querySelectorAll(FOCUSABLE)).filter(
1497
1413
  (el) => !el.hasAttribute("inert") && isVisible(el) && !el.matches("[aria-hidden='true']")
1498
1414
  );
1415
+ var trapStack = [];
1499
1416
  var installFocusTrap = (container, options = {}) => {
1500
1417
  if (typeof document === "undefined") return () => {
1501
1418
  };
1419
+ const token = {};
1420
+ trapStack.push(token);
1421
+ const isActive = () => trapStack[trapStack.length - 1] === token;
1502
1422
  const previouslyFocused = document.activeElement;
1503
1423
  const focusInitial = () => {
1504
1424
  const initial = typeof options.initialFocus === "string" ? container.querySelector(options.initialFocus) : options.initialFocus ?? focusables(container)[0] ?? container;
@@ -1506,6 +1426,7 @@ var installFocusTrap = (container, options = {}) => {
1506
1426
  };
1507
1427
  focusInitial();
1508
1428
  const handleKey = (e) => {
1429
+ if (!isActive()) return;
1509
1430
  if (e.key === "Escape" && options.onEscape) {
1510
1431
  options.onEscape();
1511
1432
  return;
@@ -1528,6 +1449,7 @@ var installFocusTrap = (container, options = {}) => {
1528
1449
  }
1529
1450
  };
1530
1451
  const handleFocusIn = (e) => {
1452
+ if (!isActive()) return;
1531
1453
  const target = e.target;
1532
1454
  if (target && !container.contains(target)) {
1533
1455
  const list = focusables(container);
@@ -1540,6 +1462,8 @@ var installFocusTrap = (container, options = {}) => {
1540
1462
  return () => {
1541
1463
  if (released) return;
1542
1464
  released = true;
1465
+ const idx = trapStack.indexOf(token);
1466
+ if (idx !== -1) trapStack.splice(idx, 1);
1543
1467
  document.removeEventListener("keydown", handleKey, true);
1544
1468
  document.removeEventListener("focusin", handleFocusIn, true);
1545
1469
  if (previouslyFocused && document.contains(previouslyFocused)) {
@@ -1595,11 +1519,17 @@ function createInertSiblings(rootProvider) {
1595
1519
  return {
1596
1520
  apply() {
1597
1521
  if (typeof document === "undefined") return;
1598
- const root = rootProvider();
1599
- const parent = root.parentElement ?? document.body;
1522
+ let top = rootProvider();
1523
+ while (top.parentElement && top.parentElement !== document.body) {
1524
+ top = top.parentElement;
1525
+ }
1526
+ const parent = top.parentElement ?? document.body;
1600
1527
  for (const child of Array.from(parent.children)) {
1601
- if (child === root) continue;
1528
+ if (child === top) continue;
1602
1529
  if (child.hasAttribute("inert")) continue;
1530
+ if (child.classList.contains("bs-backdrop") || child.classList.contains("bs-screen") || child.classList.contains("bs-root")) {
1531
+ continue;
1532
+ }
1603
1533
  child.setAttribute("inert", "");
1604
1534
  tracked.push(child);
1605
1535
  }
@@ -1615,7 +1545,6 @@ function createInertSiblings(rootProvider) {
1615
1545
  var LifecycleController = class {
1616
1546
  constructor(deps, opts) {
1617
1547
  __publicField(this, "element");
1618
- __publicField(this, "closeFn");
1619
1548
  __publicField(this, "focusTrapEnabled");
1620
1549
  __publicField(this, "initialFocus");
1621
1550
  __publicField(this, "closeOnEscape");
@@ -1624,16 +1553,11 @@ var LifecycleController = class {
1624
1553
  __publicField(this, "inertSiblings");
1625
1554
  __publicField(this, "shouldApplyInertSiblings");
1626
1555
  __publicField(this, "returnFocus");
1627
- /** Toggle-state teardowns — fire on close, NOT in destroy-only stack. */
1628
1556
  __publicField(this, "releaseFocusTrap", null);
1629
1557
  __publicField(this, "releaseScrollLock", null);
1630
- /** True between install() and release(). Guards release()/destroy() from
1631
- * re-running side effects (notably `returnFocus.focus()`) on already-
1632
- * released or never-installed instances. */
1633
1558
  __publicField(this, "installed", false);
1634
1559
  __publicField(this, "destroyed", false);
1635
1560
  this.element = deps.element;
1636
- this.closeFn = deps.close;
1637
1561
  this.focusTrapEnabled = opts.focusTrap ?? false;
1638
1562
  this.initialFocus = opts.initialFocus;
1639
1563
  this.closeOnEscape = opts.closeOnEscape ?? true;
@@ -1643,14 +1567,9 @@ var LifecycleController = class {
1643
1567
  this.returnFocus = opts.returnFocus;
1644
1568
  this.inertSiblings = createInertSiblings(() => this.element);
1645
1569
  }
1646
- /**
1647
- * Run the open-time install steps. Idempotent: each step checks its
1648
- * existing teardown ref so a re-open during a half-cancelled close
1649
- * doesn't double-install. Sets `installed=true` BEFORE the first side
1650
- * effect so a partial throw (detached / shadow-DOM target) leaves the
1651
- * caller's `release()` rollback path armed to undo whatever did succeed.
1652
- * No-op when destroyed.
1653
- */
1570
+ get isInstalled() {
1571
+ return this.installed;
1572
+ }
1654
1573
  install() {
1655
1574
  if (this.destroyed) return;
1656
1575
  this.installed = true;
@@ -1659,30 +1578,17 @@ var LifecycleController = class {
1659
1578
  }
1660
1579
  if (this.focusTrapEnabled && !this.releaseFocusTrap) {
1661
1580
  this.releaseFocusTrap = installFocusTrap(this.element, {
1662
- initialFocus: this.initialFocus,
1663
- onEscape: this.closeOnEscape ? () => void this.closeFn() : void 0
1581
+ initialFocus: this.initialFocus
1664
1582
  });
1665
1583
  }
1666
1584
  if (this.inertSiblingsEnabled && this.shouldApplyInertSiblings()) {
1667
1585
  this.inertSiblings.apply();
1668
1586
  }
1669
1587
  }
1670
- /**
1671
- * Update the post-release focus target. Overlay re-binds this when the
1672
- * consumer changes `returnFocus` after construction; the new value applies
1673
- * on the next `release()`. No-op when destroyed.
1674
- */
1675
1588
  setReturnFocus(target) {
1676
1589
  if (this.destroyed) return;
1677
1590
  this.returnFocus = target;
1678
1591
  }
1679
- /**
1680
- * Release all install-time teardowns. Short-circuits when not installed,
1681
- * so `close() → destroy()` (each routing through here) only restores
1682
- * focus once. The `installed` guard lets `inertSiblings.remove()` and
1683
- * the focus-trap/scroll-lock teardown nullings stay simple — they were
1684
- * already idempotent, but `returnFocus.focus()` was firing on every call.
1685
- */
1686
1592
  release() {
1687
1593
  if (!this.installed) return;
1688
1594
  this.installed = false;
@@ -1696,12 +1602,6 @@ var LifecycleController = class {
1696
1602
  target?.focus?.();
1697
1603
  }
1698
1604
  }
1699
- /**
1700
- * Final teardown — releases any still-installed listeners and seals the
1701
- * controller. After destroy(), install() is a no-op and setReturnFocus()
1702
- * is rejected, so post-destroy mutations can't queue up a stale focus
1703
- * restore.
1704
- */
1705
1605
  destroy() {
1706
1606
  this.release();
1707
1607
  this.destroyed = true;
@@ -1711,12 +1611,10 @@ var LifecycleController = class {
1711
1611
  // src/core/gestures.ts
1712
1612
  var isAxisVertical = (mode) => mode === "bottom" || mode === "top";
1713
1613
  var SampleRing = class {
1714
- // current count, ≤ capacity
1715
1614
  constructor(capacity) {
1716
1615
  __publicField(this, "slots");
1717
1616
  __publicField(this, "capacity");
1718
1617
  __publicField(this, "head", 0);
1719
- // next write index
1720
1618
  __publicField(this, "size", 0);
1721
1619
  this.capacity = capacity;
1722
1620
  this.slots = new Array(capacity);
@@ -1729,12 +1627,6 @@ var SampleRing = class {
1729
1627
  this.head = (this.head + 1) % this.capacity;
1730
1628
  if (this.size < this.capacity) this.size++;
1731
1629
  }
1732
- /**
1733
- * Drop oldest entries whose timestamp is below `cutoff`, but never shrink
1734
- * below `minKeep`. Mirrors the original
1735
- * `while (samples.length > minKeep && samples[0].t < cutoff) samples.shift()`
1736
- * but without copying — just advances the logical tail.
1737
- */
1738
1630
  pruneBefore(cutoff, minKeep) {
1739
1631
  while (this.size > minKeep) {
1740
1632
  const oldestIdx = (this.head - this.size + this.capacity) % this.capacity;
@@ -1840,10 +1732,11 @@ var installGestures = (handle, mode, callbacks) => {
1840
1732
  };
1841
1733
 
1842
1734
  // src/core/primitives/rubber-band.ts
1843
- var RUBBER_BAND_FACTOR = 0.55;
1844
1735
  function rubberBand(overshoot, maxAxisSize) {
1845
1736
  const cap = Math.min(maxAxisSize * 0.15, 80);
1846
- return Math.sign(overshoot) * Math.pow(Math.abs(overshoot), RUBBER_BAND_FACTOR) * Math.min(6, cap / Math.max(Math.abs(overshoot), 1));
1737
+ if (cap <= 0) return 0;
1738
+ const abs = Math.abs(overshoot);
1739
+ return Math.sign(overshoot) * (cap * abs / (abs + cap));
1847
1740
  }
1848
1741
 
1849
1742
  // src/core/features/soft-keyboard.ts
@@ -1870,28 +1763,14 @@ var GestureController = class {
1870
1763
  __publicField(this, "detach", null);
1871
1764
  __publicField(this, "isDragging_", false);
1872
1765
  __publicField(this, "dragStartSize", 0);
1873
- // Only "touch" gestures are eligible for soft-keyboard dismissal —
1874
- // mouse/trackpad users don't have a keyboard to dismiss.
1875
1766
  __publicField(this, "currentPointerKind", "touch");
1876
- // One blur per drag — flipping focus every move event races the keyboard's
1877
- // own animations on iOS and makes the page jitter.
1878
1767
  __publicField(this, "keyboardDismissed", false);
1879
- // Reused payload object for the "drag" event — mutated each onMove instead
1880
- // of allocating a fresh `{size, delta}` per frame. At 60-120Hz a long drag
1881
- // would otherwise churn tens of KB through the GC. Mirrors browser
1882
- // PointerEvent semantics: consumers must NOT retain the reference across
1883
- // frames (clone if needed). See SheetEventMap.drag JSDoc.
1884
1768
  __publicField(this, "dragPayload", { size: 0, delta: 0 });
1885
1769
  this.deps = deps;
1886
1770
  }
1887
- /** True between `dragstart` and `dragend`/`onCancel`. */
1888
1771
  get isDragging() {
1889
1772
  return this.isDragging_;
1890
1773
  }
1891
- /**
1892
- * Bind pointer listeners on the handle and return a teardown. Engine pushes
1893
- * the teardown into its TeardownStack so destroy() drains it once.
1894
- */
1895
1774
  install() {
1896
1775
  if (this.detach) return this.detach;
1897
1776
  this.detach = installGestures(this.deps.handle, this.deps.mode, {
@@ -1942,17 +1821,16 @@ var GestureController = class {
1942
1821
  this.isDragging_ = false;
1943
1822
  this.keyboardDismissed = false;
1944
1823
  this.deps.getRoot()?.removeAttribute("data-dragging");
1824
+ this.deps.emit("dragend", {
1825
+ size: this.deps.getDragContext().size,
1826
+ velocity: 0
1827
+ });
1945
1828
  const restoreTo = this.dragStartSize;
1946
1829
  void this.deps.animateTo(restoreTo, 0);
1947
1830
  }
1948
1831
  });
1949
1832
  return this.detach;
1950
1833
  }
1951
- /**
1952
- * Force-clear drag visual state. Engine calls from destroy() when destroyed
1953
- * mid-drag — gesture detach won't synthesize an onCancel, so we have to
1954
- * roll back the data-attribute + will-change ourselves.
1955
- */
1956
1834
  forceClearDragState() {
1957
1835
  if (!this.isDragging_) return;
1958
1836
  this.isDragging_ = false;
@@ -1971,11 +1849,6 @@ var AriaSliderWriter = class {
1971
1849
  mode === "bottom" || mode === "top" ? "vertical" : "horizontal"
1972
1850
  );
1973
1851
  }
1974
- /**
1975
- * Update slider position. `allowedIds` length determines `aria-valuemax`;
1976
- * `activeId` indexes into it for `aria-valuenow` + `aria-valuetext`.
1977
- * Empty list → strip every dynamic attr so AT skips the handle entirely.
1978
- */
1979
1852
  setValue(allowedIds, activeId) {
1980
1853
  if (allowedIds.length === 0) {
1981
1854
  this.handle.removeAttribute("aria-valuemin");
@@ -2025,6 +1898,287 @@ function createScrollCache(deps) {
2025
1898
  };
2026
1899
  }
2027
1900
 
1901
+ // src/core/primitives/anchor-animations.ts
1902
+ var DEFAULT_DURATION3 = 200;
1903
+ var DEFAULT_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
1904
+ var PRESET_FRAMES = {
1905
+ fade: {
1906
+ enter: [{ opacity: 0 }, { opacity: 1 }],
1907
+ exit: [{ opacity: 1 }, { opacity: 0 }]
1908
+ },
1909
+ scale: {
1910
+ enter: [
1911
+ { opacity: 0, transform: "scale(0.85)" },
1912
+ { opacity: 1, transform: "scale(1)" }
1913
+ ],
1914
+ exit: [
1915
+ { opacity: 1, transform: "scale(1)" },
1916
+ { opacity: 0, transform: "scale(0.85)" }
1917
+ ]
1918
+ },
1919
+ slide: {
1920
+ enter: [
1921
+ { opacity: 0, transform: "translateY(10px)" },
1922
+ { opacity: 1, transform: "translateY(0)" }
1923
+ ],
1924
+ exit: [
1925
+ { opacity: 1, transform: "translateY(0)" },
1926
+ { opacity: 0, transform: "translateY(10px)" }
1927
+ ]
1928
+ },
1929
+ pop: {
1930
+ enter: [
1931
+ { opacity: 0, transform: "scale(0.6)", offset: 0 },
1932
+ { opacity: 1, transform: "scale(1.06)", offset: 0.7 },
1933
+ { opacity: 1, transform: "scale(1)", offset: 1 }
1934
+ ],
1935
+ exit: [
1936
+ { opacity: 1, transform: "scale(1)" },
1937
+ { opacity: 0, transform: "scale(0.6)" }
1938
+ ]
1939
+ }
1940
+ };
1941
+ var INSTANT = {
1942
+ finished: Promise.resolve(),
1943
+ cancel: () => {
1944
+ }
1945
+ };
1946
+ function resolveTransitionFrames(spec, phase) {
1947
+ const fallback = { frames: null, duration: 0, easing: DEFAULT_EASING };
1948
+ if (spec === "none") return fallback;
1949
+ if (spec === void 0 || typeof spec === "string") {
1950
+ const preset2 = PRESET_FRAMES[spec ?? "fade"];
1951
+ return {
1952
+ frames: preset2[phase],
1953
+ duration: DEFAULT_DURATION3,
1954
+ easing: DEFAULT_EASING
1955
+ };
1956
+ }
1957
+ const custom = phase === "enter" ? spec.enter : spec.exit;
1958
+ if (custom) {
1959
+ return {
1960
+ frames: custom,
1961
+ duration: spec.duration ?? DEFAULT_DURATION3,
1962
+ easing: spec.easing ?? DEFAULT_EASING
1963
+ };
1964
+ }
1965
+ if (spec.preset === "none") return fallback;
1966
+ const preset = PRESET_FRAMES[spec.preset ?? "fade"];
1967
+ return {
1968
+ frames: preset[phase],
1969
+ duration: spec.duration ?? DEFAULT_DURATION3,
1970
+ easing: spec.easing ?? DEFAULT_EASING
1971
+ };
1972
+ }
1973
+ function runAnchorTransition(el, spec, phase) {
1974
+ const respectReduced = typeof spec === "object" ? spec.respectReducedMotion !== false : true;
1975
+ if (respectReduced && prefersReducedMotion()) return INSTANT;
1976
+ const { frames, duration, easing } = resolveTransitionFrames(spec, phase);
1977
+ if (!frames || duration <= 0) return INSTANT;
1978
+ if (typeof el.animate !== "function") return INSTANT;
1979
+ const animation = el.animate(frames, {
1980
+ duration,
1981
+ easing,
1982
+ fill: "both"
1983
+ });
1984
+ const finished = animation.finished.then(() => void 0).catch(() => void 0);
1985
+ return {
1986
+ finished,
1987
+ cancel: () => {
1988
+ try {
1989
+ animation.cancel();
1990
+ } catch {
1991
+ return;
1992
+ }
1993
+ }
1994
+ };
1995
+ }
1996
+
1997
+ // src/core/features/sheet-anchors.ts
1998
+ function attachAnchor(deps, opts) {
1999
+ const doc = deps.host.ownerDocument;
2000
+ const wrapper = doc.createElement("div");
2001
+ wrapper.className = "bs-anchor";
2002
+ const interactive = opts.interactive ?? true;
2003
+ const position = opts.position ?? "sheet-top-right";
2004
+ const isDock = position === "dock-bottom" || position === "dock-top";
2005
+ const ws = wrapper.style;
2006
+ ws.position = "fixed";
2007
+ if (isDock) {
2008
+ ws.left = "0";
2009
+ ws.right = "0";
2010
+ if (position === "dock-bottom") {
2011
+ ws.bottom = "0";
2012
+ } else {
2013
+ ws.top = "0";
2014
+ }
2015
+ } else {
2016
+ applyOverlayPosition(ws, deps.mode, position, opts.inset ?? "16px");
2017
+ }
2018
+ if (opts.fadeRange) {
2019
+ const [r0, r1] = opts.fadeRange;
2020
+ const span = Math.max(r1 - r0, 1e-4);
2021
+ ws.opacity = `clamp(0, calc((var(--bs-progress, 0) - ${r0}) / ${span}), 1)`;
2022
+ }
2023
+ wrapper.appendChild(opts.element);
2024
+ deps.host.appendChild(wrapper);
2025
+ let visible = null;
2026
+ let inFlight = null;
2027
+ const shouldShow = (state) => {
2028
+ if (typeof opts.showOn === "function") return opts.showOn(state);
2029
+ if (Array.isArray(opts.showOn)) return opts.showOn.includes(state.activeId);
2030
+ return isDock || state.size > 0;
2031
+ };
2032
+ const applyVisibility = (next, animate) => {
2033
+ if (next === visible) return;
2034
+ visible = next;
2035
+ inFlight?.cancel();
2036
+ inFlight = null;
2037
+ wrapper.style.pointerEvents = next && interactive ? "auto" : "none";
2038
+ if (next) {
2039
+ wrapper.style.visibility = "";
2040
+ if (animate) {
2041
+ inFlight = runAnchorTransition(opts.element, opts.animation, "enter");
2042
+ }
2043
+ return;
2044
+ }
2045
+ if (!animate) {
2046
+ wrapper.style.visibility = "hidden";
2047
+ return;
2048
+ }
2049
+ const handle = runAnchorTransition(opts.element, opts.animation, "exit");
2050
+ inFlight = handle;
2051
+ void handle.finished.then(() => {
2052
+ if (inFlight === handle && visible === false) {
2053
+ wrapper.style.visibility = "hidden";
2054
+ }
2055
+ });
2056
+ };
2057
+ const evaluate = (animate) => {
2058
+ if (deps.isDestroyed()) return;
2059
+ applyVisibility(shouldShow(deps.getState()), animate);
2060
+ };
2061
+ const offs = [
2062
+ deps.on("snap", () => evaluate(true)),
2063
+ deps.on("open", () => evaluate(true)),
2064
+ deps.on("close", () => evaluate(true))
2065
+ ];
2066
+ evaluate(false);
2067
+ return {
2068
+ wrapper,
2069
+ detach: () => {
2070
+ offs.forEach((off) => off());
2071
+ inFlight?.cancel();
2072
+ inFlight = null;
2073
+ if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
2074
+ },
2075
+ syncZ: (z) => {
2076
+ wrapper.style.zIndex = String(z);
2077
+ }
2078
+ };
2079
+ }
2080
+
2081
+ // src/core/features/scrim-stages.ts
2082
+ var matchesId = (def, activeId) => {
2083
+ if (def.for === void 0) return false;
2084
+ return Array.isArray(def.for) ? def.for.includes(activeId) : def.for === activeId;
2085
+ };
2086
+ var matchesRange = (def, progress) => {
2087
+ if (!def.forRange) return false;
2088
+ const [from, to] = def.forRange;
2089
+ return progress >= from && progress <= to;
2090
+ };
2091
+ function installScrimStages(deps, opts) {
2092
+ const doc = deps.host.ownerDocument;
2093
+ const entries = opts.stages.map((def) => {
2094
+ const wrapper = doc.createElement("div");
2095
+ wrapper.className = "bs-scrim-stage";
2096
+ const ws = wrapper.style;
2097
+ ws.position = "absolute";
2098
+ applyOverlayPosition(
2099
+ ws,
2100
+ deps.mode,
2101
+ def.position ?? opts.position ?? "center",
2102
+ def.inset ?? opts.inset ?? "16px"
2103
+ );
2104
+ ws.visibility = "hidden";
2105
+ ws.pointerEvents = "none";
2106
+ wrapper.appendChild(def.element);
2107
+ deps.host.appendChild(wrapper);
2108
+ return { def, wrapper, inFlight: null };
2109
+ });
2110
+ let active = null;
2111
+ const resolveActive = () => {
2112
+ const state = deps.getState();
2113
+ if (state.size === 0) {
2114
+ return entries.find((e) => matchesId(e.def, state.activeId)) ?? null;
2115
+ }
2116
+ return entries.find((e) => matchesId(e.def, state.activeId)) ?? entries.find((e) => matchesRange(e.def, state.progress)) ?? null;
2117
+ };
2118
+ const hide = (entry, animate) => {
2119
+ entry.inFlight?.cancel();
2120
+ entry.wrapper.style.pointerEvents = "none";
2121
+ if (!animate) {
2122
+ entry.inFlight = null;
2123
+ entry.wrapper.style.visibility = "hidden";
2124
+ return;
2125
+ }
2126
+ const handle = runAnchorTransition(
2127
+ entry.def.element,
2128
+ entry.def.animation ?? opts.animation,
2129
+ "exit"
2130
+ );
2131
+ entry.inFlight = handle;
2132
+ void handle.finished.then(() => {
2133
+ if (entry.inFlight === handle && active !== entry) {
2134
+ entry.wrapper.style.visibility = "hidden";
2135
+ }
2136
+ });
2137
+ };
2138
+ const show = (entry, animate) => {
2139
+ entry.inFlight?.cancel();
2140
+ entry.inFlight = null;
2141
+ entry.wrapper.style.visibility = "";
2142
+ entry.wrapper.style.pointerEvents = entry.def.interactive ?? opts.interactive ?? false ? "auto" : "none";
2143
+ if (animate) {
2144
+ entry.inFlight = runAnchorTransition(
2145
+ entry.def.element,
2146
+ entry.def.animation ?? opts.animation,
2147
+ "enter"
2148
+ );
2149
+ }
2150
+ };
2151
+ const update = (animate) => {
2152
+ if (deps.isDestroyed()) return;
2153
+ const next = resolveActive();
2154
+ if (next === active) return;
2155
+ const prev = active;
2156
+ active = next;
2157
+ if (prev) hide(prev, animate);
2158
+ if (next) show(next, animate);
2159
+ };
2160
+ const offs = [
2161
+ deps.on("snap", () => update(true)),
2162
+ deps.on("open", () => update(true)),
2163
+ deps.on("close", () => update(true))
2164
+ ];
2165
+ if (entries.some((e) => e.def.forRange)) {
2166
+ offs.push(deps.on("progress", () => update(true)));
2167
+ }
2168
+ update(false);
2169
+ return () => {
2170
+ offs.forEach((off) => off());
2171
+ for (const entry of entries) {
2172
+ entry.inFlight?.cancel();
2173
+ entry.inFlight = null;
2174
+ if (entry.wrapper.parentNode) {
2175
+ entry.wrapper.parentNode.removeChild(entry.wrapper);
2176
+ }
2177
+ }
2178
+ active = null;
2179
+ };
2180
+ }
2181
+
2028
2182
  // src/core/primitives/engine-options.ts
2029
2183
  var DEFAULT_FLICK_VELOCITY = 0.65;
2030
2184
  var DEFAULT_DRAG_THRESHOLD = 18;
@@ -2049,9 +2203,6 @@ function resolveEngineOptions(opts, extras) {
2049
2203
  scrimMode: opts.scrimMode,
2050
2204
  scrimColor: opts.scrimColor,
2051
2205
  scrimBlur: opts.scrimBlur,
2052
- // Auto-promote pointer-events when tap-to-close is on so the click
2053
- // listener actually receives events. Caller can still force `false`
2054
- // explicitly to opt out — `??` only kicks in for `undefined`.
2055
2206
  scrimInteractive: opts.scrimInteractive ?? (opts.scrimTapToClose ? true : void 0),
2056
2207
  scrimTapToClose: opts.scrimTapToClose,
2057
2208
  scrimPreset: opts.scrimPreset,
@@ -2072,10 +2223,6 @@ function resolveEngineOptions(opts, extras) {
2072
2223
  closeOnEscape: opts.closeOnEscape,
2073
2224
  lockBodyScroll: opts.lockBodyScroll,
2074
2225
  inertSiblings: opts.inertSiblings,
2075
- // Folded in from `extras` so the lifecycle bag is fully assembled at
2076
- // the call site — engine no longer needs a spread+override at
2077
- // controller construction. When `extras` is omitted, the field stays
2078
- // undefined and the controller uses its own default predicate.
2079
2226
  shouldApplyInertSiblings: extras?.shouldApplyInertSiblings
2080
2227
  }
2081
2228
  };
@@ -2110,6 +2257,7 @@ var BottomSheetEngine = class {
2110
2257
  __publicField(this, "scrollContainer");
2111
2258
  __publicField(this, "backdrop");
2112
2259
  __publicField(this, "screenComponent");
2260
+ __publicField(this, "scrimParent", null);
2113
2261
  __publicField(this, "mode");
2114
2262
  __publicField(this, "flickVelocity");
2115
2263
  __publicField(this, "dragThreshold");
@@ -2120,40 +2268,22 @@ var BottomSheetEngine = class {
2120
2268
  __publicField(this, "lifecycle");
2121
2269
  __publicField(this, "closeOnBack");
2122
2270
  __publicField(this, "routedTo");
2123
- // Engine retains `snapPointsRaw` for the "find a closed-id" query in
2124
- // close() (raw input, not resolved geometry). Resolver owns geometry.
2125
2271
  __publicField(this, "snapPointsRaw");
2126
2272
  __publicField(this, "snaps");
2127
2273
  __publicField(this, "activeId");
2128
2274
  __publicField(this, "size", 0);
2129
- // Constructed in attach(); a plugin's install() could read `state` before
2130
- // attach completes if the constructor throws partway through. State getter
2131
- // null-guards via `this.gesture?.isDragging ?? false`.
2132
2275
  __publicField(this, "gesture");
2133
2276
  __publicField(this, "rootEl", null);
2134
2277
  __publicField(this, "destroyed", false);
2135
- // In-flight ViewTransition handle. A second snapTo while a VT is in flight
2136
- // would otherwise let Chromium reject the second's promise and flicker —
2137
- // newCycle() calls skipTransition() to abort cleanly.
2138
2278
  __publicField(this, "currentViewTransition", null);
2139
- // Each cycle captures the signal at entry; when a newer cycle aborts the
2140
- // controller, the captured signal flips to `aborted=true` and post-await
2141
- // guards short-circuit (prevents stale snap/open/close emits).
2142
2279
  __publicField(this, "currentAbort", new AbortController());
2143
- // Sub-pixel `--bs-size` deltas and <1% `--bs-progress` deltas are not
2144
- // visually distinguishable; gating CSSOM writes drops 30-50% of writes
2145
- // during the spring's settle tail.
2146
2280
  __publicField(this, "sizeWriteSentinel", new WriteSentinel());
2147
2281
  __publicField(this, "progressWriteSentinel", new WriteSentinel());
2148
2282
  __publicField(this, "isTopSheet", true);
2149
- // Pooled payload — identity documented in SheetEventMap.progress JSDoc;
2150
- // consumers must not retain across frames.
2151
2283
  __publicField(this, "progressPayload", {
2152
2284
  value: 0,
2153
2285
  size: 0
2154
2286
  });
2155
- // Pooled buffer for `getDragContext()` — onMove calls this 60-120×/s.
2156
- // Inner `range` object identity is also stable (mutated in place).
2157
2287
  __publicField(this, "dragContextBuf", {
2158
2288
  size: 0,
2159
2289
  maxAxisSize: 0,
@@ -2164,13 +2294,13 @@ var BottomSheetEngine = class {
2164
2294
  __publicField(this, "persistKey");
2165
2295
  __publicField(this, "linkedSheets", []);
2166
2296
  __publicField(this, "scrollCache");
2167
- // Single LIFO stack for ALL destroy-only teardowns. Plugins (added last)
2168
- // tear down first before the infrastructure they depend on. Toggle-semantic
2169
- // listeners (focus-trap / scroll-lock / scrim tap-to-close) stay as named
2170
- // fields on their controllers.
2171
2297
  __publicField(this, "teardowns", new TeardownStack());
2172
- // Pre-computed transform template — built once per engine since `mode` is
2173
- // fixed for its lifetime. Avoids the 4-arm switch at 60-120 Hz.
2298
+ __publicField(this, "anchors", []);
2299
+ __publicField(this, "anchorHost", null);
2300
+ __publicField(this, "stackZ", 100);
2301
+ __publicField(this, "detachScrimStages", null);
2302
+ __publicField(this, "stackEffectEnabled", false);
2303
+ __publicField(this, "stackEffectPrimed", false);
2174
2304
  __publicField(this, "transformTemplate");
2175
2305
  const resolved = resolveEngineOptions(opts, {
2176
2306
  shouldApplyInertSiblings: () => {
@@ -2185,7 +2315,8 @@ var BottomSheetEngine = class {
2185
2315
  this.handle = opts.handle ?? opts.element;
2186
2316
  this.scrollContainer = opts.scrollContainer;
2187
2317
  this.backdrop = opts.backdrop;
2188
- this.screenComponent = opts.screenComponent;
2318
+ this.screenComponent = opts.scrim ?? opts.screenComponent;
2319
+ this.scrimParent = this.screenComponent?.parentElement ?? null;
2189
2320
  this.mode = resolved.mode;
2190
2321
  this.transformTemplate = buildTransformTemplate(this.mode);
2191
2322
  this.aria = new AriaSliderWriter(this.handle, this.mode);
@@ -2203,6 +2334,9 @@ var BottomSheetEngine = class {
2203
2334
  getResolvedSnaps: () => this.snaps.getResolvedSnaps().slice(),
2204
2335
  snapTo: (id) => {
2205
2336
  void this.snapTo(id);
2337
+ },
2338
+ close: () => {
2339
+ void this.close();
2206
2340
  }
2207
2341
  },
2208
2342
  resolved.scrim
@@ -2213,8 +2347,6 @@ var BottomSheetEngine = class {
2213
2347
  getRootEl: () => this.rootEl,
2214
2348
  applySize: (size) => this.applySize(size),
2215
2349
  getSize: () => this.size,
2216
- // Optional chain protects synthetic test paths that drive animateTo
2217
- // before attach() (gesture is constructed there).
2218
2350
  isDragging: () => this.gesture?.isDragging ?? false
2219
2351
  },
2220
2352
  resolved.animation
@@ -2228,6 +2360,7 @@ var BottomSheetEngine = class {
2228
2360
  );
2229
2361
  this.closeOnBack = resolved.closeOnBack;
2230
2362
  this.routedTo = opts.routedTo;
2363
+ this.stackEffectEnabled = opts.stackEffect ?? false;
2231
2364
  this.snapPointsRaw = opts.snapPoints;
2232
2365
  this.persistKey = opts.persistKey;
2233
2366
  if (opts.linkedSheets) {
@@ -2240,7 +2373,7 @@ var BottomSheetEngine = class {
2240
2373
  this.snapPointsRaw,
2241
2374
  resolved.initialAllowed,
2242
2375
  this.mode,
2243
- () => this.handle.offsetHeight,
2376
+ () => layoutAxis(this.mode) === "width" ? this.handle.offsetWidth : this.handle.offsetHeight,
2244
2377
  (maxAxisSize) => {
2245
2378
  this.element.style[layoutAxis(this.mode)] = `${maxAxisSize}px`;
2246
2379
  }
@@ -2319,21 +2452,6 @@ var BottomSheetEngine = class {
2319
2452
  on(event, fn) {
2320
2453
  return this.bus.on(event, fn);
2321
2454
  }
2322
- /**
2323
- * Register a plugin. `install` runs synchronously; if it returns a function,
2324
- * it's captured for teardown during `destroy()`. No-op if already destroyed
2325
- * — installing into a torn-down engine would leak listeners.
2326
- *
2327
- * Throws from `plugin.install` are caught and rethrown via `queueMicrotask`
2328
- * so a buggy plugin can't take the engine down with it: the engine stays
2329
- * usable, sibling plugins still install, and the error surfaces in dev tools
2330
- * on the next tick instead of being swallowed. Mirrors the per-fn isolation
2331
- * model used for teardowns.
2332
- *
2333
- * Plugins that accept the second `scope` argument get transactional cleanup:
2334
- * push partial-install cleanups via `scope.add(fn)` and the engine drains
2335
- * them on throw, OR merges them into destroy-time teardowns on success.
2336
- */
2337
2455
  use(plugin) {
2338
2456
  if (this.destroyed) {
2339
2457
  console.warn(
@@ -2343,9 +2461,6 @@ var BottomSheetEngine = class {
2343
2461
  }
2344
2462
  const scoped = [];
2345
2463
  const scope = {
2346
- // typeof check — not just truthiness — so a JS-only plugin pushing a
2347
- // bogus value is silently rejected instead of queueing a confusing
2348
- // TypeError on destroy() drain.
2349
2464
  add: (fn) => {
2350
2465
  if (typeof fn === "function") scoped.push(fn);
2351
2466
  }
@@ -2374,36 +2489,13 @@ var BottomSheetEngine = class {
2374
2489
  }
2375
2490
  return this;
2376
2491
  }
2377
- /**
2378
- * Replace the linked-sheets list at runtime. Use when peer engines aren't
2379
- * available at construction time (mounted later in the tree). The wiring
2380
- * to the `open` event is independent of this list — it reads `linkedSheets`
2381
- * lazily on emit, so this setter is a simple field swap.
2382
- */
2383
2492
  setLinkedSheets(sheets) {
2384
2493
  if (this.destroyed) return;
2385
2494
  this.linkedSheets = sheets;
2386
2495
  }
2387
- /** Read-only view of the current allow-list, for cross-sheet target negotiation. */
2388
2496
  getAllowedIds() {
2389
2497
  return this.snaps.getAllowedIds().slice();
2390
2498
  }
2391
- /**
2392
- * Animate to a snap point by id. Resolves when the animation completes.
2393
- *
2394
- * Two call shapes:
2395
- * ```ts
2396
- * engine.snapTo("full"); // default velocity
2397
- * engine.snapTo("full", 0.5); // px/ms initial velocity
2398
- * engine.snapTo("full", { velocity: 0.5, signal }); // abortable
2399
- * ```
2400
- *
2401
- * When `opts.signal` is provided, calling `controller.abort()` cancels the
2402
- * in-flight tween/spring and the returned promise resolves without firing
2403
- * `snap`. The captured `AbortSignal` is checked after each await so external
2404
- * aborts ride the same cycle invariant as internal ones (resize, destroy,
2405
- * concurrent snapTo).
2406
- */
2407
2499
  async snapTo(id, velocityOrOpts = 0, _skipBeforeSnap = false) {
2408
2500
  if (this.destroyed) return;
2409
2501
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2462,16 +2554,6 @@ var BottomSheetEngine = class {
2462
2554
  this.handleClose();
2463
2555
  }
2464
2556
  }
2465
- /**
2466
- * Drive the sheet to an arbitrary px size, bypassing snap matching.
2467
- * Useful for preview scrubs, animation chaining, or programmatic gestures.
2468
- * Animates with the configured spring/tween. Resolves when the animation
2469
- * completes. Does NOT update `activeId` — call `snapTo(id)` after to commit.
2470
- *
2471
- * Unlike `snapTo`, this does not fire `before-snap` (it isn't a snap commit)
2472
- * nor `snap` (the sheet isn't settling to a known snap). `progress` events
2473
- * still fire via applySize. Target is clamped to `[0, maxAxisSize]`.
2474
- */
2475
2557
  async dragTo(targetSize, velocityOrOpts = 0) {
2476
2558
  if (this.destroyed) return;
2477
2559
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2501,28 +2583,13 @@ var BottomSheetEngine = class {
2501
2583
  }
2502
2584
  setAllowed(ids, snap) {
2503
2585
  if (this.destroyed) return;
2504
- const willSnap = snap !== void 0 && ids.includes(snap) || !ids.includes(this.activeId) && ids.length > 0;
2505
- if (!willSnap) {
2506
- this.animation.cancel();
2507
- this.newCycle();
2508
- }
2509
2586
  this.snaps.setAllowedIds(ids);
2510
2587
  this.updateAriaSlider();
2511
2588
  if (snap && ids.includes(snap)) {
2512
- void this.snapTo(
2513
- snap,
2514
- 0,
2515
- /* internal */
2516
- true
2517
- );
2589
+ void this.snapTo(snap, 0, true);
2518
2590
  } else if (!ids.includes(this.activeId)) {
2519
2591
  const fallback = ids[0];
2520
- if (fallback) void this.snapTo(
2521
- fallback,
2522
- 0,
2523
- /* internal */
2524
- true
2525
- );
2592
+ if (fallback) void this.snapTo(fallback, 0, true);
2526
2593
  }
2527
2594
  }
2528
2595
  setBackdropRange(range) {
@@ -2540,21 +2607,6 @@ var BottomSheetEngine = class {
2540
2607
  setScrimInteractive(interactive) {
2541
2608
  this.scrim.setScrimInteractive(interactive);
2542
2609
  }
2543
- /**
2544
- * Batch-update multiple scrim fields in one call. Apply order is:
2545
- * 1. `preset` — fills baseline color/blur/range/interactive.
2546
- * 2. `mode` — re-positions the scrim (fixed inset / clear / hide).
2547
- * 3. `enabled` — flips the opacity contribution on/off.
2548
- * 4. `tapToClose` — installs/tears the click listener.
2549
- * 5. `color` / `blur` / `interactive` / `range` — explicit overrides.
2550
- * 6. ONE `applySize` so the per-frame opacity stays consistent.
2551
- *
2552
- * Performance: collecting changes here (vs calling individual setters)
2553
- * avoids repeated `applySize` calls during a multi-field swap. Setters
2554
- * called here suppress their own internal `applySize` either by passing
2555
- * through fast-paths (mode/enabled no-op when value unchanged) or by
2556
- * being visual-only (color/blur/interactive don't influence opacity).
2557
- */
2558
2610
  setScrim(opts) {
2559
2611
  this.scrim.setScrim(opts, () => this.applySize(this.size));
2560
2612
  }
@@ -2570,11 +2622,79 @@ var BottomSheetEngine = class {
2570
2622
  setScrimOverlay(opts) {
2571
2623
  return this.scrim.setScrimOverlay(opts);
2572
2624
  }
2573
- /**
2574
- * Read-only snapshot of scrim state. Use the `setScrim*` setters to mutate.
2575
- * Stable public API — internals (e.g. cached opacity sentinels) are NOT
2576
- * exposed so consumers can't accidentally couple to ScrimController shape.
2577
- */
2625
+ addAnchor(opts) {
2626
+ if (this.destroyed || typeof document === "undefined") return () => {
2627
+ };
2628
+ if (!this.anchorHost) {
2629
+ this.anchorHost = this.rootEl ?? this.element.parentElement ?? document.body;
2630
+ this.anchorHost.style.setProperty("--bs-size", `${this.size}px`);
2631
+ this.anchorHost.style.setProperty(
2632
+ "--bs-progress",
2633
+ String(this.computeProgress(this.size))
2634
+ );
2635
+ }
2636
+ const handle = attachAnchor(
2637
+ {
2638
+ mode: this.mode,
2639
+ host: this.anchorHost,
2640
+ getState: () => ({
2641
+ activeId: this.activeId,
2642
+ size: this.size,
2643
+ progress: this.computeProgress(this.size)
2644
+ }),
2645
+ on: (event, fn) => this.on(event, fn),
2646
+ isDestroyed: () => this.destroyed
2647
+ },
2648
+ opts
2649
+ );
2650
+ handle.syncZ(this.stackZ + 1);
2651
+ this.anchors.push(handle);
2652
+ const detach = () => {
2653
+ const idx = this.anchors.indexOf(handle);
2654
+ if (idx === -1) return;
2655
+ this.anchors.splice(idx, 1);
2656
+ handle.detach();
2657
+ };
2658
+ this.teardowns.add(detach);
2659
+ return detach;
2660
+ }
2661
+ setScrimStages(opts) {
2662
+ if (this.destroyed || typeof document === "undefined") return () => {
2663
+ };
2664
+ this.detachScrimStages?.();
2665
+ this.detachScrimStages = null;
2666
+ if (!opts) return () => {
2667
+ };
2668
+ const host = this.screenComponent?.parentElement;
2669
+ if (!host) {
2670
+ console.warn(
2671
+ "[BottomSheet] setScrimStages: no scrim element mounted \u2014 pass `scrim` to the engine first."
2672
+ );
2673
+ return () => {
2674
+ };
2675
+ }
2676
+ const detachInstalled = installScrimStages(
2677
+ {
2678
+ mode: this.mode,
2679
+ host,
2680
+ getState: () => ({
2681
+ activeId: this.activeId,
2682
+ size: this.size,
2683
+ progress: this.computeProgress(this.size)
2684
+ }),
2685
+ on: (event, fn) => this.on(event, fn),
2686
+ isDestroyed: () => this.destroyed
2687
+ },
2688
+ opts
2689
+ );
2690
+ const detach = () => {
2691
+ if (this.detachScrimStages === detach) this.detachScrimStages = null;
2692
+ detachInstalled();
2693
+ };
2694
+ this.detachScrimStages = detach;
2695
+ this.teardowns.add(() => this.detachScrimStages?.());
2696
+ return detach;
2697
+ }
2578
2698
  getScrimState() {
2579
2699
  return { mode: this.scrim.scrimMode, enabled: this.scrim.scrimEnabled };
2580
2700
  }
@@ -2620,24 +2740,11 @@ var BottomSheetEngine = class {
2620
2740
  emit(event, payload) {
2621
2741
  this.bus.emit(event, payload);
2622
2742
  }
2623
- /**
2624
- * Abort the current cycle's signal and replace the controller. Returns the
2625
- * fresh signal so the caller can capture it locally before any await — when
2626
- * a future newCycle() / destroy aborts our controller, the captured signal
2627
- * flips to aborted and post-await guards short-circuit.
2628
- */
2629
2743
  newCycle() {
2630
2744
  this.currentAbort.abort();
2631
2745
  this.currentAbort = new AbortController();
2632
2746
  return this.currentAbort.signal;
2633
2747
  }
2634
- /**
2635
- * Fire `before-snap` and report whether a listener cancelled it. The
2636
- * `cancel()` callback exposed to listeners is frozen after the synchronous
2637
- * emit phase so async callers can't silently miss — they get a console
2638
- * warning. Returns `true` when cancelled so callers can early-return /
2639
- * spring-back without re-implementing the cancel/frozen pattern.
2640
- */
2641
2748
  emitBeforeSnap(target, previousId) {
2642
2749
  let cancelled = false;
2643
2750
  let frozen = false;
@@ -2671,59 +2778,54 @@ var BottomSheetEngine = class {
2671
2778
  this.snaps.setRaw(this.snapPointsRaw);
2672
2779
  this.scrim.invalidateOpacityCache();
2673
2780
  }
2674
- /**
2675
- * Memoised `allowedRange()` — delegates to SnapResolver which owns the cache.
2676
- * Engine wrapper kept so existing call sites don't all need updating;
2677
- * resolver-level memoization survives across method invocations.
2678
- */
2679
2781
  getAllowedRange() {
2680
2782
  return this.snaps.getAllowedRange();
2681
2783
  }
2682
- /**
2683
- * Write ARIA slider value attributes onto the handle so the engine itself
2684
- * is the source of truth for screen-reader announcements. Called from the
2685
- * constructor and from every code path that mutates `activeId` /
2686
- * `allowedIds`. WAI-ARIA 1.2 requires `aria-valuenow` for `role="slider"` —
2687
- * without it screen readers can't announce drag/snap progress to keyboard
2688
- * + AT users (WCAG 4.1.2 Name, Role, Value).
2689
- *
2690
- * Engine does NOT set `role="slider"` itself — that's the adapter's or
2691
- * consumer markup's responsibility. We only decorate with dynamic values.
2692
- *
2693
- * Empty allowed-list is malformed input (settable via `setAllowed([])`);
2694
- * short-circuit to remove ARIA value attributes so screen readers see a
2695
- * no-value slider rather than a degenerate `min=max=now=0` (WAI-ARIA 1.2
2696
- * requires `valuemax > valuemin` for `role="slider"`).
2697
- */
2784
+ allowedIdsBySize() {
2785
+ return this.snaps.getAllowedIds().slice().sort(
2786
+ (a, b) => (this.snaps.findById(a)?.size ?? 0) - (this.snaps.findById(b)?.size ?? 0)
2787
+ );
2788
+ }
2698
2789
  updateAriaSlider() {
2699
- this.aria.setValue(this.snaps.getAllowedIds(), this.activeId);
2790
+ this.aria.setValue(this.allowedIdsBySize(), this.activeId);
2700
2791
  }
2701
2792
  registerInStack() {
2702
2793
  this.teardowns.add(sheetStack.push({
2703
2794
  id: this.id,
2704
2795
  setZIndex: (z) => {
2796
+ this.stackZ = z;
2705
2797
  this.element.style.zIndex = String(z);
2706
2798
  if (this.backdrop) this.backdrop.style.zIndex = String(z - 1);
2799
+ for (const anchor of this.anchors) anchor.syncZ(z + 1);
2707
2800
  },
2708
2801
  setIsTop: (isTop) => {
2709
2802
  this.isTopSheet = isTop;
2710
2803
  if (this.backdrop) {
2711
2804
  this.backdrop.style.display = isTop ? "" : "none";
2712
2805
  }
2713
- }
2806
+ },
2807
+ isOpen: () => this.size > 0,
2808
+ setDepth: (depth) => this.applyStackDepth(depth)
2714
2809
  }));
2715
2810
  }
2811
+ applyStackDepth(depth) {
2812
+ if (!this.stackEffectEnabled) return;
2813
+ this.element.setAttribute("data-stack-depth", String(depth));
2814
+ if (!this.stackEffectPrimed) {
2815
+ this.stackEffectPrimed = true;
2816
+ const isVertical = this.mode === "bottom" || this.mode === "top";
2817
+ this.element.style.transformOrigin = isVertical ? "50% 100%" : "0% 50%";
2818
+ this.element.style.transition = "scale 320ms cubic-bezier(0.29, 1.04, 0.84, 0.99)";
2819
+ }
2820
+ const scale = Math.max(1 - depth * 0.04, 0.86);
2821
+ this.element.style.scale = scale === 1 ? "" : String(scale);
2822
+ }
2716
2823
  attach() {
2717
2824
  this.gesture = new GestureController({
2718
2825
  handle: this.handle,
2719
2826
  element: this.element,
2720
2827
  mode: this.mode,
2721
2828
  getRoot: () => this.rootEl,
2722
- // Single coalesced reader — narrows the DI surface and removes 3 getter
2723
- // dispatches per onMove frame at 60-120Hz. Returns a SHARED buffer
2724
- // (this.dragContextBuf) — see field JSDoc for the retention contract.
2725
- // Inner `range` object is mutated in place too so callers caching its
2726
- // identity between frames stay valid.
2727
2829
  getDragContext: () => {
2728
2830
  const buf = this.dragContextBuf;
2729
2831
  const r = this.getAllowedRange();
@@ -2777,7 +2879,7 @@ var BottomSheetEngine = class {
2777
2879
  container: this.scrollContainer,
2778
2880
  isDragging: () => this.gesture?.isDragging ?? false,
2779
2881
  isAnimating: () => this.animation.isAnimating,
2780
- getAllowedIds: () => this.snaps.getAllowedIds(),
2882
+ getAllowedIds: () => this.allowedIdsBySize(),
2781
2883
  getActiveId: () => this.activeId,
2782
2884
  snapTo: (id) => {
2783
2885
  void this.snapTo(id);
@@ -2806,11 +2908,13 @@ var BottomSheetEngine = class {
2806
2908
  },
2807
2909
  newCycle: () => {
2808
2910
  this.newCycle();
2809
- }
2911
+ },
2912
+ isAnimating: () => this.animation.isAnimating,
2913
+ resyncAfterCancel: () => this.resyncAfterResize()
2810
2914
  }));
2811
2915
  if (this.lifecycle.closeOnEscape) {
2812
2916
  const onKey = (e) => {
2813
- if (this.destroyed) return;
2917
+ if (this.destroyed || e.defaultPrevented) return;
2814
2918
  if (e.key === "Escape" && this.isTopSheet && this.size > 0) {
2815
2919
  void this.close();
2816
2920
  }
@@ -2842,7 +2946,7 @@ var BottomSheetEngine = class {
2842
2946
  handle: this.handle,
2843
2947
  mode: this.mode,
2844
2948
  isDestroyed: () => this.destroyed,
2845
- getAllowedIds: () => this.snaps.getAllowedIds(),
2949
+ getAllowedIds: () => this.allowedIdsBySize(),
2846
2950
  getActiveId: () => this.activeId,
2847
2951
  snapTo: (id) => {
2848
2952
  void this.snapTo(id);
@@ -2863,6 +2967,7 @@ var BottomSheetEngine = class {
2863
2967
  });
2864
2968
  if (!target) return;
2865
2969
  const previousId = this.activeId;
2970
+ if (target.id === previousId && target.size === this.size) return;
2866
2971
  if (this.emitBeforeSnap(target, previousId)) {
2867
2972
  const restore = this.snaps.findById(previousId);
2868
2973
  if (restore) {
@@ -2871,6 +2976,7 @@ var BottomSheetEngine = class {
2871
2976
  return;
2872
2977
  }
2873
2978
  const previousSize = this.size;
2979
+ const previousSnapSize = this.snaps.findById(previousId)?.size;
2874
2980
  this.scrollCache.cache(previousId, previousSize, target.size);
2875
2981
  this.activeId = target.id;
2876
2982
  void this.animation.animateTo(target.size, velocity).then(() => {
@@ -2879,9 +2985,10 @@ var BottomSheetEngine = class {
2879
2985
  this.updateAriaSlider();
2880
2986
  this.emit("snap", { id: target.id, size: target.size });
2881
2987
  this.haptic();
2882
- if (previousId === "closed" && target.size > 0) {
2988
+ if (previousSnapSize === 0 && target.size > 0) {
2883
2989
  this.emit("open", { id: target.id });
2884
2990
  this.handleOpen();
2991
+ notifyLinkedSheets(this.linkedSheets, this);
2885
2992
  }
2886
2993
  if (target.size === 0) {
2887
2994
  this.emit("close", void 0);
@@ -2889,7 +2996,6 @@ var BottomSheetEngine = class {
2889
2996
  }
2890
2997
  });
2891
2998
  }
2892
- /** Trigger a brief haptic tick (iOS Safari + Android Chrome support). */
2893
2999
  haptic() {
2894
3000
  if (typeof navigator !== "undefined" && "vibrate" in navigator) {
2895
3001
  try {
@@ -2905,6 +3011,12 @@ var BottomSheetEngine = class {
2905
3011
  style.transform = this.transformTemplate(offset);
2906
3012
  if (this.sizeWriteSentinel.shouldWrite(size, SIZE_WRITE_EPSILON)) {
2907
3013
  style.setProperty("--bs-size", `${size}px`);
3014
+ if (this.scrimParent) {
3015
+ this.scrimParent.style.setProperty("--bs-size", `${size}px`);
3016
+ }
3017
+ if (this.anchorHost && this.anchorHost !== this.scrimParent) {
3018
+ this.anchorHost.style.setProperty("--bs-size", `${size}px`);
3019
+ }
2908
3020
  }
2909
3021
  const progress = this.computeProgress(size);
2910
3022
  const progressChanged = this.progressWriteSentinel.shouldWrite(
@@ -2913,6 +3025,9 @@ var BottomSheetEngine = class {
2913
3025
  );
2914
3026
  if (progressChanged) {
2915
3027
  style.setProperty("--bs-progress", String(progress));
3028
+ if (this.anchorHost) {
3029
+ this.anchorHost.style.setProperty("--bs-progress", String(progress));
3030
+ }
2916
3031
  }
2917
3032
  this.scrim.applyOpacity(progress, progressChanged);
2918
3033
  if (progressChanged && this.bus.listenerCount("progress") > 0) {
@@ -2927,6 +3042,7 @@ var BottomSheetEngine = class {
2927
3042
  return Math.min(Math.max((size - min) / (max - min), 0), 1);
2928
3043
  }
2929
3044
  handleOpen() {
3045
+ sheetStack.promote(this.id);
2930
3046
  try {
2931
3047
  this.lifecycle.install();
2932
3048
  } catch (err) {
@@ -2940,8 +3056,24 @@ var BottomSheetEngine = class {
2940
3056
  });
2941
3057
  }
2942
3058
  }
3059
+ resyncAfterResize() {
3060
+ if (this.destroyed) return;
3061
+ const target = this.snaps.findById(this.activeId);
3062
+ if (!target) return;
3063
+ this.updateAriaSlider();
3064
+ this.emit("snap", { id: target.id, size: target.size });
3065
+ if (target.size > 0 && !this.lifecycle.isInstalled) {
3066
+ this.emit("open", { id: target.id });
3067
+ this.handleOpen();
3068
+ notifyLinkedSheets(this.linkedSheets, this);
3069
+ } else if (target.size === 0 && this.lifecycle.isInstalled) {
3070
+ this.emit("close", void 0);
3071
+ this.handleClose();
3072
+ }
3073
+ }
2943
3074
  handleClose() {
2944
3075
  this.lifecycle.release();
3076
+ sheetStack.update();
2945
3077
  }
2946
3078
  };
2947
3079
 
@@ -2974,7 +3106,7 @@ function useBottomSheet(opts) {
2974
3106
  handle: handleRef.value ?? void 0,
2975
3107
  scrollContainer: contentRef.value ?? void 0,
2976
3108
  backdrop: backdropRef.value ?? void 0,
2977
- screenComponent: screenRef.value ?? void 0
3109
+ scrim: screenRef.value ?? void 0
2978
3110
  });
2979
3111
  sync();
2980
3112
  engine.on("snap", (payload) => {
@@ -3007,6 +3139,13 @@ function useBottomSheet(opts) {
3007
3139
  close: () => engine?.close() ?? Promise.resolve(),
3008
3140
  setAllowed: (ids, snap) => engine?.setAllowed(ids, snap),
3009
3141
  setSnapPoints: (points, allowed) => engine?.setSnapPoints(points, allowed),
3142
+ setScrim: (opts2) => engine?.setScrim(opts2),
3143
+ setScrimOverlay: (opts2) => engine?.setScrimOverlay(opts2) ?? (() => {
3144
+ }),
3145
+ addAnchor: (opts2) => engine?.addAnchor(opts2) ?? (() => {
3146
+ }),
3147
+ setScrimStages: (opts2) => engine?.setScrimStages(opts2) ?? (() => {
3148
+ }),
3010
3149
  on: (event, fn) => {
3011
3150
  if (engine) return engine.on(event, fn);
3012
3151
  const entry = {
@@ -3024,7 +3163,8 @@ function useBottomSheet(opts) {
3024
3163
  if (idx !== -1) pending.splice(idx, 1);
3025
3164
  }
3026
3165
  };
3027
- }
3166
+ },
3167
+ getEngine: () => engine
3028
3168
  };
3029
3169
  }
3030
3170
 
@@ -3041,6 +3181,10 @@ var export_helper_default = (sfc, props) => {
3041
3181
  var _sfc_main = /* @__PURE__ */ defineComponent({
3042
3182
  __name: "BottomSheet",
3043
3183
  props: {
3184
+ screenComponent: {
3185
+ type: null,
3186
+ required: false
3187
+ },
3044
3188
  mode: {
3045
3189
  type: String,
3046
3190
  required: false
@@ -3153,6 +3297,10 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
3153
3297
  type: Boolean,
3154
3298
  required: false
3155
3299
  },
3300
+ stackEffect: {
3301
+ type: Boolean,
3302
+ required: false
3303
+ },
3156
3304
  snapPoints: {
3157
3305
  type: Array,
3158
3306
  required: true
@@ -3194,7 +3342,7 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
3194
3342
  setup(__props, { expose: __expose, emit: __emit }) {
3195
3343
  const props = __props;
3196
3344
  const emit = __emit;
3197
- const { sheetRef, handleRef, contentRef, backdropRef, screenRef, state, snapTo, open, close, setAllowed, setSnapPoints, on } = useBottomSheet(props);
3345
+ const { sheetRef, handleRef, contentRef, backdropRef, screenRef, state, snapTo, open, close, setAllowed, setSnapPoints, setScrim, setScrimOverlay, on, getEngine } = useBottomSheet(props);
3198
3346
  watch(() => ({ ...state }), (next) => emit("change", next), { deep: false });
3199
3347
  on("snap", (payload) => emit("snap", payload.id));
3200
3348
  on("open", (payload) => emit("open", payload.id));
@@ -3215,10 +3363,10 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
3215
3363
  close,
3216
3364
  setAllowed,
3217
3365
  setSnapPoints,
3366
+ setScrim,
3367
+ setScrimOverlay,
3218
3368
  on,
3219
- // readonly() prevents external write-through (e.g. `bsRef.value.state.size = 999`)
3220
- // that would desync the local view-state proxy from the engine's truth.
3221
- // Mirrors React's getter-based exposure and Svelte's getState() snapshot.
3369
+ getEngine,
3222
3370
  state: readonly(state)
3223
3371
  });
3224
3372
  const __returned__ = {
@@ -3235,7 +3383,10 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
3235
3383
  close,
3236
3384
  setAllowed,
3237
3385
  setSnapPoints,
3386
+ setScrim,
3387
+ setScrimOverlay,
3238
3388
  on,
3389
+ getEngine,
3239
3390
  isVerticalAxis,
3240
3391
  allowedIds,
3241
3392
  activeIdx,
@@ -3250,7 +3401,6 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
3250
3401
  });
3251
3402
  var _hoisted_1 = { class: "bs-root" };
3252
3403
  var _hoisted_2 = {
3253
- key: 1,
3254
3404
  ref: "screenRef",
3255
3405
  class: "bs-screen"
3256
3406
  };
@@ -3304,13 +3454,14 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
3304
3454
  512
3305
3455
  /* NEED_PATCH */
3306
3456
  )) : createCommentVNode("v-if", true),
3307
- _ctx.$slots.screen ? (openBlock(), createElementBlock(
3457
+ createCommentVNode("\n Scrim surface always renders so the engine can host scrim presets\n (color/blur via setScrim), overlay injections (setScrimOverlay),\n and progress-driven opacity. The `screen` slot, when used, renders\n inside as user content. Without a slot it stays an empty positioning\n context.\n "),
3458
+ createElementVNode(
3308
3459
  "div",
3309
3460
  _hoisted_2,
3310
3461
  [renderSlot(_ctx.$slots, "screen")],
3311
3462
  512
3312
3463
  /* NEED_PATCH */
3313
- )) : createCommentVNode("v-if", true),
3464
+ ),
3314
3465
  createElementVNode("section", {
3315
3466
  ref: "sheetRef",
3316
3467
  class: normalizeClass(["bs-sheet"]),
@@ -3351,7 +3502,7 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
3351
3502
  ], 8, _hoisted_3)
3352
3503
  ]);
3353
3504
  }
3354
- var BottomSheet_default = /* @__PURE__ */ export_helper_default(_sfc_main, [["render", _sfc_render], ["__file", "/Users/maksimkravcov/Desktop/\u043F\u0440\u043E\u0435\u043A\u0442\u044B/BottomSheet/src/vue/BottomSheet.vue"]]);
3505
+ var BottomSheet_default = /* @__PURE__ */ export_helper_default(_sfc_main, [["render", _sfc_render], ["__file", "/home/runner/work/BottomSheet/BottomSheet/src/vue/BottomSheet.vue"]]);
3355
3506
 
3356
3507
  export { BottomSheet_default as BottomSheet, useBottomSheet };
3357
3508
  //# sourceMappingURL=vue.js.map