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