@supermousejs/core 2.3.0 → 2.4.1

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.
package/dist/index.mjs CHANGED
@@ -1,44 +1,57 @@
1
- const d = "2.3.0";
2
- function p(n, t, e) {
3
- return n + (t - n) * e;
1
+ const c = "2.4.1";
2
+ function p(o, t, e) {
3
+ return o + (t - o) * e;
4
4
  }
5
- function c(n, t, e, i) {
6
- return p(n, t, 1 - Math.exp(-e * i));
5
+ function d(o, t, e, s) {
6
+ return p(o, t, 1 - Math.exp(-e * s));
7
7
  }
8
- class f {
9
- constructor(t, e, i, a) {
10
- this.state = t, this.options = e, this.getHoverSelector = i, this.onEnableChange = a, this.dataPrefix = this.options.dataPrefix ?? "supermouse", this.normalizedDataPrefix = this.dataPrefix.toLowerCase(), this.ignoreAttribute = `data-${this.dataPrefix}-ignore`, this.checkDeviceCapability(), this.checkMotionPreference(), this.bindEvents();
8
+ const h = { x: -100, y: -100 }, g = /* @__PURE__ */ new Set(["input", "textarea", "select"]), f = /* @__PURE__ */ new Set([
9
+ "default",
10
+ "auto",
11
+ "pointer",
12
+ "none",
13
+ "inherit",
14
+ "grab",
15
+ "grabbing"
16
+ ]), m = [
17
+ "a",
18
+ "button",
19
+ "input",
20
+ "textarea",
21
+ "[data-hover]",
22
+ "[data-cursor]"
23
+ ];
24
+ class v {
25
+ constructor(t, e, s, i) {
26
+ this.state = t, this.options = e, this.getHoverSelector = s, this.onEnableChange = i, this.dataPrefix = this.options.dataPrefix ?? "supermouse", this.normalizedDataPrefix = this.dataPrefix.toLowerCase(), this.ignoreAttribute = `data-${this.dataPrefix}-ignore`, this.ruleEntries = this.options.rules ? Object.entries(this.options.rules) : [], this.checkDeviceCapability(), this.checkMotionPreference(), this.setupContainerRectTracking(), this.bindEvents();
11
27
  }
12
28
  state;
13
29
  options;
14
30
  getHoverSelector;
15
31
  onEnableChange;
16
32
  mediaQueryList;
17
- mediaQueryHandler;
18
33
  motionQuery;
19
34
  dataPrefix;
20
35
  normalizedDataPrefix;
21
36
  ignoreAttribute;
22
- /**
23
- * Master switch for input processing.
24
- * Toggled by `Supermouse.enable()`/`disable()` or automatically by device capability checks.
25
- */
26
- isEnabled = !0;
27
37
  abortController = new AbortController();
28
- /**
29
- * Automatically disables the custom cursor on devices without fine pointer control.
30
- */
38
+ nativeTarget = null;
39
+ hasSeenPointer = !1;
40
+ isEnabled = !0;
41
+ containerRect = null;
42
+ resizeObserver;
43
+ /** Cached matched rules for the current interaction element. */
44
+ matchedRules = [];
45
+ lastParsedTarget = null;
46
+ /** Precomputed list of rule entries for faster iteration. */
47
+ ruleEntries;
48
+ /** The actual element currently under the pointer (regardless of hover selectors). */
49
+ currentTarget = null;
31
50
  checkDeviceCapability() {
32
- this.options.autoDisableOnMobile && (this.mediaQueryList = window.matchMedia("(pointer: fine)"), this.updateEnabledState(this.mediaQueryList.matches), this.mediaQueryHandler = (t) => {
33
- this.updateEnabledState(t.matches);
34
- }, this.mediaQueryList.addEventListener("change", this.mediaQueryHandler, {
51
+ this.options.autoDisableOnMobile && (this.mediaQueryList = window.matchMedia("(pointer: fine)"), this.updateEnabledState(this.mediaQueryList.matches), this.mediaQueryList.addEventListener("change", (t) => this.updateEnabledState(t.matches), {
35
52
  signal: this.abortController.signal
36
53
  }));
37
54
  }
38
- /**
39
- * Checks for `prefers-reduced-motion`.
40
- * If true, the core physics engine will switch to instant snapping (high damping) to avoid motion sickness.
41
- */
42
55
  checkMotionPreference() {
43
56
  this.motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"), this.state.reducedMotion = this.motionQuery.matches, this.motionQuery.addEventListener(
44
57
  "change",
@@ -51,115 +64,174 @@ class f {
51
64
  updateEnabledState(t) {
52
65
  this.isEnabled = t, this.onEnableChange(t);
53
66
  }
67
+ /** Caches container rect; updates on resize/scroll/ResizeObserver. */
68
+ setupContainerRectTracking() {
69
+ const t = this.options.container;
70
+ if (!t || t === document.body) return;
71
+ const e = () => {
72
+ this.containerRect = t.getBoundingClientRect();
73
+ };
74
+ e(), window.addEventListener("resize", e, { signal: this.abortController.signal }), window.addEventListener("scroll", e, {
75
+ passive: !0,
76
+ signal: this.abortController.signal
77
+ }), typeof ResizeObserver < "u" && (this.resizeObserver = new ResizeObserver(e), this.resizeObserver.observe(t));
78
+ }
79
+ /**
80
+ * Evaluates rules against the given element.
81
+ * Selector matching is cached per element; only function values re-evaluated each frame.
82
+ */
54
83
  parseDOMInteraction(t) {
55
- if (this.options.resolveInteraction) {
56
- this.state.interaction = this.options.resolveInteraction(t) || {};
57
- return;
84
+ if (t !== this.lastParsedTarget) {
85
+ this.lastParsedTarget = t, this.matchedRules = [];
86
+ for (const [i, n] of this.ruleEntries)
87
+ this.matchesSelector(t, i) && this.matchedRules.push({ selector: i, rules: n });
58
88
  }
59
89
  const e = {};
60
- if (this.options.rules)
61
- for (const [s, o] of Object.entries(this.options.rules))
62
- t.matches(s) && Object.assign(e, o);
63
- const i = this.normalizedDataPrefix, a = this.dataPrefix.length;
64
- for (const s in t.dataset) {
65
- if (!s.toLowerCase().startsWith(i)) continue;
66
- const o = s.slice(a);
67
- if (!o) continue;
68
- const r = t.dataset[s];
69
- e[o[0].toLowerCase() + o.slice(1)] = r === "" ? !0 : r;
90
+ for (const { rules: i } of this.matchedRules)
91
+ try {
92
+ const n = typeof i == "function" ? i(t) : i;
93
+ if (!n || typeof n != "object") continue;
94
+ for (const [r, a] of Object.entries(n))
95
+ e[r] = typeof a == "function" ? a(t) : a;
96
+ } catch (n) {
97
+ console.error("[Supermouse] Rule threw during evaluation:", n);
98
+ }
99
+ const s = this.normalizedDataPrefix;
100
+ for (const i in t.dataset) {
101
+ if (!i.toLowerCase().startsWith(s)) continue;
102
+ const n = i.slice(s.length);
103
+ if (!n) continue;
104
+ const r = t.dataset[i];
105
+ e[n[0].toLowerCase() + n.slice(1)] = r === "" ? !0 : r;
70
106
  }
71
107
  this.state.interaction = e;
72
108
  }
73
- handleMove(t) {
74
- if (!this.isEnabled || this.options.autoDisableOnMobile && t.pointerType === "touch" && !this.options.enableTouch)
75
- return;
76
- let e = t.clientX, i = t.clientY;
77
- if (this.options.container && this.options.container !== document.body) {
78
- const a = this.options.container.getBoundingClientRect();
79
- e -= a.left, i -= a.top;
109
+ /**
110
+ * Optimized selector matching:
111
+ * - For simple selectors (no spaces), just call `element.matches`.
112
+ * - For complex selectors with descendant combinators, fallback to splitting.
113
+ */
114
+ matchesSelector(t, e) {
115
+ try {
116
+ if (!/\s/.test(e.trim()))
117
+ return t.matches(e);
118
+ } catch {
119
+ }
120
+ const s = e.trim().split(/\s+/);
121
+ if (s.length < 2) return !1;
122
+ const i = s.pop(), n = s.join(" ");
123
+ if (!i || !n) return !1;
124
+ try {
125
+ return t.matches(i) && !!t.closest(n);
126
+ } catch {
127
+ return !1;
80
128
  }
81
- this.state.pointer.x = e, this.state.pointer.y = i, this.state.hasReceivedInput || (this.state.hasReceivedInput = !0, this.state.target.x = this.state.smooth.x = e, this.state.target.y = this.state.smooth.y = i);
82
129
  }
83
- handleDown() {
84
- this.isEnabled && (this.state.isDown = !0);
130
+ isOutsideContainer(t) {
131
+ const { container: e } = this.options;
132
+ return !!e && e !== document.body && !e.contains(t);
85
133
  }
86
- handleUp() {
87
- this.isEnabled && (this.state.isDown = !1);
134
+ resolveComputedCursor(t) {
135
+ return window.getComputedStyle(t).cursor;
88
136
  }
89
- handleMouseOver(t) {
137
+ handleMove = (t) => {
138
+ if (this.options.autoDisableOnMobile && t.pointerType === "touch" && !this.options.enableTouch)
139
+ return;
140
+ let e = t.clientX, s = t.clientY;
141
+ const i = this.options.container;
142
+ i && this.containerRect && i !== document.body && (e -= this.containerRect.left, s -= this.containerRect.top), this.state.pointer.x = e, this.state.pointer.y = s, this.hasSeenPointer = !0, this.isEnabled && (this.state.hasReceivedInput || (this.state.hasReceivedInput = !0, this.state.target.x = this.state.smooth.x = e, this.state.target.y = this.state.smooth.y = s));
143
+ };
144
+ handleDown = () => {
145
+ this.isEnabled && (this.state.isDown = !0);
146
+ };
147
+ handleUp = () => {
148
+ this.isEnabled && (this.state.isDown = !1);
149
+ };
150
+ handleMouseOver = (t) => {
90
151
  if (!this.isEnabled) return;
91
152
  const e = t.target;
92
- if (e.closest(`[${this.ignoreAttribute}]`)) {
93
- this.state.isNative = !0;
153
+ if (this.isOutsideContainer(e)) return;
154
+ if (this.state.cursorMode === "auto" && e.closest(`[${this.ignoreAttribute}]`)) {
155
+ this.state.isHover = !1, this.state.hoverTarget = null, this.state.interaction = {}, this.currentTarget = null, this.lastParsedTarget = null, this.matchedRules = [], this.state.isNative = !0, this.nativeTarget = e;
94
156
  return;
95
157
  }
96
- const i = this.getHoverSelector(), a = e.closest(i);
97
- a && (this.state.isHover = !0, this.state.hoverTarget = a, this.parseDOMInteraction(this.state.hoverTarget));
98
- const s = this.options.ignoreOnNative;
99
- if (s && s !== null) {
100
- const o = s === "auto" || s === "tag", r = s === "auto" || s === "css";
101
- let u = !1;
102
- if (o) {
103
- const h = e.localName;
104
- (h === "input" || h === "textarea" || h === "select" || e.isContentEditable) && (u = !0);
105
- }
106
- if (!u && r) {
107
- const h = window.getComputedStyle(e).cursor;
108
- ["default", "auto", "pointer", "none", "inherit"].includes(h) || (u = !0);
109
- }
110
- u && (this.state.isNative = !0);
158
+ if (this.state.isNative = !1, this.nativeTarget = null, this.currentTarget = e, this.parseDOMInteraction(e), this.state.cursorMode !== "auto") {
159
+ const r = e.closest(this.getHoverSelector());
160
+ r && (this.state.isHover = !0, this.state.hoverTarget = r);
161
+ return;
111
162
  }
112
- }
113
- handleMouseOut(t) {
163
+ const s = e.closest(this.getHoverSelector());
164
+ s && (this.state.isHover = !0, this.state.hoverTarget = s);
165
+ const i = g.has(e.localName) || e.isContentEditable, n = !f.has(this.resolveComputedCursor(e));
166
+ (i || n) && (this.state.isNative = !0, this.nativeTarget = e);
167
+ };
168
+ handleMouseOut = (t) => {
114
169
  if (!this.isEnabled) return;
115
- const e = t.target;
116
- (e === this.state.hoverTarget || e.contains(this.state.hoverTarget)) && (!t.relatedTarget || !this.state.hoverTarget?.contains(t.relatedTarget)) && (this.state.isHover = !1, this.state.hoverTarget = null, this.state.interaction = {}), this.state.isNative && (this.state.isNative = !1);
117
- }
118
- handleWindowLeave() {
119
- this.options.hideOnLeave && (this.state.hasReceivedInput = !1, this.state.pointer = { ...l });
120
- }
170
+ const e = t.target, s = t.relatedTarget;
171
+ this.isOutsideContainer(e) || ((e === this.state.hoverTarget || e.contains(this.state.hoverTarget)) && (!s || !this.state.hoverTarget?.contains(s)) && (this.state.isHover = !1, this.state.hoverTarget = null), this.nativeTarget && (e === this.nativeTarget || e.contains(this.nativeTarget)) && (!s || !this.nativeTarget.contains(s)) && (this.state.isNative = !1, this.nativeTarget = null), e === this.currentTarget && (this.currentTarget = null, this.lastParsedTarget = null, this.matchedRules = []));
172
+ };
173
+ /**
174
+ * Handles the case where the pointer leaves the entire document.
175
+ * This is more reliable than the `mouseleave` event on `document`.
176
+ */
177
+ handleDocumentMouseOut = (t) => {
178
+ this.isEnabled && t.relatedTarget === null && this.options.hideOnLeave && this.handleWindowLeave();
179
+ };
180
+ handleWindowLeave = () => {
181
+ this.options.hideOnLeave && (this.state.hasReceivedInput = !1, this.state.pointer = { ...h });
182
+ };
121
183
  clearHover() {
122
- this.state.isHover = !1, this.state.hoverTarget = null, this.state.isNative = !1, this.state.interaction = {};
184
+ this.state.isHover = !1, this.state.hoverTarget = null, this.state.isNative = !1, this.nativeTarget = null, this.state.interaction = {}, this.currentTarget = null, this.lastParsedTarget = null, this.matchedRules = [];
185
+ }
186
+ /** Returns the raw element currently under the pointer. */
187
+ getCurrentTarget() {
188
+ return this.currentTarget;
123
189
  }
124
190
  bindEvents() {
125
191
  const { signal: t } = this.abortController;
126
- window.addEventListener("pointermove", this.handleMove.bind(this), { passive: !0, signal: t }), window.addEventListener("pointerdown", this.handleDown.bind(this), { passive: !0, signal: t }), window.addEventListener("pointerup", this.handleUp.bind(this), { signal: t }), document.addEventListener("mouseover", this.handleMouseOver.bind(this), { signal: t }), document.addEventListener("mouseout", this.handleMouseOut.bind(this), { signal: t }), document.addEventListener("mouseleave", this.handleWindowLeave.bind(this), { signal: t });
192
+ window.addEventListener("pointermove", this.handleMove, { passive: !0, signal: t }), window.addEventListener("pointerdown", this.handleDown, { passive: !0, signal: t }), window.addEventListener("pointerup", this.handleUp, { signal: t });
193
+ const s = !this.options.container || this.options.container === document.body ? document : this.options.container;
194
+ s.addEventListener("mouseover", this.handleMouseOver, { signal: t }), s.addEventListener("mouseout", this.handleMouseOut, { signal: t }), document.addEventListener("mouseout", this.handleDocumentMouseOut, { signal: t });
127
195
  }
128
196
  destroy() {
129
- this.abortController.abort();
197
+ this.abortController.abort(), this.resizeObserver?.disconnect();
130
198
  }
131
199
  }
132
- let g = 0;
133
- class v {
134
- constructor(t = document.body, e) {
135
- if (this.container = t, this.hideNativeCursor = e, !t || !(t instanceof HTMLElement))
200
+ let b = 0;
201
+ class y {
202
+ constructor(t = document.body, e = 9999) {
203
+ if (this.container = t, this.zIndex = e, !t || !(t instanceof HTMLElement))
136
204
  throw new Error(`[Supermouse] Invalid container: ${t}. Must be an HTMLElement.`);
137
- const i = g++;
138
- this.id = `supermouse-style-${i}`, this.scopeClass = `supermouse-scope-${i}`;
139
- const a = t === document.body;
205
+ t.isConnected || console.warn(
206
+ "[Supermouse] container is not attached to the document — stage sizing/positioning will be wrong until it is."
207
+ );
208
+ const s = b++;
209
+ this.id = `supermouse-style-${s}`, this.scopeClass = `supermouse-scope-${s}`, this.hideClass = `supermouse-hide-${s}`;
210
+ const i = t === document.body;
140
211
  if (this.element = document.createElement("div"), Object.assign(this.element.style, {
141
- position: a ? "fixed" : "absolute",
212
+ position: i ? "fixed" : "absolute",
142
213
  inset: "0px",
143
214
  pointerEvents: "none",
144
- zIndex: "9999",
215
+ zIndex: String(this.zIndex),
145
216
  opacity: "1",
146
217
  transition: "opacity 0.15s ease"
147
- }), !a) {
148
- const s = window.getComputedStyle(t);
149
- this.originalContainerPosition = s.position, s.position === "static" && (t.style.position = "relative");
218
+ }), !i) {
219
+ const n = window.getComputedStyle(t);
220
+ this.originalContainerPosition = n.position, n.position === "static" && (t.style.position = "relative");
150
221
  }
151
- this.originalContainerCursor = t.style.cursor, t.appendChild(this.element), this.styleTag = document.createElement("style"), this.styleTag.id = this.id, document.head.appendChild(this.styleTag), this.container.classList.add(this.scopeClass), this.hideNativeCursor && this.setNativeCursor("none");
222
+ this.originalContainerCursor = t.style.cursor, t.appendChild(this.element), this.styleTag = document.createElement("style"), this.styleTag.id = this.id, document.head.appendChild(this.styleTag), this.container.classList.add("supermouse-scope", this.scopeClass), this.updateCursorCSS();
152
223
  }
153
224
  container;
154
- hideNativeCursor;
155
- /** The container element appended to the document. */
225
+ zIndex;
156
226
  element;
157
227
  styleTag;
158
228
  id;
159
229
  scopeClass;
230
+ hideClass;
160
231
  currentCursorState = null;
161
232
  originalContainerPosition = "";
162
233
  originalContainerCursor = "";
234
+ /** Selectors that need explicit `cursor: none !important` to override UA styles. */
163
235
  selectors = /* @__PURE__ */ new Set([
164
236
  "a",
165
237
  "button",
@@ -169,23 +241,26 @@ class v {
169
241
  '[role="button"]',
170
242
  "[tabindex]"
171
243
  ]);
172
- /**
173
- * Adds a new CSS selector to the `selectors` set.
174
- * Called by `Supermouse` and subsequently plugins during install to ensure
175
- * the native cursor is hidden on their specific interactive targets.
176
- */
244
+ /** Batch add selectors. Comma‑separated groups are split and scoped individually. */
245
+ addSelectors(t) {
246
+ let e = !1;
247
+ for (const s of t)
248
+ s.split(",").forEach((i) => {
249
+ const n = i.trim();
250
+ n && !this.selectors.has(n) && (this.selectors.add(n), e = !0);
251
+ });
252
+ e && this.updateCursorCSS();
253
+ }
254
+ /** Add a single selector (or comma‑separated group). */
177
255
  addSelector(t) {
178
- this.selectors.add(t), this.hideNativeCursor && this.updateCursorCSS();
256
+ this.addSelectors([t]);
179
257
  }
180
258
  setVisibility(t) {
181
259
  this.element.style.opacity = t ? "1" : "0";
182
260
  }
183
- /**
184
- * Toggles the visibility of the native cursor via CSS injection.
185
- * @param type 'none' to hide, 'auto' to show.
186
- */
261
+ /** Toggle native cursor visibility. */
187
262
  setNativeCursor(t) {
188
- !this.hideNativeCursor && t === "none" || t !== this.currentCursorState && (this.currentCursorState = t, t === "none" ? (this.container.style.cursor = "none", this.updateCursorCSS()) : (this.container.style.cursor = "", this.styleTag.innerText = ""));
263
+ t !== this.currentCursorState && (this.currentCursorState = t, this.container.classList.toggle(this.hideClass, t === "none"), this.container.style.cursor = t === "none" ? "none" : this.originalContainerCursor);
189
264
  }
190
265
  updateCursorCSS() {
191
266
  const t = Array.from(this.selectors);
@@ -193,227 +268,241 @@ class v {
193
268
  this.styleTag.innerText = "";
194
269
  return;
195
270
  }
196
- const e = t.map((i) => `.${this.scopeClass} ${i}`).join(", ");
271
+ const e = `:not(.${this.scopeClass} .supermouse-scope):not(.${this.scopeClass} .supermouse-scope *)`, s = (a) => `.${this.scopeClass}.${this.hideClass} ${a}${e} { cursor: none !important; }`, i = t.map(s).join(`
272
+ `), n = `.${this.scopeClass}.${this.hideClass} *${e} { cursor: none !important; }`, r = `.${this.scopeClass}.${this.hideClass} { cursor: none !important; }`;
197
273
  this.styleTag.innerText = `
198
- ${e} {
199
- cursor: none !important;
200
- }
201
- `;
274
+ ${r}
275
+ ${n}
276
+ ${i}
277
+ ${s("label")}
278
+ ${s("select")}
279
+ ${s('input[type="range"]::-webkit-slider-thumb')}
280
+ ${s('input[type="range"]::-moz-range-thumb')}
281
+ `;
202
282
  }
203
283
  destroy() {
204
- this.element.remove(), this.styleTag.remove(), this.container.style.cursor = this.originalContainerCursor, this.container.classList.remove(this.scopeClass), this.container !== document.body && this.originalContainerPosition === "static" && (this.container.style.position = "");
284
+ this.element.remove(), this.styleTag.remove(), this.container.style.cursor = this.originalContainerCursor, this.container.classList.remove("supermouse-scope", this.scopeClass, this.hideClass), this.container !== document.body && this.originalContainerPosition === "static" && (this.container.style.position = "");
205
285
  }
206
286
  }
207
- const l = { x: -100, y: -100 }, m = [
208
- "a",
209
- "button",
210
- "input",
211
- "textarea",
212
- "[data-hover]",
213
- "[data-cursor]"
214
- ];
215
- class y {
216
- static version = d;
217
- version = d;
287
+ class C {
288
+ static version = c;
289
+ version = c;
218
290
  state;
219
- /**
220
- * Configuration options.
221
- */
291
+ /** Configuration options, fully resolved with defaults applied. */
222
292
  options;
223
293
  plugins = [];
224
- stage;
294
+ _stage;
225
295
  input;
226
296
  rafId = 0;
227
297
  lastTime = 0;
228
298
  isRunning = !1;
299
+ isSuspended = !1;
300
+ visibilityAbortController = new AbortController();
229
301
  hoverSelectors;
230
- /**
231
- * Creates a new Supermouse instance.
232
- *
233
- * @param options - Global configuration options.
234
- * @throws Will throw if running in a non-browser environment (window/document undefined).
235
- */
302
+ hoverSelectorString;
303
+ crashedPlugins = [];
236
304
  constructor(t = {}) {
237
305
  this.options = {
238
306
  smoothness: 0.15,
239
307
  enableTouch: !1,
240
308
  autoDisableOnMobile: !0,
241
- ignoreOnNative: "auto",
242
- hideCursor: !0,
309
+ cursor: "auto",
243
310
  hideOnLeave: !0,
244
311
  autoStart: !0,
245
312
  container: document.body,
246
313
  dataPrefix: "supermouse",
314
+ zIndex: 9999,
247
315
  ...t
248
316
  }, this.state = {
249
- pointer: { x: -100, y: -100 },
250
- target: { x: -100, y: -100 },
251
- smooth: { x: -100, y: -100 },
317
+ pointer: { ...h },
318
+ target: { ...h },
319
+ smooth: { ...h },
252
320
  velocity: { x: 0, y: 0 },
321
+ displacement: { x: 0, y: 0 },
253
322
  angle: 0,
254
323
  isDown: !1,
255
324
  isHover: !1,
256
325
  isNative: !1,
257
- forcedCursor: null,
326
+ cursorMode: this.options.cursor,
258
327
  hoverTarget: null,
259
328
  reducedMotion: !1,
260
329
  hasReceivedInput: !1,
261
330
  shape: null,
262
331
  interaction: {}
263
- }, this.options.hoverSelectors ? this.hoverSelectors = new Set(this.options.hoverSelectors) : this.hoverSelectors = new Set(m), this.stage = new v(this.options.container, !!this.options.hideCursor), this.hoverSelectors.forEach((e) => this.stage.addSelector(e)), this.input = new f(
332
+ }, this.hoverSelectors = new Set(this.options.hoverSelectors ?? m), this.hoverSelectorString = Array.from(this.hoverSelectors).join(", "), this._stage = new y(this.options.container, this.options.zIndex), this._stage.addSelectors(this.hoverSelectors), this.input = new v(
264
333
  this.state,
265
334
  this.options,
266
- () => Array.from(this.hoverSelectors).join(", "),
335
+ () => this.hoverSelectorString,
267
336
  (e) => {
268
337
  e || this.reset(!0);
269
338
  }
270
- ), this.options.plugins && this.options.plugins.forEach((e) => this.use(e)), this.init();
339
+ ), this.options.plugins?.forEach((e) => this.use(e)), this.bindVisibilityHandling(), this.init();
271
340
  }
272
- /**
273
- * Retrieves a registered plugin instance by its unique name.
274
- */
341
+ /** Look up a registered plugin by name. */
275
342
  getPlugin(t) {
276
343
  return this.plugins.find((e) => e.name === t);
277
344
  }
278
- /**
279
- * Returns whether the cursor system is currently enabled (processing input).
280
- */
345
+ /** Whether the instance is not disabled/suspended and is processing input. */
281
346
  get isEnabled() {
282
347
  return this.input.isEnabled;
283
348
  }
284
- /**
285
- * Enables a specific plugin by name.
286
- * Triggers the `onEnable` lifecycle hook of the plugin.
287
- */
349
+ /** Enable a plugin by name. */
288
350
  enablePlugin(t) {
289
351
  const e = this.getPlugin(t);
290
- e && e.isEnabled === !1 && (e.isEnabled = !0, e.onEnable?.(this));
352
+ e && e.isEnabled === !1 && (e.isEnabled = !0, e.element && (e.element.style.display = ""), e.onEnable?.(this));
291
353
  }
292
- /**
293
- * Disables a specific plugin by name.
294
- * Triggers the `onDisable` lifecycle hook.
295
- */
354
+ /** Disable a plugin by name and hide its element. */
296
355
  disablePlugin(t) {
297
356
  const e = this.getPlugin(t);
298
- e && e.isEnabled !== !1 && (e.isEnabled = !1, e.onDisable?.(this));
357
+ if (e && e.isEnabled !== !1) {
358
+ e.isEnabled = !1;
359
+ const s = () => {
360
+ e.element && (e.element.style.display = "none"), e.onDisable?.(this);
361
+ }, i = e.onBeforeDisable?.(this);
362
+ i && typeof i.then == "function" ? Promise.resolve(i).then(s).catch((n) => {
363
+ console.error(`[Supermouse] Plugin '${e.name}' onBeforeDisable threw:`, n), s();
364
+ }) : s();
365
+ }
299
366
  }
300
- /**
301
- * Toggles the enabled state of a plugin.
302
- */
367
+ /** Toggle a plugin's enabled state by name. */
303
368
  togglePlugin(t) {
304
369
  const e = this.getPlugin(t);
305
370
  e && (e.isEnabled === !1 ? this.enablePlugin(t) : this.disablePlugin(t));
306
371
  }
372
+ /** Add a selector to hover detection and cursor suppression. */
307
373
  registerHoverTarget(t) {
308
- this.hoverSelectors.has(t) || (this.hoverSelectors.add(t), this.stage.addSelector(t));
374
+ this.hoverSelectors.has(t) || (this.hoverSelectors.add(t), this.hoverSelectorString = Array.from(this.hoverSelectors).join(", "), this._stage.addSelector(t));
309
375
  }
310
- /**
311
- * The fixed container element where plugins should append their DOM nodes.
312
- */
376
+ /** The DOM element the instance is scoped to. */
313
377
  get container() {
314
- return this.stage.element;
378
+ return this.options.container;
315
379
  }
316
- /**
317
- * Sets the native cursor visibility.
318
- *
319
- * @param mode
320
- */
321
- setNativeCursor(t) {
322
- this.state.forcedCursor = t === "auto" ? null : t === "hide" ? "none" : "auto";
380
+ /** The stage element that plugins append their visuals into. */
381
+ get stage() {
382
+ return this._stage.element;
383
+ }
384
+ /** Set the current cursor mode. */
385
+ setCursor(t) {
386
+ this.state.cursorMode = t;
323
387
  }
324
388
  init() {
325
389
  this.options.autoStart && this.startLoop();
326
390
  }
391
+ /** Re‑enable input processing and re‑apply cursor state. */
327
392
  enable() {
328
- this.input.isEnabled = !0, this.stage.setNativeCursor("none");
393
+ this.input.isEnabled = !0, this.input.hasSeenPointer && (this.state.target.x = this.state.smooth.x = this.state.pointer.x, this.state.target.y = this.state.smooth.y = this.state.pointer.y, this.resetMotion(), this.state.hasReceivedInput = !0), this._stage.setNativeCursor(this.resolveCursorState());
329
394
  }
395
+ /** Disable input processing and restore native cursor. */
330
396
  disable() {
331
- this.input.isEnabled = !1, this.stage.setNativeCursor("auto"), this.reset(!0);
397
+ this.input.isEnabled = !1, this._stage.setNativeCursor("auto"), this._stage.setVisibility(!1), this.reset(!0);
398
+ }
399
+ /** Temporarily yield to a scoped instance. */
400
+ suspend() {
401
+ this.input.isEnabled && (this.isSuspended = !0, this.input.isEnabled = !1, this.input.clearHover(), this._stage.setVisibility(!1));
402
+ }
403
+ /** Resume from `suspend()`. */
404
+ resume() {
405
+ if (this.isSuspended) {
406
+ this.isSuspended = !1, this.input.isEnabled = !0, this.state.hasReceivedInput && (this.state.target.x = this.state.smooth.x = this.state.pointer.x, this.state.target.y = this.state.smooth.y = this.state.pointer.y, this.resetMotion());
407
+ for (let t = this.plugins.length - 1; t >= 0; t--)
408
+ this.runPluginSafe(this.plugins[t], 0);
409
+ this._stage.setVisibility(!0);
410
+ }
332
411
  }
333
- /**
334
- * Registers a new plugin.
335
- *
336
- * @param plugin - The plugin object to install.
337
- */
412
+ /** Register a new plugin. */
338
413
  use(t) {
339
- if (this.plugins.some((i) => i.name === t.name))
414
+ if (this.plugins.some((e) => e.name === t.name))
340
415
  return console.warn(`[Supermouse] Plugin "${t.name}" already installed.`), this;
341
- t.isEnabled === void 0 && (t.isEnabled = !0);
416
+ t.isEnabled ??= !0;
342
417
  try {
343
418
  t.install?.(this);
344
- } catch (i) {
345
- return console.error(`[Supermouse] Failed to install plugin '${t.name}'.`, i), this;
419
+ } catch (e) {
420
+ return console.error(`[Supermouse] Failed to install plugin '${t.name}'.`, e), this;
346
421
  }
347
- return this.plugins.push(t), this.plugins.sort((i, a) => (i.priority || 0) - (a.priority || 0)), this;
422
+ return this.plugins.push(t), this.plugins.sort((e, s) => (e.priority ?? 0) - (s.priority ?? 0)), this;
348
423
  }
424
+ /** Reset physics; optionally clear all input state. */
349
425
  reset(t = !1) {
350
- this.state.pointer = { ...l }, this.state.target = { ...l }, this.state.smooth = { ...l }, this.state.velocity = { x: 0, y: 0 }, this.state.angle = 0, t && (this.state.hasReceivedInput = !1, this.state.shape = null, this.state.interaction = {});
426
+ this.state.target = { ...h }, this.state.smooth = { ...h }, this.resetMotion(), this.state.angle = 0, t && (this.state.hasReceivedInput = !1, this.state.shape = null, this.state.interaction = {});
351
427
  }
352
428
  startLoop() {
353
- this.isRunning || (this.isRunning = !0, this.lastTime = performance.now(), this.tick(this.lastTime));
429
+ this.isRunning || (this.isRunning = !0, !document.hidden && (this.lastTime = performance.now(), this.rafId = requestAnimationFrame(this.tick)));
354
430
  }
355
- /**
356
- * Starts the animation loop. This is automatically called if `autoStart` is true.
357
- * Plugins can call this method to resume the loop if it has been stopped.
358
- */
431
+ /** Start the animation loop. */
359
432
  start() {
360
433
  this.startLoop();
361
434
  }
362
- /**
363
- * Manually steps the animation loop.
364
- *
365
- * @param time Current timestamp in milliseconds.
366
- */
435
+ /** Manually step the animation loop. */
367
436
  step(t) {
368
- this.tick(t);
437
+ this.update(t);
369
438
  }
370
439
  runPluginSafe(t, e) {
371
440
  if (t.isEnabled !== !1)
372
441
  try {
373
442
  t.update?.(this, e);
374
- } catch (i) {
375
- console.error(`[Supermouse] Plugin '${t.name}' crashed and has been disabled.`, i);
376
- const a = this.plugins.indexOf(t);
377
- a > -1 && this.plugins.splice(a, 1), t.isEnabled = !1;
443
+ } catch (s) {
444
+ console.error(`[Supermouse] Plugin '${t.name}' crashed and has been disabled.`, s), t.isEnabled = !1, this.crashedPlugins.push(t);
445
+ }
446
+ }
447
+ cleanupCrashedPlugins() {
448
+ if (this.crashedPlugins.length !== 0) {
449
+ for (const t of this.crashedPlugins) {
450
+ const e = this.plugins.indexOf(t);
451
+ e > -1 && this.plugins.splice(e, 1);
378
452
  try {
379
- t.destroy?.(this), t.onDisable?.(this);
453
+ t.onDisable?.(this), t.destroy?.(this);
380
454
  } catch (s) {
381
455
  console.error(`[Supermouse] Failed to cleanup crashed plugin '${t.name}'.`, s);
382
456
  }
457
+ t.element?.remove();
383
458
  }
384
- }
385
- /**
386
- * Runs on every animation frame.
387
- */
388
- tick = (t) => {
389
- const e = t - this.lastTime, i = Math.min(e / 1e3, 0.1);
390
- this.lastTime = t, this.state.hoverTarget && !this.state.hoverTarget.isConnected && this.input.clearHover();
391
- const a = this.input.isEnabled && !this.state.isNative && this.state.hasReceivedInput;
392
- if (this.stage.setVisibility(a), this.input.isEnabled && this.options.hideCursor) {
393
- let s = "auto";
394
- this.state.forcedCursor !== null ? s = this.state.forcedCursor : s = this.state.isNative || !this.state.hasReceivedInput ? "auto" : "none", this.stage.setNativeCursor(s);
459
+ this.crashedPlugins = [];
395
460
  }
396
- this.input.isEnabled && this.state.hasReceivedInput ? (this.state.target.x = this.state.pointer.x, this.state.target.y = this.state.pointer.y) : this.state.target = { ...l };
397
- for (let s = 0; s < this.plugins.length; s++)
398
- this.runPluginSafe(this.plugins[s], e);
399
- if (this.input.isEnabled) {
400
- const s = this.state.reducedMotion ? 1e3 : 1 / this.options.smoothness * 2;
401
- this.state.smooth.x = c(this.state.smooth.x, this.state.target.x, s, i), this.state.smooth.y = c(this.state.smooth.y, this.state.target.y, s, i), this.state.velocity.x = this.state.target.x - this.state.smooth.x, this.state.velocity.y = this.state.target.y - this.state.smooth.y;
402
- const { x: o, y: r } = this.state.velocity;
403
- (Math.abs(o) > 0.1 || Math.abs(r) > 0.1) && (this.state.angle = Math.atan2(r, o) * (180 / Math.PI));
461
+ }
462
+ resolveStageVisibility() {
463
+ return this.state.cursorMode === "native" ? !1 : this.state.cursorMode === "both" ? this.input.isEnabled && this.state.hasReceivedInput : this.state.cursorMode === "custom" ? this.input.isEnabled && this.state.hasReceivedInput : this.input.isEnabled && !this.state.isNative && this.state.hasReceivedInput;
464
+ }
465
+ resolveCursorState() {
466
+ return !this.input.isEnabled || this.state.cursorMode === "both" || this.state.cursorMode === "native" ? "auto" : this.state.cursorMode === "custom" ? "none" : this.state.isNative || !this.state.hasReceivedInput ? "auto" : "none";
467
+ }
468
+ resetMotion() {
469
+ this.state.velocity = { x: 0, y: 0 }, this.state.displacement = { x: 0, y: 0 };
470
+ }
471
+ update(t) {
472
+ const e = t - this.lastTime, s = Math.min(e / 1e3, 0.1);
473
+ this.lastTime = t;
474
+ const i = this.input.getCurrentTarget();
475
+ i && !i.isConnected ? this.input.clearHover() : i && this.input.parseDOMInteraction(i), this._stage.setVisibility(this.resolveStageVisibility()), this.input.isEnabled && this._stage.setNativeCursor(this.resolveCursorState()), this.input.isEnabled && this.state.hasReceivedInput && (this.state.target.x = this.state.pointer.x, this.state.target.y = this.state.pointer.y);
476
+ for (let n = 0; n < this.plugins.length; n++)
477
+ this.runPluginSafe(this.plugins[n], e);
478
+ if (this.cleanupCrashedPlugins(), this.input.isEnabled) {
479
+ const n = this.state.reducedMotion ? 1e3 : 1 / this.options.smoothness * 2, r = this.state.smooth.x, a = this.state.smooth.y;
480
+ this.state.smooth.x = d(this.state.smooth.x, this.state.target.x, n, s), this.state.smooth.y = d(this.state.smooth.y, this.state.target.y, n, s), this.state.displacement.x = this.state.target.x - this.state.smooth.x, this.state.displacement.y = this.state.target.y - this.state.smooth.y, s > 0 ? (this.state.velocity.x = (this.state.smooth.x - r) / s, this.state.velocity.y = (this.state.smooth.y - a) / s) : (this.state.velocity.x = 0, this.state.velocity.y = 0);
481
+ const { x: l, y: u } = this.state.velocity;
482
+ (Math.abs(l) > 0.1 || Math.abs(u) > 0.1) && (this.state.angle = Math.atan2(u, l) * (180 / Math.PI));
404
483
  }
405
- this.isRunning && (this.rafId = requestAnimationFrame(this.tick));
484
+ }
485
+ tick = (t) => {
486
+ this.update(t), this.isRunning && (this.rafId = requestAnimationFrame(this.tick));
406
487
  };
407
- /**
408
- * Destroys the instance.
409
- */
488
+ /** Pause rAF loop when tab hidden; resume on visible. */
489
+ bindVisibilityHandling() {
490
+ document.addEventListener(
491
+ "visibilitychange",
492
+ () => {
493
+ this.isRunning && (document.hidden ? cancelAnimationFrame(this.rafId) : (this.lastTime = performance.now(), this.rafId = requestAnimationFrame(this.tick)));
494
+ },
495
+ { signal: this.visibilityAbortController.signal }
496
+ );
497
+ }
498
+ /** Destroy the instance, freeing all resources. */
410
499
  destroy() {
411
- this.isRunning = !1, cancelAnimationFrame(this.rafId), this.input.destroy(), this.stage.destroy(), this.plugins.forEach((t) => t.destroy?.(this)), this.plugins = [];
500
+ this.isRunning = !1, cancelAnimationFrame(this.rafId), this.visibilityAbortController.abort(), this.input.destroy(), this._stage.destroy(), this.plugins.forEach((t) => t.destroy?.(this)), this.plugins = [];
412
501
  }
413
502
  }
414
503
  export {
415
504
  m as DEFAULT_HOVER_SELECTORS,
416
- f as Input,
417
- v as Stage,
418
- y as Supermouse
505
+ v as Input,
506
+ y as Stage,
507
+ C as Supermouse
419
508
  };