@surdeddd/bottom-sheet 0.1.0 → 0.2.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 (50) hide show
  1. package/README.md +6 -0
  2. package/dist/BottomSheetEngine-CIT9Htti.d.cts +219 -0
  3. package/dist/BottomSheetEngine-Df9MwBbm.d.ts +219 -0
  4. package/dist/element.cjs +95 -440
  5. package/dist/element.d.cts +3 -56
  6. package/dist/element.d.ts +3 -56
  7. package/dist/element.iife.global.js +2 -2
  8. package/dist/element.js +95 -440
  9. package/dist/index.cjs +84 -494
  10. package/dist/index.d.cts +3 -84
  11. package/dist/index.d.ts +3 -84
  12. package/dist/index.js +84 -494
  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 +0 -87
  18. package/dist/overlay.d.cts +1 -137
  19. package/dist/overlay.d.ts +1 -137
  20. package/dist/overlay.js +0 -87
  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 +87 -439
  25. package/dist/qwik.d.cts +3 -24
  26. package/dist/qwik.d.ts +3 -24
  27. package/dist/qwik.js +87 -439
  28. package/dist/react.cjs +91 -496
  29. package/dist/react.d.cts +16 -158
  30. package/dist/react.d.ts +16 -158
  31. package/dist/react.js +91 -496
  32. package/dist/solid.cjs +87 -439
  33. package/dist/solid.d.cts +4 -31
  34. package/dist/solid.d.ts +4 -31
  35. package/dist/solid.js +87 -439
  36. package/dist/styles.css +27 -102
  37. package/dist/svelte.cjs +139 -462
  38. package/dist/svelte.d.ts +9 -2
  39. package/dist/svelte.js +139 -462
  40. package/dist/themes/ios.css +0 -3
  41. package/dist/themes/material.css +0 -3
  42. package/dist/themes/vercel.css +0 -3
  43. package/dist/{transform-Brkf-wzo.d.cts → transform-jVfAikE9.d.cts} +0 -1
  44. package/dist/{transform-Brkf-wzo.d.ts → transform-jVfAikE9.d.ts} +0 -1
  45. package/dist/vue.cjs +104 -446
  46. package/dist/vue.d.ts +7 -1
  47. package/dist/vue.js +105 -447
  48. package/package.json +3 -3
  49. package/dist/BottomSheetEngine-CvSVbo6c.d.cts +0 -704
  50. package/dist/BottomSheetEngine-DbO8rW_3.d.ts +0 -704
package/dist/react.js CHANGED
@@ -237,11 +237,6 @@ var SnapResolver = class {
237
237
  this.allowedIds = allowed ?? raw.map((p) => p.id);
238
238
  this.recompute();
239
239
  }
240
- /**
241
- * Re-resolve raw → resolved against the current viewport / fit-measure,
242
- * recompute maxAxisSize, invalidate range cache, notify engine. Called on
243
- * resize, orientationchange, visualViewport changes, and snap-list swaps.
244
- */
245
240
  recompute() {
246
241
  this.resolved = resolveSnapList(this.raw, this.mode, this.measureFit);
247
242
  this.maxAxisSize = this.resolved.reduce(
@@ -251,15 +246,10 @@ var SnapResolver = class {
251
246
  this.rangeCache = null;
252
247
  this.onMaxAxisSizeChange?.(this.maxAxisSize);
253
248
  }
254
- /** Replace the raw snap list and recompute. Engine calls on `setSnapPoints`. */
255
249
  setRaw(raw) {
256
250
  this.raw = raw;
257
251
  this.recompute();
258
252
  }
259
- /**
260
- * Replace the allow-list. Range cache depends on `(resolvedSnaps ×
261
- * allowedIds)` so flipping ids alone invalidates without a full recompute.
262
- */
263
253
  setAllowedIds(ids) {
264
254
  this.allowedIds = ids;
265
255
  this.rangeCache = null;
@@ -267,11 +257,6 @@ var SnapResolver = class {
267
257
  findById(id) {
268
258
  return findById(id, this.resolved);
269
259
  }
270
- /**
271
- * Pure drag-settle target resolution. Engine passes the gesture-specific
272
- * fields (delta, velocity, pointerKind, flickVelocity, dragThreshold) plus
273
- * `size`/`activeId` since those are still engine-owned.
274
- */
275
260
  findDragSettleTarget(args) {
276
261
  return findDragSettleTarget({
277
262
  ...args,
@@ -280,18 +265,9 @@ var SnapResolver = class {
280
265
  maxAxisSize: this.maxAxisSize
281
266
  });
282
267
  }
283
- /**
284
- * Memoised allowed-range query. Twice-per-drag-frame (gesture onMove +
285
- * applySize→computeProgress); cache collapses 2× iteration of resolvedSnaps
286
- * to one recompute per geometry/allow-list change.
287
- */
288
268
  getAllowedRange() {
289
269
  return this.rangeCache ?? (this.rangeCache = allowedRange(this.resolved, this.allowedIds));
290
270
  }
291
- /**
292
- * Read-only view of resolved snaps. Returned as `readonly` to discourage
293
- * accidental mutation by feature install sites that close over this list.
294
- */
295
271
  getResolvedSnaps() {
296
272
  return this.resolved;
297
273
  }
@@ -301,18 +277,6 @@ var SnapResolver = class {
301
277
  getMaxAxisSize() {
302
278
  return this.maxAxisSize;
303
279
  }
304
- /**
305
- * External clamp override. The resize-observer feature compares
306
- * `getMaxAxisSize()` against the live viewport and shrinks if the largest
307
- * resolved snap exceeds it (mobile address-bar collapse / soft-keyboard
308
- * window resize). The next `recompute()` overwrites this clamp from the
309
- * resolved-snap reduction.
310
- *
311
- * Does NOT invalidate the range cache — viewport clamping doesn't change
312
- * `(resolvedSnaps × allowedIds) → range`. DOES fire `onMaxAxisSizeChange`
313
- * so the resolver owns the layout-axis style write end-to-end (symmetric
314
- * with `recompute()` — feature call sites no longer duplicate the write).
315
- */
316
280
  setMaxAxisSize(n) {
317
281
  this.maxAxisSize = n;
318
282
  this.onMaxAxisSizeChange?.(n);
@@ -687,73 +651,49 @@ var WriteSentinel = class {
687
651
  constructor() {
688
652
  __publicField(this, "last", -1);
689
653
  }
690
- /**
691
- * Returns `true` (and updates the cached value) if `next` differs from the
692
- * cached `last` by more than `epsilon`. Returns `false` (and leaves the
693
- * cache untouched) when the delta is within epsilon — caller skips the
694
- * CSSOM write.
695
- */
696
654
  shouldWrite(next, epsilon) {
697
655
  if (Math.abs(next - this.last) <= epsilon) return false;
698
656
  this.last = next;
699
657
  return true;
700
658
  }
701
- /**
702
- * Force the next `shouldWrite` to pass. Resets the cache to `-1` so any
703
- * non-negative `next` exceeds the epsilon comparison. Called from setters
704
- * that mutate state under the cache (range/mode/enabled flips that change
705
- * the progress→opacity mapping but don't move `progress` itself).
706
- */
707
659
  invalidate() {
708
660
  this.last = -1;
709
661
  }
710
- /**
711
- * Record `value` as the last-written without performing a write. Used
712
- * when a setter writes a literal value through a different path (e.g.
713
- * `setScrimMode("off")` writes `opacity: "0"` inline) and wants future
714
- * `shouldWrite` calls to short-circuit against that known cached state.
715
- */
716
662
  setLastWritten(value) {
717
663
  this.last = value;
718
664
  }
719
- /** Read-only accessor for diagnostic / dedup-aware peek. */
720
665
  get value() {
721
666
  return this.last;
722
667
  }
723
668
  };
724
669
 
670
+ // src/core/primitives/devWarn.ts
671
+ function devWarn(...args) {
672
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV !== "production" && typeof console !== "undefined") {
673
+ console.warn(...args);
674
+ }
675
+ }
676
+
725
677
  // src/core/controllers/scrim-controller.ts
678
+ var VALID_CSS_LENGTH = /^-?\d+(?:\.\d+)?(?:px|em|rem|%)$/;
726
679
  var ScrimController = class {
727
680
  constructor(deps, opts) {
728
- // ── DOM refs (mirror engine's; cached for hot path) ─────────────────
729
681
  __publicField(this, "screenComponent");
730
682
  __publicField(this, "backdrop");
731
683
  __publicField(this, "mode");
732
- // ── Engine callbacks ────────────────────────────────────────────────
733
684
  __publicField(this, "isDestroyed");
734
685
  __publicField(this, "isTopSheet");
735
686
  __publicField(this, "getAllowedIds");
736
687
  __publicField(this, "getResolvedSnaps");
737
688
  __publicField(this, "snapToFn");
738
- // ── Mode + state ────────────────────────────────────────────────────
739
689
  __publicField(this, "scrimMode");
740
690
  __publicField(this, "backdropRange");
741
691
  __publicField(this, "screenRange");
742
- // setScrimEnabled toggle — gated through `scrimEnabled` rather than nuking
743
- // ranges so re-enable can restore prior values without a remount. Public
744
- // so tests / introspection can read it without an `as any` cast.
745
692
  __publicField(this, "scrimEnabled", true);
746
693
  __publicField(this, "savedScrimRanges", null);
747
- // ── Tap-to-close ────────────────────────────────────────────────────
748
694
  __publicField(this, "scrimTapToCloseEnabled", false);
749
695
  __publicField(this, "detachScrimTap", null);
750
- // ── Scrim overlay slot (positioned floating element) ────────────────
751
696
  __publicField(this, "scrimOverlayTeardown", null);
752
- // ── Hot-path opacity dedup caches (per-frame writes) ────────────────
753
- // Numeric sentinels migrated to WriteSentinel primitive — single
754
- // invalidation surface, shared with the engine's size/progress sentinels.
755
- // The two string-typed sentinels (`Pointer`, `Display`) keep their inline
756
- // pattern: equality on string union, no epsilon, different reset semantics.
757
697
  __publicField(this, "backdropOpacitySentinel", new WriteSentinel());
758
698
  __publicField(this, "screenOpacitySentinel", new WriteSentinel());
759
699
  __publicField(this, "lastBackdropPointer", "");
@@ -788,41 +728,15 @@ var ScrimController = class {
788
728
  }
789
729
  }
790
730
  }
791
- /**
792
- * Invalidate the opacity dedup caches. Called from any setter that mutates
793
- * range/mode/enabled — opacity is a pure function of (progress, range,
794
- * enabled), so a config change makes the cached values stale even if
795
- * `progress` itself didn't move. Next `applyOpacity(progress, false)` call
796
- * sees the -1 sentinel and re-runs the math instead of short-circuiting.
797
- *
798
- * Public for the engine: `setSnapPoints` mutates resolved geometry which
799
- * shifts `progress→opacity` mapping under the cache; engine calls this to
800
- * keep the next render frame correct on a static sheet.
801
- * @internal
802
- */
803
731
  invalidateOpacityCache() {
804
732
  this.backdropOpacitySentinel.invalidate();
805
733
  this.screenOpacitySentinel.invalidate();
806
734
  }
807
- /** Engine calls this when its `attach()` runs — installs scrim-tap-click if requested. */
808
735
  attach() {
809
736
  if (this.scrimTapToCloseEnabled) {
810
737
  this.setScrimTapToClose(true);
811
738
  }
812
739
  }
813
- // ── Hot-path: opacity computation called per-frame from engine.applySize ──
814
- /**
815
- * Per-frame opacity writer. Computes backdrop + screen opacity from the
816
- * sheet's progress (0..1 within allowed range), gated by sub-pixel dedup
817
- * caches so steady-state frames don't trigger style recalc.
818
- *
819
- * `progressChanged` is the engine's already-computed dedup gate — when
820
- * `false`, opacity is by construction a pure function of `progress` so
821
- * cached values are still valid; we short-circuit the Math.min/max +
822
- * division work entirely. Setters that mutate ranges/mode/enabled
823
- * invalidate the cache by writing -1 sentinels so the next call re-runs
824
- * the math.
825
- */
826
740
  applyOpacity(progress, progressChanged) {
827
741
  if (!progressChanged && this.backdropOpacitySentinel.value !== -1 && this.screenOpacitySentinel.value !== -1) {
828
742
  return;
@@ -868,80 +782,30 @@ var ScrimController = class {
868
782
  }
869
783
  }
870
784
  }
871
- // ── Public setters (engine forwards these 1:1) ─────────────────────
872
- /**
873
- * Replace the [start, end] progress window in which the backdrop fades 0→1.
874
- * @param range - Tuple of two progress values in 0..1 (start <= end).
875
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
876
- * @example
877
- * // Backdrop only starts darkening after the sheet crosses 30% open.
878
- * engine.setBackdropRange([0.3, 1]);
879
- */
880
785
  setBackdropRange(range, applySize) {
881
786
  if (this.isDestroyed()) return;
882
787
  this.backdropRange = range;
883
788
  this.invalidateOpacityCache();
884
789
  applySize();
885
790
  }
886
- /**
887
- * Replace the [start, end] progress window in which the screen scrim fades 0→1.
888
- * @param range - Tuple of two progress values in 0..1 (start <= end).
889
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
890
- * @example
891
- * // Scrim is fully opaque only when the sheet is past 50% — softer feel.
892
- * engine.setScreenRange([0, 0.5]);
893
- */
894
791
  setScreenRange(range, applySize) {
895
792
  if (this.isDestroyed()) return;
896
793
  this.screenRange = range;
897
794
  this.invalidateOpacityCache();
898
795
  applySize();
899
796
  }
900
- /**
901
- * Set the scrim background color. Pass `null`/`undefined` to clear the
902
- * inline write and let consumer CSS take over.
903
- * @param color - Any valid CSS color string, or `null`/`undefined` to clear.
904
- * @example
905
- * engine.setScrimColor("rgba(0, 0, 0, 0.6)");
906
- * engine.setScrimColor(null); // hand back to consumer CSS
907
- */
908
797
  setScrimColor(color) {
909
798
  if (this.isDestroyed()) return;
910
799
  this.applyScrimStyles({ color: color === void 0 ? null : color });
911
800
  }
912
- /**
913
- * Set the scrim backdrop-filter blur radius. Pass `null`/`undefined` to clear.
914
- * @param blur - CSS length (e.g. `"8px"`) consumed as `blur(<value>)`, or
915
- * `null`/`undefined` to remove the filter.
916
- * @example
917
- * engine.setScrimBlur("12px"); // frosted-glass effect
918
- * engine.setScrimBlur(null); // disable blur
919
- */
920
801
  setScrimBlur(blur) {
921
802
  if (this.isDestroyed()) return;
922
803
  this.applyScrimStyles({ blur: blur === void 0 ? null : blur });
923
804
  }
924
- /**
925
- * Toggle whether the scrim layer captures pointer events.
926
- * @param interactive - `true` → `pointer-events: auto`; `false` → `none`.
927
- * @example
928
- * engine.setScrimInteractive(true); // scrim swallows clicks
929
- * engine.setScrimInteractive(false); // clicks pass through to content below
930
- */
931
805
  setScrimInteractive(interactive) {
932
806
  if (this.isDestroyed()) return;
933
807
  this.applyScrimStyles({ interactive });
934
808
  }
935
- /**
936
- * Switch scrim positioning at runtime.
937
- * @param mode - `"full"` (consumer CSS), `"above-sheet"` (engine-pinned inset
938
- * so only the area NOT covered by the sheet is dimmed), or
939
- * `"off"` (display:none + opacity:0 — no remount needed).
940
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
941
- * @example
942
- * engine.setScrimMode("above-sheet"); // dim only the area above the sheet
943
- * engine.setScrimMode("off"); // disable scrim layer
944
- */
945
809
  setScrimMode(mode, applySize) {
946
810
  if (this.isDestroyed()) return;
947
811
  if (mode === this.scrimMode) return;
@@ -971,15 +835,6 @@ var ScrimController = class {
971
835
  }
972
836
  applySize();
973
837
  }
974
- /**
975
- * Enable or disable click-on-scrim → snap to first non-zero allowed snap.
976
- * Auto-promotes `pointer-events: auto` when scrim is non-interactive so the
977
- * click listener actually fires.
978
- * @param enabled - `true` installs the click listener; `false` removes it.
979
- * @example
980
- * engine.setScrimTapToClose(true); // tap outside sheet collapses it
981
- * engine.setScrimTapToClose(false); // require explicit close button
982
- */
983
838
  setScrimTapToClose(enabled) {
984
839
  if (this.isDestroyed()) return;
985
840
  if (!this.screenComponent) return;
@@ -1009,16 +864,6 @@ var ScrimController = class {
1009
864
  this.scrimTapToCloseEnabled = false;
1010
865
  }
1011
866
  }
1012
- /**
1013
- * Toggle the scrim layer without remounting. Disabling stashes current
1014
- * `screenRange`/`backdropRange` and degenerates them to `[1, 1]` (opacity
1015
- * formula → 0 across the whole range); re-enabling restores the saved values.
1016
- * @param enabled - `false` hides scrim, `true` restores prior ranges.
1017
- * @param applySize - Engine-supplied recompute hook; runs one applyOpacity pass.
1018
- * @example
1019
- * engine.setScrimEnabled(false); // hide scrim, ranges remembered
1020
- * engine.setScrimEnabled(true); // restore prior ranges (no remount)
1021
- */
1022
867
  setScrimEnabled(enabled, applySize) {
1023
868
  if (this.isDestroyed()) return;
1024
869
  if (enabled === this.scrimEnabled) return;
@@ -1037,20 +882,6 @@ var ScrimController = class {
1037
882
  }
1038
883
  applySize();
1039
884
  }
1040
- /**
1041
- * Batch-update multiple scrim fields with a single applySize() at the end.
1042
- * If `preset` is set, it seeds color/blur/interactive/range first; explicit
1043
- * fields override.
1044
- * @param opts - Partial scrim config: preset, mode, enabled, tapToClose,
1045
- * color, blur, interactive, range.
1046
- * @param applySize - Engine-supplied recompute hook; runs ONCE per call.
1047
- * @example
1048
- * engine.setScrim({
1049
- * preset: "frosted",
1050
- * mode: "above-sheet",
1051
- * tapToClose: true,
1052
- * });
1053
- */
1054
885
  setScrim(opts, applySize) {
1055
886
  if (this.isDestroyed()) return;
1056
887
  if (opts.preset) {
@@ -1073,27 +904,18 @@ var ScrimController = class {
1073
904
  interactive: opts.interactive
1074
905
  });
1075
906
  if (opts.range !== void 0) {
1076
- this.screenRange = opts.range;
907
+ if (!this.scrimEnabled && this.savedScrimRanges) {
908
+ this.savedScrimRanges = {
909
+ screen: opts.range,
910
+ backdrop: this.savedScrimRanges.backdrop
911
+ };
912
+ } else {
913
+ this.screenRange = opts.range;
914
+ }
1077
915
  }
1078
916
  this.invalidateOpacityCache();
1079
917
  applySize();
1080
918
  }
1081
- /**
1082
- * Inject a positioned floating element into the scrim layer (e.g. a close
1083
- * button anchored top-right). Replaces any prior overlay so repeated calls
1084
- * (reactive effects re-running) don't accumulate wrappers.
1085
- * @param opts - Children element, optional `inset` (default `"16px"`),
1086
- * `position` (9-way grid, default `"top-right"`), and
1087
- * `interactive` flag (default `true`).
1088
- * @returns Teardown function that removes the wrapper from the DOM.
1089
- * @example
1090
- * const teardown = engine.setScrimOverlay({
1091
- * children: closeBtn,
1092
- * position: "top-right",
1093
- * inset: "20px",
1094
- * });
1095
- * // later: teardown(); // remove the floating button
1096
- */
1097
919
  setScrimOverlay(opts) {
1098
920
  if (this.isDestroyed()) return () => {
1099
921
  };
@@ -1101,7 +923,18 @@ var ScrimController = class {
1101
923
  };
1102
924
  this.scrimOverlayTeardown?.();
1103
925
  const screen = this.screenComponent;
1104
- const inset = opts.inset ?? "16px";
926
+ const insetRaw = opts.inset ?? "16px";
927
+ const inset = VALID_CSS_LENGTH.test(insetRaw) ? insetRaw : "16px";
928
+ if (inset !== insetRaw) {
929
+ devWarn(
930
+ `[bottom-sheet] setScrimOverlay: invalid inset ${JSON.stringify(insetRaw)}; expected a CSS length like "16px" / "1rem" / "5%". Falling back to "16px".`
931
+ );
932
+ }
933
+ if (opts.children.ownerDocument && opts.children.ownerDocument !== screen.ownerDocument) {
934
+ devWarn(
935
+ "[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."
936
+ );
937
+ }
1105
938
  const position = opts.position ?? "top-right";
1106
939
  const interactive = opts.interactive ?? true;
1107
940
  const wrapper = screen.ownerDocument.createElement("div");
@@ -1150,12 +983,30 @@ var ScrimController = class {
1150
983
  ws.bottom = inset;
1151
984
  ws.right = inset;
1152
985
  break;
986
+ case "sheet-top-left":
987
+ case "sheet-top-center":
988
+ case "sheet-top-right":
989
+ applySheetAnchoredPosition(
990
+ ws,
991
+ this.mode,
992
+ position,
993
+ inset
994
+ );
995
+ break;
1153
996
  }
1154
997
  if (interactive) {
1155
998
  ws.pointerEvents = "auto";
1156
999
  }
1157
1000
  wrapper.appendChild(opts.children);
1158
- screen.appendChild(wrapper);
1001
+ const host = screen.parentElement;
1002
+ if (!host) {
1003
+ devWarn(
1004
+ "[bottom-sheet] setScrimOverlay: scrim element has no parent, skipping overlay injection. Mount the sheet root before calling setScrimOverlay."
1005
+ );
1006
+ return () => {
1007
+ };
1008
+ }
1009
+ host.appendChild(wrapper);
1159
1010
  const teardown = () => {
1160
1011
  if (wrapper.parentNode) wrapper.parentNode.removeChild(wrapper);
1161
1012
  if (this.scrimOverlayTeardown === teardown) {
@@ -1165,7 +1016,6 @@ var ScrimController = class {
1165
1016
  this.scrimOverlayTeardown = teardown;
1166
1017
  return teardown;
1167
1018
  }
1168
- /** Tear down toggle-state listeners + remove any injected overlay wrapper. */
1169
1019
  destroy() {
1170
1020
  this.detachScrimTap?.();
1171
1021
  this.detachScrimTap = null;
@@ -1184,8 +1034,6 @@ var ScrimController = class {
1184
1034
  this.lastBackdropPointer = "";
1185
1035
  this.lastScreenDisplay = "";
1186
1036
  }
1187
- // ── Private helpers ────────────────────────────────────────────────
1188
- /** Centralized scrim style writer — atomic 3-property write across all setters. */
1189
1037
  applyScrimStyles(opts) {
1190
1038
  if (!this.screenComponent) return;
1191
1039
  const s = this.screenComponent.style;
@@ -1207,7 +1055,6 @@ var ScrimController = class {
1207
1055
  s.pointerEvents = opts.interactive ? "auto" : "none";
1208
1056
  }
1209
1057
  }
1210
- /** Pin the screen overlay to the area NOT occupied by the sheet. */
1211
1058
  applyAboveSheetInset() {
1212
1059
  if (!this.screenComponent) return;
1213
1060
  const insetByMode = {
@@ -1222,6 +1069,35 @@ var ScrimController = class {
1222
1069
  s.pointerEvents = "none";
1223
1070
  }
1224
1071
  };
1072
+ function resolveSheetAnchoredStyle(mode, position, inset) {
1073
+ const sizeOffset = `calc(var(--bs-size, 0px) + ${inset})`;
1074
+ switch (mode) {
1075
+ case "bottom":
1076
+ if (position === "sheet-top-left") return { bottom: sizeOffset, left: inset };
1077
+ if (position === "sheet-top-right") return { bottom: sizeOffset, right: inset };
1078
+ return { bottom: sizeOffset, left: "50%", transform: "translateX(-50%)" };
1079
+ case "top":
1080
+ if (position === "sheet-top-left") return { top: sizeOffset, left: inset };
1081
+ if (position === "sheet-top-right") return { top: sizeOffset, right: inset };
1082
+ return { top: sizeOffset, left: "50%", transform: "translateX(-50%)" };
1083
+ case "left":
1084
+ if (position === "sheet-top-left") return { left: sizeOffset, top: inset };
1085
+ if (position === "sheet-top-right") return { left: sizeOffset, bottom: inset };
1086
+ return { left: sizeOffset, top: "50%", transform: "translateY(-50%)" };
1087
+ case "right":
1088
+ if (position === "sheet-top-left") return { right: sizeOffset, top: inset };
1089
+ if (position === "sheet-top-right") return { right: sizeOffset, bottom: inset };
1090
+ return { right: sizeOffset, top: "50%", transform: "translateY(-50%)" };
1091
+ }
1092
+ }
1093
+ function applySheetAnchoredPosition(ws, mode, position, inset) {
1094
+ const style = resolveSheetAnchoredStyle(mode, position, inset);
1095
+ if (style.top !== void 0) ws.top = style.top;
1096
+ if (style.bottom !== void 0) ws.bottom = style.bottom;
1097
+ if (style.left !== void 0) ws.left = style.left;
1098
+ if (style.right !== void 0) ws.right = style.right;
1099
+ if (style.transform !== void 0) ws.transform = style.transform;
1100
+ }
1225
1101
 
1226
1102
  // src/core/animation/animation.ts
1227
1103
  var easeOutBack = (t) => {
@@ -1374,13 +1250,9 @@ var AnimationRunner = class {
1374
1250
  __publicField(this, "duration");
1375
1251
  __publicField(this, "easing");
1376
1252
  __publicField(this, "respectReducedMotion");
1377
- /** Live-tracked via mq listener so animateTo skips matchMedia per snap. */
1378
1253
  __publicField(this, "reducedMotion", false);
1379
- /** Disposes the matchMedia change listener — drained from destroy(). */
1380
1254
  __publicField(this, "detachReducedMotion", null);
1381
- /** Computed once — both inputs (option + API presence) stable for life. */
1382
1255
  __publicField(this, "viewTransitionsAvailable");
1383
- /** In-flight handles. Public for tests / debugging only — engine drives via cancel(). */
1384
1256
  __publicField(this, "currentTween", null);
1385
1257
  __publicField(this, "currentSpring", null);
1386
1258
  this.element = deps.element;
@@ -1411,26 +1283,13 @@ var AnimationRunner = class {
1411
1283
  }
1412
1284
  }
1413
1285
  }
1414
- /**
1415
- * Live derived flag — `true` whenever a spring/tween handle is in flight.
1416
- * Reading the canonical handles (instead of mirroring into a separate
1417
- * boolean) eliminates the reduced-motion early-return latent bug where
1418
- * the flag stayed true forever, and removes the cross-controller
1419
- * setIsAnimating callback.
1420
- */
1421
1286
  get isAnimating() {
1422
1287
  return this.currentTween !== null || this.currentSpring !== null;
1423
1288
  }
1424
- /** Cancel any in-flight tween/spring without touching engine state. */
1425
1289
  cancel() {
1426
1290
  this.currentTween?.cancel();
1427
1291
  this.currentSpring?.cancel();
1428
1292
  }
1429
- /**
1430
- * Animate from current size to `target`. Resolves when the animation
1431
- * completes; resolves early if cancelled mid-flight (caller checks an
1432
- * abort signal post-await for cycle-invariant correctness).
1433
- */
1434
1293
  async animateTo(target, velocityPxPerMs) {
1435
1294
  if (target === this.getSize()) return;
1436
1295
  this.currentTween?.cancel();
@@ -1471,7 +1330,6 @@ var AnimationRunner = class {
1471
1330
  if (!this.isDragging()) this.element.style.willChange = "auto";
1472
1331
  }
1473
1332
  }
1474
- /** Tear down the matchMedia listener. Engine calls from destroy(). */
1475
1333
  destroy() {
1476
1334
  this.detachReducedMotion?.();
1477
1335
  this.detachReducedMotion = null;
@@ -1631,12 +1489,8 @@ var LifecycleController = class {
1631
1489
  __publicField(this, "inertSiblings");
1632
1490
  __publicField(this, "shouldApplyInertSiblings");
1633
1491
  __publicField(this, "returnFocus");
1634
- /** Toggle-state teardowns — fire on close, NOT in destroy-only stack. */
1635
1492
  __publicField(this, "releaseFocusTrap", null);
1636
1493
  __publicField(this, "releaseScrollLock", null);
1637
- /** True between install() and release(). Guards release()/destroy() from
1638
- * re-running side effects (notably `returnFocus.focus()`) on already-
1639
- * released or never-installed instances. */
1640
1494
  __publicField(this, "installed", false);
1641
1495
  __publicField(this, "destroyed", false);
1642
1496
  this.element = deps.element;
@@ -1650,14 +1504,6 @@ var LifecycleController = class {
1650
1504
  this.returnFocus = opts.returnFocus;
1651
1505
  this.inertSiblings = createInertSiblings(() => this.element);
1652
1506
  }
1653
- /**
1654
- * Run the open-time install steps. Idempotent: each step checks its
1655
- * existing teardown ref so a re-open during a half-cancelled close
1656
- * doesn't double-install. Sets `installed=true` BEFORE the first side
1657
- * effect so a partial throw (detached / shadow-DOM target) leaves the
1658
- * caller's `release()` rollback path armed to undo whatever did succeed.
1659
- * No-op when destroyed.
1660
- */
1661
1507
  install() {
1662
1508
  if (this.destroyed) return;
1663
1509
  this.installed = true;
@@ -1674,22 +1520,10 @@ var LifecycleController = class {
1674
1520
  this.inertSiblings.apply();
1675
1521
  }
1676
1522
  }
1677
- /**
1678
- * Update the post-release focus target. Overlay re-binds this when the
1679
- * consumer changes `returnFocus` after construction; the new value applies
1680
- * on the next `release()`. No-op when destroyed.
1681
- */
1682
1523
  setReturnFocus(target) {
1683
1524
  if (this.destroyed) return;
1684
1525
  this.returnFocus = target;
1685
1526
  }
1686
- /**
1687
- * Release all install-time teardowns. Short-circuits when not installed,
1688
- * so `close() → destroy()` (each routing through here) only restores
1689
- * focus once. The `installed` guard lets `inertSiblings.remove()` and
1690
- * the focus-trap/scroll-lock teardown nullings stay simple — they were
1691
- * already idempotent, but `returnFocus.focus()` was firing on every call.
1692
- */
1693
1527
  release() {
1694
1528
  if (!this.installed) return;
1695
1529
  this.installed = false;
@@ -1703,12 +1537,6 @@ var LifecycleController = class {
1703
1537
  target?.focus?.();
1704
1538
  }
1705
1539
  }
1706
- /**
1707
- * Final teardown — releases any still-installed listeners and seals the
1708
- * controller. After destroy(), install() is a no-op and setReturnFocus()
1709
- * is rejected, so post-destroy mutations can't queue up a stale focus
1710
- * restore.
1711
- */
1712
1540
  destroy() {
1713
1541
  this.release();
1714
1542
  this.destroyed = true;
@@ -1718,12 +1546,10 @@ var LifecycleController = class {
1718
1546
  // src/core/gestures.ts
1719
1547
  var isAxisVertical = (mode) => mode === "bottom" || mode === "top";
1720
1548
  var SampleRing = class {
1721
- // current count, ≤ capacity
1722
1549
  constructor(capacity) {
1723
1550
  __publicField(this, "slots");
1724
1551
  __publicField(this, "capacity");
1725
1552
  __publicField(this, "head", 0);
1726
- // next write index
1727
1553
  __publicField(this, "size", 0);
1728
1554
  this.capacity = capacity;
1729
1555
  this.slots = new Array(capacity);
@@ -1736,12 +1562,6 @@ var SampleRing = class {
1736
1562
  this.head = (this.head + 1) % this.capacity;
1737
1563
  if (this.size < this.capacity) this.size++;
1738
1564
  }
1739
- /**
1740
- * Drop oldest entries whose timestamp is below `cutoff`, but never shrink
1741
- * below `minKeep`. Mirrors the original
1742
- * `while (samples.length > minKeep && samples[0].t < cutoff) samples.shift()`
1743
- * but without copying — just advances the logical tail.
1744
- */
1745
1565
  pruneBefore(cutoff, minKeep) {
1746
1566
  while (this.size > minKeep) {
1747
1567
  const oldestIdx = (this.head - this.size + this.capacity) % this.capacity;
@@ -1877,28 +1697,14 @@ var GestureController = class {
1877
1697
  __publicField(this, "detach", null);
1878
1698
  __publicField(this, "isDragging_", false);
1879
1699
  __publicField(this, "dragStartSize", 0);
1880
- // Only "touch" gestures are eligible for soft-keyboard dismissal —
1881
- // mouse/trackpad users don't have a keyboard to dismiss.
1882
1700
  __publicField(this, "currentPointerKind", "touch");
1883
- // One blur per drag — flipping focus every move event races the keyboard's
1884
- // own animations on iOS and makes the page jitter.
1885
1701
  __publicField(this, "keyboardDismissed", false);
1886
- // Reused payload object for the "drag" event — mutated each onMove instead
1887
- // of allocating a fresh `{size, delta}` per frame. At 60-120Hz a long drag
1888
- // would otherwise churn tens of KB through the GC. Mirrors browser
1889
- // PointerEvent semantics: consumers must NOT retain the reference across
1890
- // frames (clone if needed). See SheetEventMap.drag JSDoc.
1891
1702
  __publicField(this, "dragPayload", { size: 0, delta: 0 });
1892
1703
  this.deps = deps;
1893
1704
  }
1894
- /** True between `dragstart` and `dragend`/`onCancel`. */
1895
1705
  get isDragging() {
1896
1706
  return this.isDragging_;
1897
1707
  }
1898
- /**
1899
- * Bind pointer listeners on the handle and return a teardown. Engine pushes
1900
- * the teardown into its TeardownStack so destroy() drains it once.
1901
- */
1902
1708
  install() {
1903
1709
  if (this.detach) return this.detach;
1904
1710
  this.detach = installGestures(this.deps.handle, this.deps.mode, {
@@ -1955,11 +1761,6 @@ var GestureController = class {
1955
1761
  });
1956
1762
  return this.detach;
1957
1763
  }
1958
- /**
1959
- * Force-clear drag visual state. Engine calls from destroy() when destroyed
1960
- * mid-drag — gesture detach won't synthesize an onCancel, so we have to
1961
- * roll back the data-attribute + will-change ourselves.
1962
- */
1963
1764
  forceClearDragState() {
1964
1765
  if (!this.isDragging_) return;
1965
1766
  this.isDragging_ = false;
@@ -1978,11 +1779,6 @@ var AriaSliderWriter = class {
1978
1779
  mode === "bottom" || mode === "top" ? "vertical" : "horizontal"
1979
1780
  );
1980
1781
  }
1981
- /**
1982
- * Update slider position. `allowedIds` length determines `aria-valuemax`;
1983
- * `activeId` indexes into it for `aria-valuenow` + `aria-valuetext`.
1984
- * Empty list → strip every dynamic attr so AT skips the handle entirely.
1985
- */
1986
1782
  setValue(allowedIds, activeId) {
1987
1783
  if (allowedIds.length === 0) {
1988
1784
  this.handle.removeAttribute("aria-valuemin");
@@ -2056,9 +1852,6 @@ function resolveEngineOptions(opts, extras) {
2056
1852
  scrimMode: opts.scrimMode,
2057
1853
  scrimColor: opts.scrimColor,
2058
1854
  scrimBlur: opts.scrimBlur,
2059
- // Auto-promote pointer-events when tap-to-close is on so the click
2060
- // listener actually receives events. Caller can still force `false`
2061
- // explicitly to opt out — `??` only kicks in for `undefined`.
2062
1855
  scrimInteractive: opts.scrimInteractive ?? (opts.scrimTapToClose ? true : void 0),
2063
1856
  scrimTapToClose: opts.scrimTapToClose,
2064
1857
  scrimPreset: opts.scrimPreset,
@@ -2079,10 +1872,6 @@ function resolveEngineOptions(opts, extras) {
2079
1872
  closeOnEscape: opts.closeOnEscape,
2080
1873
  lockBodyScroll: opts.lockBodyScroll,
2081
1874
  inertSiblings: opts.inertSiblings,
2082
- // Folded in from `extras` so the lifecycle bag is fully assembled at
2083
- // the call site — engine no longer needs a spread+override at
2084
- // controller construction. When `extras` is omitted, the field stays
2085
- // undefined and the controller uses its own default predicate.
2086
1875
  shouldApplyInertSiblings: extras?.shouldApplyInertSiblings
2087
1876
  }
2088
1877
  };
@@ -2117,6 +1906,7 @@ var BottomSheetEngine = class {
2117
1906
  __publicField(this, "scrollContainer");
2118
1907
  __publicField(this, "backdrop");
2119
1908
  __publicField(this, "screenComponent");
1909
+ __publicField(this, "scrimParent", null);
2120
1910
  __publicField(this, "mode");
2121
1911
  __publicField(this, "flickVelocity");
2122
1912
  __publicField(this, "dragThreshold");
@@ -2127,40 +1917,22 @@ var BottomSheetEngine = class {
2127
1917
  __publicField(this, "lifecycle");
2128
1918
  __publicField(this, "closeOnBack");
2129
1919
  __publicField(this, "routedTo");
2130
- // Engine retains `snapPointsRaw` for the "find a closed-id" query in
2131
- // close() (raw input, not resolved geometry). Resolver owns geometry.
2132
1920
  __publicField(this, "snapPointsRaw");
2133
1921
  __publicField(this, "snaps");
2134
1922
  __publicField(this, "activeId");
2135
1923
  __publicField(this, "size", 0);
2136
- // Constructed in attach(); a plugin's install() could read `state` before
2137
- // attach completes if the constructor throws partway through. State getter
2138
- // null-guards via `this.gesture?.isDragging ?? false`.
2139
1924
  __publicField(this, "gesture");
2140
1925
  __publicField(this, "rootEl", null);
2141
1926
  __publicField(this, "destroyed", false);
2142
- // In-flight ViewTransition handle. A second snapTo while a VT is in flight
2143
- // would otherwise let Chromium reject the second's promise and flicker —
2144
- // newCycle() calls skipTransition() to abort cleanly.
2145
1927
  __publicField(this, "currentViewTransition", null);
2146
- // Each cycle captures the signal at entry; when a newer cycle aborts the
2147
- // controller, the captured signal flips to `aborted=true` and post-await
2148
- // guards short-circuit (prevents stale snap/open/close emits).
2149
1928
  __publicField(this, "currentAbort", new AbortController());
2150
- // Sub-pixel `--bs-size` deltas and <1% `--bs-progress` deltas are not
2151
- // visually distinguishable; gating CSSOM writes drops 30-50% of writes
2152
- // during the spring's settle tail.
2153
1929
  __publicField(this, "sizeWriteSentinel", new WriteSentinel());
2154
1930
  __publicField(this, "progressWriteSentinel", new WriteSentinel());
2155
1931
  __publicField(this, "isTopSheet", true);
2156
- // Pooled payload — identity documented in SheetEventMap.progress JSDoc;
2157
- // consumers must not retain across frames.
2158
1932
  __publicField(this, "progressPayload", {
2159
1933
  value: 0,
2160
1934
  size: 0
2161
1935
  });
2162
- // Pooled buffer for `getDragContext()` — onMove calls this 60-120×/s.
2163
- // Inner `range` object identity is also stable (mutated in place).
2164
1936
  __publicField(this, "dragContextBuf", {
2165
1937
  size: 0,
2166
1938
  maxAxisSize: 0,
@@ -2171,13 +1943,7 @@ var BottomSheetEngine = class {
2171
1943
  __publicField(this, "persistKey");
2172
1944
  __publicField(this, "linkedSheets", []);
2173
1945
  __publicField(this, "scrollCache");
2174
- // Single LIFO stack for ALL destroy-only teardowns. Plugins (added last)
2175
- // tear down first before the infrastructure they depend on. Toggle-semantic
2176
- // listeners (focus-trap / scroll-lock / scrim tap-to-close) stay as named
2177
- // fields on their controllers.
2178
1946
  __publicField(this, "teardowns", new TeardownStack());
2179
- // Pre-computed transform template — built once per engine since `mode` is
2180
- // fixed for its lifetime. Avoids the 4-arm switch at 60-120 Hz.
2181
1947
  __publicField(this, "transformTemplate");
2182
1948
  const resolved = resolveEngineOptions(opts, {
2183
1949
  shouldApplyInertSiblings: () => {
@@ -2192,7 +1958,8 @@ var BottomSheetEngine = class {
2192
1958
  this.handle = opts.handle ?? opts.element;
2193
1959
  this.scrollContainer = opts.scrollContainer;
2194
1960
  this.backdrop = opts.backdrop;
2195
- this.screenComponent = opts.screenComponent;
1961
+ this.screenComponent = opts.scrim ?? opts.screenComponent;
1962
+ this.scrimParent = this.screenComponent?.parentElement ?? null;
2196
1963
  this.mode = resolved.mode;
2197
1964
  this.transformTemplate = buildTransformTemplate(this.mode);
2198
1965
  this.aria = new AriaSliderWriter(this.handle, this.mode);
@@ -2220,8 +1987,6 @@ var BottomSheetEngine = class {
2220
1987
  getRootEl: () => this.rootEl,
2221
1988
  applySize: (size) => this.applySize(size),
2222
1989
  getSize: () => this.size,
2223
- // Optional chain protects synthetic test paths that drive animateTo
2224
- // before attach() (gesture is constructed there).
2225
1990
  isDragging: () => this.gesture?.isDragging ?? false
2226
1991
  },
2227
1992
  resolved.animation
@@ -2326,21 +2091,6 @@ var BottomSheetEngine = class {
2326
2091
  on(event, fn) {
2327
2092
  return this.bus.on(event, fn);
2328
2093
  }
2329
- /**
2330
- * Register a plugin. `install` runs synchronously; if it returns a function,
2331
- * it's captured for teardown during `destroy()`. No-op if already destroyed
2332
- * — installing into a torn-down engine would leak listeners.
2333
- *
2334
- * Throws from `plugin.install` are caught and rethrown via `queueMicrotask`
2335
- * so a buggy plugin can't take the engine down with it: the engine stays
2336
- * usable, sibling plugins still install, and the error surfaces in dev tools
2337
- * on the next tick instead of being swallowed. Mirrors the per-fn isolation
2338
- * model used for teardowns.
2339
- *
2340
- * Plugins that accept the second `scope` argument get transactional cleanup:
2341
- * push partial-install cleanups via `scope.add(fn)` and the engine drains
2342
- * them on throw, OR merges them into destroy-time teardowns on success.
2343
- */
2344
2094
  use(plugin) {
2345
2095
  if (this.destroyed) {
2346
2096
  console.warn(
@@ -2350,9 +2100,6 @@ var BottomSheetEngine = class {
2350
2100
  }
2351
2101
  const scoped = [];
2352
2102
  const scope = {
2353
- // typeof check — not just truthiness — so a JS-only plugin pushing a
2354
- // bogus value is silently rejected instead of queueing a confusing
2355
- // TypeError on destroy() drain.
2356
2103
  add: (fn) => {
2357
2104
  if (typeof fn === "function") scoped.push(fn);
2358
2105
  }
@@ -2381,36 +2128,13 @@ var BottomSheetEngine = class {
2381
2128
  }
2382
2129
  return this;
2383
2130
  }
2384
- /**
2385
- * Replace the linked-sheets list at runtime. Use when peer engines aren't
2386
- * available at construction time (mounted later in the tree). The wiring
2387
- * to the `open` event is independent of this list — it reads `linkedSheets`
2388
- * lazily on emit, so this setter is a simple field swap.
2389
- */
2390
2131
  setLinkedSheets(sheets) {
2391
2132
  if (this.destroyed) return;
2392
2133
  this.linkedSheets = sheets;
2393
2134
  }
2394
- /** Read-only view of the current allow-list, for cross-sheet target negotiation. */
2395
2135
  getAllowedIds() {
2396
2136
  return this.snaps.getAllowedIds().slice();
2397
2137
  }
2398
- /**
2399
- * Animate to a snap point by id. Resolves when the animation completes.
2400
- *
2401
- * Two call shapes:
2402
- * ```ts
2403
- * engine.snapTo("full"); // default velocity
2404
- * engine.snapTo("full", 0.5); // px/ms initial velocity
2405
- * engine.snapTo("full", { velocity: 0.5, signal }); // abortable
2406
- * ```
2407
- *
2408
- * When `opts.signal` is provided, calling `controller.abort()` cancels the
2409
- * in-flight tween/spring and the returned promise resolves without firing
2410
- * `snap`. The captured `AbortSignal` is checked after each await so external
2411
- * aborts ride the same cycle invariant as internal ones (resize, destroy,
2412
- * concurrent snapTo).
2413
- */
2414
2138
  async snapTo(id, velocityOrOpts = 0, _skipBeforeSnap = false) {
2415
2139
  if (this.destroyed) return;
2416
2140
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2469,16 +2193,6 @@ var BottomSheetEngine = class {
2469
2193
  this.handleClose();
2470
2194
  }
2471
2195
  }
2472
- /**
2473
- * Drive the sheet to an arbitrary px size, bypassing snap matching.
2474
- * Useful for preview scrubs, animation chaining, or programmatic gestures.
2475
- * Animates with the configured spring/tween. Resolves when the animation
2476
- * completes. Does NOT update `activeId` — call `snapTo(id)` after to commit.
2477
- *
2478
- * Unlike `snapTo`, this does not fire `before-snap` (it isn't a snap commit)
2479
- * nor `snap` (the sheet isn't settling to a known snap). `progress` events
2480
- * still fire via applySize. Target is clamped to `[0, maxAxisSize]`.
2481
- */
2482
2196
  async dragTo(targetSize, velocityOrOpts = 0) {
2483
2197
  if (this.destroyed) return;
2484
2198
  const opts = typeof velocityOrOpts === "object" ? velocityOrOpts : { velocity: velocityOrOpts };
@@ -2516,20 +2230,10 @@ var BottomSheetEngine = class {
2516
2230
  this.snaps.setAllowedIds(ids);
2517
2231
  this.updateAriaSlider();
2518
2232
  if (snap && ids.includes(snap)) {
2519
- void this.snapTo(
2520
- snap,
2521
- 0,
2522
- /* internal */
2523
- true
2524
- );
2233
+ void this.snapTo(snap, 0, true);
2525
2234
  } else if (!ids.includes(this.activeId)) {
2526
2235
  const fallback = ids[0];
2527
- if (fallback) void this.snapTo(
2528
- fallback,
2529
- 0,
2530
- /* internal */
2531
- true
2532
- );
2236
+ if (fallback) void this.snapTo(fallback, 0, true);
2533
2237
  }
2534
2238
  }
2535
2239
  setBackdropRange(range) {
@@ -2547,21 +2251,6 @@ var BottomSheetEngine = class {
2547
2251
  setScrimInteractive(interactive) {
2548
2252
  this.scrim.setScrimInteractive(interactive);
2549
2253
  }
2550
- /**
2551
- * Batch-update multiple scrim fields in one call. Apply order is:
2552
- * 1. `preset` — fills baseline color/blur/range/interactive.
2553
- * 2. `mode` — re-positions the scrim (fixed inset / clear / hide).
2554
- * 3. `enabled` — flips the opacity contribution on/off.
2555
- * 4. `tapToClose` — installs/tears the click listener.
2556
- * 5. `color` / `blur` / `interactive` / `range` — explicit overrides.
2557
- * 6. ONE `applySize` so the per-frame opacity stays consistent.
2558
- *
2559
- * Performance: collecting changes here (vs calling individual setters)
2560
- * avoids repeated `applySize` calls during a multi-field swap. Setters
2561
- * called here suppress their own internal `applySize` either by passing
2562
- * through fast-paths (mode/enabled no-op when value unchanged) or by
2563
- * being visual-only (color/blur/interactive don't influence opacity).
2564
- */
2565
2254
  setScrim(opts) {
2566
2255
  this.scrim.setScrim(opts, () => this.applySize(this.size));
2567
2256
  }
@@ -2577,11 +2266,6 @@ var BottomSheetEngine = class {
2577
2266
  setScrimOverlay(opts) {
2578
2267
  return this.scrim.setScrimOverlay(opts);
2579
2268
  }
2580
- /**
2581
- * Read-only snapshot of scrim state. Use the `setScrim*` setters to mutate.
2582
- * Stable public API — internals (e.g. cached opacity sentinels) are NOT
2583
- * exposed so consumers can't accidentally couple to ScrimController shape.
2584
- */
2585
2269
  getScrimState() {
2586
2270
  return { mode: this.scrim.scrimMode, enabled: this.scrim.scrimEnabled };
2587
2271
  }
@@ -2627,24 +2311,11 @@ var BottomSheetEngine = class {
2627
2311
  emit(event, payload) {
2628
2312
  this.bus.emit(event, payload);
2629
2313
  }
2630
- /**
2631
- * Abort the current cycle's signal and replace the controller. Returns the
2632
- * fresh signal so the caller can capture it locally before any await — when
2633
- * a future newCycle() / destroy aborts our controller, the captured signal
2634
- * flips to aborted and post-await guards short-circuit.
2635
- */
2636
2314
  newCycle() {
2637
2315
  this.currentAbort.abort();
2638
2316
  this.currentAbort = new AbortController();
2639
2317
  return this.currentAbort.signal;
2640
2318
  }
2641
- /**
2642
- * Fire `before-snap` and report whether a listener cancelled it. The
2643
- * `cancel()` callback exposed to listeners is frozen after the synchronous
2644
- * emit phase so async callers can't silently miss — they get a console
2645
- * warning. Returns `true` when cancelled so callers can early-return /
2646
- * spring-back without re-implementing the cancel/frozen pattern.
2647
- */
2648
2319
  emitBeforeSnap(target, previousId) {
2649
2320
  let cancelled = false;
2650
2321
  let frozen = false;
@@ -2678,30 +2349,9 @@ var BottomSheetEngine = class {
2678
2349
  this.snaps.setRaw(this.snapPointsRaw);
2679
2350
  this.scrim.invalidateOpacityCache();
2680
2351
  }
2681
- /**
2682
- * Memoised `allowedRange()` — delegates to SnapResolver which owns the cache.
2683
- * Engine wrapper kept so existing call sites don't all need updating;
2684
- * resolver-level memoization survives across method invocations.
2685
- */
2686
2352
  getAllowedRange() {
2687
2353
  return this.snaps.getAllowedRange();
2688
2354
  }
2689
- /**
2690
- * Write ARIA slider value attributes onto the handle so the engine itself
2691
- * is the source of truth for screen-reader announcements. Called from the
2692
- * constructor and from every code path that mutates `activeId` /
2693
- * `allowedIds`. WAI-ARIA 1.2 requires `aria-valuenow` for `role="slider"` —
2694
- * without it screen readers can't announce drag/snap progress to keyboard
2695
- * + AT users (WCAG 4.1.2 Name, Role, Value).
2696
- *
2697
- * Engine does NOT set `role="slider"` itself — that's the adapter's or
2698
- * consumer markup's responsibility. We only decorate with dynamic values.
2699
- *
2700
- * Empty allowed-list is malformed input (settable via `setAllowed([])`);
2701
- * short-circuit to remove ARIA value attributes so screen readers see a
2702
- * no-value slider rather than a degenerate `min=max=now=0` (WAI-ARIA 1.2
2703
- * requires `valuemax > valuemin` for `role="slider"`).
2704
- */
2705
2355
  updateAriaSlider() {
2706
2356
  this.aria.setValue(this.snaps.getAllowedIds(), this.activeId);
2707
2357
  }
@@ -2726,11 +2376,6 @@ var BottomSheetEngine = class {
2726
2376
  element: this.element,
2727
2377
  mode: this.mode,
2728
2378
  getRoot: () => this.rootEl,
2729
- // Single coalesced reader — narrows the DI surface and removes 3 getter
2730
- // dispatches per onMove frame at 60-120Hz. Returns a SHARED buffer
2731
- // (this.dragContextBuf) — see field JSDoc for the retention contract.
2732
- // Inner `range` object is mutated in place too so callers caching its
2733
- // identity between frames stay valid.
2734
2379
  getDragContext: () => {
2735
2380
  const buf = this.dragContextBuf;
2736
2381
  const r = this.getAllowedRange();
@@ -2896,7 +2541,6 @@ var BottomSheetEngine = class {
2896
2541
  }
2897
2542
  });
2898
2543
  }
2899
- /** Trigger a brief haptic tick (iOS Safari + Android Chrome support). */
2900
2544
  haptic() {
2901
2545
  if (typeof navigator !== "undefined" && "vibrate" in navigator) {
2902
2546
  try {
@@ -2912,6 +2556,9 @@ var BottomSheetEngine = class {
2912
2556
  style.transform = this.transformTemplate(offset);
2913
2557
  if (this.sizeWriteSentinel.shouldWrite(size, SIZE_WRITE_EPSILON)) {
2914
2558
  style.setProperty("--bs-size", `${size}px`);
2559
+ if (this.scrimParent) {
2560
+ this.scrimParent.style.setProperty("--bs-size", `${size}px`);
2561
+ }
2915
2562
  }
2916
2563
  const progress = this.computeProgress(size);
2917
2564
  const progressChanged = this.progressWriteSentinel.shouldWrite(
@@ -2982,7 +2629,7 @@ function useBottomSheet(opts) {
2982
2629
  handle: handleRef.current ?? void 0,
2983
2630
  scrollContainer: contentRef.current ?? void 0,
2984
2631
  backdrop: backdropRef.current ?? void 0,
2985
- screenComponent: screenRef.current ?? void 0
2632
+ scrim: screenRef.current ?? void 0
2986
2633
  });
2987
2634
  engineRef.current = engine;
2988
2635
  cachedSnapshotRef.current = { ...engine.state };
@@ -3116,6 +2763,11 @@ var BottomSheet = forwardRef(
3116
2763
  open,
3117
2764
  close,
3118
2765
  setAllowed,
2766
+ setSnapPoints: (points, allowed) => getEngine()?.setSnapPoints(points, allowed),
2767
+ setScrim: (opts) => getEngine()?.setScrim(opts),
2768
+ setScrimOverlay: (opts) => getEngine()?.setScrimOverlay(opts) ?? (() => {
2769
+ }),
2770
+ getEngine,
3119
2771
  get state() {
3120
2772
  return getEngine()?.state ?? state;
3121
2773
  }
@@ -3135,7 +2787,7 @@ var BottomSheet = forwardRef(
3135
2787
  onClick: closeOnBackdrop ? () => void close() : void 0
3136
2788
  }
3137
2789
  ),
3138
- screen && /* @__PURE__ */ jsx(
2790
+ /* @__PURE__ */ jsx(
3139
2791
  "div",
3140
2792
  {
3141
2793
  ref: screenRef,
@@ -3258,8 +2910,6 @@ var OverlayEngine = class {
3258
2910
  __publicField(this, "exitEasing");
3259
2911
  __publicField(this, "backdropOpacity");
3260
2912
  __publicField(this, "backdropFilter");
3261
- // Definite-assignment: written via assignSwipeFromConfig in the
3262
- // constructor — TS can't follow the helper, mark the assertion explicit.
3263
2913
  __publicField(this, "swipeToCloseEnabled");
3264
2914
  __publicField(this, "swipeThreshold");
3265
2915
  __publicField(this, "swipeVelocityThreshold");
@@ -3279,13 +2929,7 @@ var OverlayEngine = class {
3279
2929
  __publicField(this, "isOpen_", false);
3280
2930
  __publicField(this, "isAnimating_", false);
3281
2931
  __publicField(this, "destroyed", false);
3282
- // Tracks whether setOverlayChildren ever populated the panel — lets
3283
- // clearOverlayChildren skip the DOM mutation when nothing was injected
3284
- // (so the field also acts as a zero-allocation feature flag).
3285
2932
  __publicField(this, "hasInjectedChildren", false);
3286
- // Bumped on every open/close. awaitTransition captures it and ignores
3287
- // transitionend/timeout fired by older cycles — prevents cross-cycle
3288
- // listener bleed during rapid toggling.
3289
2933
  __publicField(this, "cycleNonce", 0);
3290
2934
  __publicField(this, "bus", createEventBus());
3291
2935
  __publicField(this, "detachEscape", null);
@@ -3338,9 +2982,6 @@ var OverlayEngine = class {
3338
2982
  closeOnEscape: this.closeOnEscape,
3339
2983
  lockBodyScroll: this.bodyScrollLockEnabled,
3340
2984
  inertSiblings: this.inertSiblingsEnabled,
3341
- // Overlay-specific guard: only paint body siblings inert when the
3342
- // overlay element is actually a body descendant. Detached / shadow-
3343
- // DOM / portal overlays would otherwise lock unrelated trees.
3344
2985
  shouldApplyInertSiblings: () => {
3345
2986
  const bodyChildren = Array.from(document.body.children);
3346
2987
  return bodyChildren.some(
@@ -3494,12 +3135,6 @@ var OverlayEngine = class {
3494
3135
  this.releaseStackEntry = null;
3495
3136
  this.restoreMount();
3496
3137
  }
3497
- /**
3498
- * Runtime visual setter — opacity is applied immediately when open so a
3499
- * live-controls panel can preview the change without re-opening. When
3500
- * closed we only stash the field; next open() picks it up so we don't
3501
- * mutate hidden DOM (avoids paint thrash on detached panels).
3502
- */
3503
3138
  setBackdropOpacity(opacity) {
3504
3139
  if (this.destroyed) return;
3505
3140
  this.backdropOpacity = opacity;
@@ -3507,11 +3142,6 @@ var OverlayEngine = class {
3507
3142
  this.backdrop.style.opacity = String(opacity);
3508
3143
  }
3509
3144
  }
3510
- /**
3511
- * Runtime visual setter for `backdrop-filter`. `undefined` clears the
3512
- * filter (mirrors the close() behaviour). Defers to next open() when the
3513
- * overlay is currently closed.
3514
- */
3515
3145
  setBackdropFilter(filter) {
3516
3146
  if (this.destroyed) return;
3517
3147
  this.backdropFilter = filter;
@@ -3525,12 +3155,6 @@ var OverlayEngine = class {
3525
3155
  style.webkitBackdropFilter = "";
3526
3156
  }
3527
3157
  }
3528
- /**
3529
- * Toggle / reconfigure swipe-to-close at runtime. Re-resolves the flat
3530
- * threshold fields, then installs or tears down the pointer listener so
3531
- * the change applies to the CURRENT open cycle. Idempotent: safe to call
3532
- * with the same config repeatedly.
3533
- */
3534
3158
  setSwipeToClose(config) {
3535
3159
  if (this.destroyed) return;
3536
3160
  const wasEnabled = this.swipeToCloseEnabled;
@@ -3543,7 +3167,6 @@ var OverlayEngine = class {
3543
3167
  this.detachSwipe = null;
3544
3168
  }
3545
3169
  }
3546
- /** Animation setters only stage the field — applied on next open/close. */
3547
3170
  setEnterAnimation(animation) {
3548
3171
  if (this.destroyed) return;
3549
3172
  this.enterAnimation = animation;
@@ -3552,23 +3175,15 @@ var OverlayEngine = class {
3552
3175
  if (this.destroyed) return;
3553
3176
  this.exitAnimation = animation;
3554
3177
  }
3555
- /** Updates the field used at next close() — no immediate side effect. */
3556
3178
  setReturnFocus(target) {
3557
3179
  if (this.destroyed) return;
3558
3180
  this.lifecycle.setReturnFocus(target);
3559
3181
  }
3560
- /**
3561
- * Batch update — preset first, then individual overrides. Applied as a
3562
- * single DOM-write phase when open so live-controls don't trigger
3563
- * cascading reflows. Safe to call when closed (defers to next open).
3564
- */
3565
3182
  setOverlay(opts) {
3566
3183
  if (this.destroyed) return;
3567
3184
  const preset = opts.preset ? OVERLAY_PRESETS[opts.preset] : void 0;
3568
3185
  const merged = preset ? {
3569
3186
  backdropOpacity: preset.backdropOpacity,
3570
- // Preset's `undefined` filter means "no filter" — surface as null
3571
- // so the merged step clears the field instead of inheriting old.
3572
3187
  backdropFilter: preset.backdropFilter === void 0 ? null : preset.backdropFilter,
3573
3188
  swipeToClose: preset.swipeToClose,
3574
3189
  enterAnimation: preset.enterAnimation,
@@ -3627,30 +3242,17 @@ var OverlayEngine = class {
3627
3242
  }
3628
3243
  }
3629
3244
  }
3630
- /**
3631
- * Runtime children injection — replace the panel's contents without
3632
- * unmounting the engine. `replaceChildren()` is a single atomic DOM op,
3633
- * so callers can swap content rapidly without intermediate flicker.
3634
- * Function form is resolved exactly ONCE per call (cheap lazy content).
3635
- */
3636
3245
  setOverlayChildren(children) {
3637
3246
  if (this.destroyed) return;
3638
3247
  const node = typeof children === "function" ? children() : children;
3639
3248
  this.element.replaceChildren(node);
3640
3249
  this.hasInjectedChildren = true;
3641
3250
  }
3642
- /**
3643
- * Clear injected children. No-op when nothing was injected so consumers
3644
- * who never used setOverlayChildren can call this defensively without
3645
- * paying for a DOM mutation.
3646
- */
3647
3251
  clearOverlayChildren() {
3648
3252
  if (this.destroyed || !this.hasInjectedChildren) return;
3649
3253
  this.element.replaceChildren();
3650
3254
  this.hasInjectedChildren = false;
3651
3255
  }
3652
- // Shared swipe-config normalizer — used by constructor, setSwipeToClose,
3653
- // and setOverlay so the union/object shape is decoded in exactly one place.
3654
3256
  assignSwipeFromConfig(swipe) {
3655
3257
  if (typeof swipe === "object" && swipe !== null) {
3656
3258
  this.swipeToCloseEnabled = swipe.enabled ?? true;
@@ -3794,13 +3396,6 @@ var OverlayEngine = class {
3794
3396
  this.detachOutsidePointer = null;
3795
3397
  this.lifecycle.release();
3796
3398
  }
3797
- /**
3798
- * Light pointer-driven dismiss. Tracks finger along the edge axis, follows
3799
- * with translate, dismisses if past `swipeThreshold` (fraction of panel
3800
- * size) or release velocity exceeds `swipeVelocityThreshold` (px/ms).
3801
- * Pure CSS-transform during drag — no rAF or physics required since the
3802
- * panel only moves one direction.
3803
- */
3804
3399
  installSwipeToClose() {
3805
3400
  const el = this.element;
3806
3401
  const edge = this.edge;