@tanstack/hotkeys 0.11.0 → 0.12.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.
@@ -1,29 +1,42 @@
1
1
  import { normalizeKeyName } from "./constants.js";
2
2
  import { detectPlatform } from "./platform.js";
3
- import { parseHotkey, rawHotkeyToParsedHotkey } from "./parse.js";
3
+ import { normalizeKeyboardEvent } from "./_keyboard-event.js";
4
+ import { isHotkeySequence, isModifierKey, parseHotkey, rawHotkeyToParsedHotkey, simplifyHotkey } from "./parse.js";
4
5
  import { formatHotkey } from "./format.js";
5
6
  import { matchKeyboardEvent } from "./_match.js";
6
- import { areHotkeysEqual } from "./match.js";
7
+ import { DEFAULT_SEQUENCE_TIMEOUT, areHotkeysEqual } from "./match.js";
7
8
  import { isRecordingEvent } from "./_recording-guard.js";
8
9
  import { defaultHotkeyOptions, getDefaultIgnoreInputs, handleConflict, optionsEqual } from "./_registration.js";
9
10
  import { isEventForTarget, shouldIgnoreInputEvent } from "./_event-target.js";
10
11
  import { Store } from "@tanstack/store";
11
12
 
12
13
  //#region src/hotkey-manager.ts
13
- /**
14
- * Creates a public view from an internal registration,
15
- * stripping the callback function.
16
- */
14
+ /** Creates a public view from an internal registration, stripping the callback. */
17
15
  function toHotkeyRegistrationView(reg) {
18
- return {
19
- hotkey: reg.hotkey,
16
+ const base = {
20
17
  id: reg.id,
21
- options: reg.options,
22
- parsedHotkey: reg.parsedHotkey,
23
18
  target: reg.target,
24
19
  triggerCount: reg.triggerCount,
25
20
  hasFired: reg.hasFired
26
21
  };
22
+ if (reg.kind === "chord") return {
23
+ ...base,
24
+ kind: "chord",
25
+ hotkey: reg.hotkey,
26
+ parsedHotkey: reg.parsedHotkey,
27
+ options: reg.options
28
+ };
29
+ const length = reg.parsedSequence.length;
30
+ const inProgress = reg.currentIndex > 0 && reg.currentIndex < length;
31
+ return {
32
+ ...base,
33
+ kind: "sequence",
34
+ hotkey: reg.hotkey,
35
+ parsedSequence: reg.parsedSequence,
36
+ options: reg.options,
37
+ matchedStepCount: inProgress ? reg.currentIndex : 0,
38
+ partialMatchLastKeyTime: inProgress ? reg.lastKeyTime : 0
39
+ };
27
40
  }
28
41
  let registrationIdCounter = 0;
29
42
  /**
@@ -32,29 +45,55 @@ let registrationIdCounter = 0;
32
45
  function generateId() {
33
46
  return `hotkey_${++registrationIdCounter}`;
34
47
  }
48
+ /** Drops options that do not apply to a registration's kind. */
49
+ function optionsForKind(kind, options) {
50
+ if (kind === "chord") {
51
+ const { timeout: _timeout, ...rest } = options;
52
+ return rest;
53
+ }
54
+ const { requireReset: _requireReset, ...rest } = options;
55
+ return rest;
56
+ }
57
+ /** Steps with Mod already resolved for the registration's platform. */
58
+ function resolvedSteps(registration) {
59
+ return registration.kind === "chord" ? [registration.parsedHotkey] : registration.parsedSequence;
60
+ }
61
+ function sameResolvedSteps(left, right) {
62
+ return left.length === right.length && left.every((step, index) => areHotkeysEqual(step, right[index]));
63
+ }
64
+ function formatForMessage(hotkey) {
65
+ return isHotkeySequence(hotkey) ? hotkey.join(" ") : hotkey;
66
+ }
35
67
  /**
36
- * Singleton manager for hotkey registrations.
68
+ * Singleton manager for hotkey registrations, including sequences.
37
69
  *
38
- * This class provides a centralized way to register and manage keyboard hotkeys.
39
- * It uses a single event listener for efficiency, regardless of how many hotkeys
40
- * are registered.
70
+ * This class provides a centralized way to register and manage keyboard
71
+ * hotkeys. It attaches one keydown and one keyup listener per target,
72
+ * regardless of how many hotkeys are registered.
73
+ *
74
+ * Sequences (Vim-style hotkeys such as `['G', 'G']`) match when every step is
75
+ * pressed within the timeout. Modifier-only events (Shift, Control, Alt, or
76
+ * Meta alone) neither advance nor reset a sequence, so chained modifier chords
77
+ * such as `['Shift+R', 'Shift+T']` work.
41
78
  *
42
79
  * @example
43
80
  * ```ts
44
81
  * const manager = HotkeyManager.getInstance()
45
82
  *
46
- * const unregister = manager.register('Mod+S', (event, context) => {
83
+ * const save = manager.register('Mod+S', (event, context) => {
47
84
  * console.log('Save triggered!')
48
85
  * })
86
+ * const top = manager.register(['G', 'G'], () => scrollToTop(), { timeout: 500 })
49
87
  *
50
88
  * // Later, to unregister:
51
- * unregister()
89
+ * save.unregister()
90
+ * top.unregister()
52
91
  * ```
53
92
  */
54
93
  var HotkeyManager = class HotkeyManager {
55
94
  static #instance = null;
56
95
  /**
57
- * The TanStack Store containing all hotkey registrations.
96
+ * The TanStack Store containing all hotkey registrations, chords and sequences.
58
97
  * Use this to subscribe to registration changes or access current registrations.
59
98
  *
60
99
  * @example
@@ -73,6 +112,11 @@ var HotkeyManager = class HotkeyManager {
73
112
  * ```
74
113
  */
75
114
  registrations = new Store(/* @__PURE__ */ new Map());
115
+ /**
116
+ * The app hotkeys store published by a framework provider, so tools such as
117
+ * devtools can list declared hotkeys that are not registered.
118
+ */
119
+ appHotkeys = new Store(null);
76
120
  #platform;
77
121
  #targetListeners = /* @__PURE__ */ new Map();
78
122
  #targetRegistrations = /* @__PURE__ */ new Map();
@@ -95,32 +139,13 @@ var HotkeyManager = class HotkeyManager {
95
139
  HotkeyManager.#instance = null;
96
140
  }
97
141
  }
98
- /**
99
- * Registers a hotkey handler and returns a handle for updating the registration.
100
- *
101
- * The returned handle allows updating the callback and options without
102
- * re-registering, which is useful for avoiding stale closures in React.
103
- *
104
- * @param hotkey - The hotkey string (e.g., 'Mod+S') or RawHotkey object
105
- * @param callback - The function to call when the hotkey is pressed
106
- * @param options - Options for the hotkey behavior
107
- * @returns A handle for managing the registration
108
- *
109
- * @example
110
- * ```ts
111
- * const handle = manager.register('Mod+S', callback)
112
- *
113
- * // Update callback without re-registering (avoids stale closures)
114
- * handle.callback = newCallback
115
- *
116
- * // Update options
117
- * handle.setOptions({ enabled: false })
118
- *
119
- * // Unregister when done
120
- * handle.unregister()
121
- * ```
122
- */
123
- register(hotkey, callback, options = {}) {
142
+ /** Notifies subscribers after in-place updates such as counts and sequence progress. */
143
+ #notify() {
144
+ this.registrations.setState((prev) => new Map(prev));
145
+ }
146
+ register(input, callback, options = {}) {
147
+ const hotkey = simplifyHotkey(input);
148
+ if (isHotkeySequence(hotkey) && hotkey.length === 0) throw new Error("Sequence must contain at least one hotkey");
124
149
  if (typeof document === "undefined" && !options.target) {
125
150
  let currentCallback = callback;
126
151
  return {
@@ -140,29 +165,51 @@ var HotkeyManager = class HotkeyManager {
140
165
  }
141
166
  const id = generateId();
142
167
  const platform = options.platform ?? this.#platform;
143
- const parsedHotkey = typeof hotkey === "string" ? parseHotkey(hotkey, platform) : rawHotkeyToParsedHotkey(hotkey, platform);
144
- const hotkeyStr = typeof hotkey === "string" ? hotkey : formatHotkey(parsedHotkey);
145
168
  const target = options.target ?? document;
146
- const conflictBehavior = options.conflictBehavior ?? "warn";
147
- const conflictingRegistration = this.#findConflictingRegistration(hotkeyStr, target);
148
- if (conflictingRegistration) handleConflict(conflictingRegistration.id, hotkeyStr, conflictBehavior, (id) => this.#unregister(id));
149
- const resolvedIgnoreInputs = options.ignoreInputs ?? getDefaultIgnoreInputs(parsedHotkey);
150
- const registration = {
151
- id,
152
- hotkey: hotkeyStr,
153
- parsedHotkey,
154
- callback,
155
- options: {
156
- ...defaultHotkeyOptions,
157
- requireReset: false,
158
- ...options,
159
- platform,
160
- ignoreInputs: resolvedIgnoreInputs
161
- },
162
- hasFired: false,
163
- triggerCount: 0,
164
- target
165
- };
169
+ let registration;
170
+ if (isHotkeySequence(hotkey)) {
171
+ const parsedSequence = hotkey.map((step) => parseHotkey(step, platform));
172
+ registration = {
173
+ kind: "sequence",
174
+ id,
175
+ hotkey,
176
+ parsedSequence,
177
+ callback,
178
+ options: {
179
+ ...defaultHotkeyOptions,
180
+ timeout: DEFAULT_SEQUENCE_TIMEOUT,
181
+ ...optionsForKind("sequence", options),
182
+ platform,
183
+ ignoreInputs: options.ignoreInputs ?? getDefaultIgnoreInputs(parsedSequence[0])
184
+ },
185
+ target,
186
+ currentIndex: 0,
187
+ lastKeyTime: 0,
188
+ triggerCount: 0,
189
+ hasFired: false
190
+ };
191
+ } else {
192
+ const parsedHotkey = typeof hotkey === "string" ? parseHotkey(hotkey, platform) : rawHotkeyToParsedHotkey(hotkey, platform);
193
+ registration = {
194
+ kind: "chord",
195
+ id,
196
+ hotkey: typeof hotkey === "string" ? hotkey : formatHotkey(parsedHotkey),
197
+ parsedHotkey,
198
+ callback,
199
+ options: {
200
+ ...defaultHotkeyOptions,
201
+ requireReset: false,
202
+ ...optionsForKind("chord", options),
203
+ platform,
204
+ ignoreInputs: options.ignoreInputs ?? getDefaultIgnoreInputs(parsedHotkey)
205
+ },
206
+ target,
207
+ hasFired: false,
208
+ triggerCount: 0
209
+ };
210
+ }
211
+ const conflicting = this.#findConflictingRegistration(registration);
212
+ if (conflicting) handleConflict(conflicting.id, formatForMessage(registration.hotkey), options.conflictBehavior ?? "warn", (conflictId) => this.#unregister(conflictId));
166
213
  this.registrations.setState((prev) => new Map(prev).set(id, registration));
167
214
  if (!this.#targetRegistrations.has(target)) this.#targetRegistrations.set(target, /* @__PURE__ */ new Set());
168
215
  this.#targetRegistrations.get(target).add(id);
@@ -185,21 +232,16 @@ var HotkeyManager = class HotkeyManager {
185
232
  setOptions: (newOptions) => {
186
233
  manager.registrations.setState((prev) => {
187
234
  const reg = prev.get(id);
188
- if (reg && !optionsEqual(reg.options, {
235
+ if (!reg) return prev;
236
+ const options = {
189
237
  ...reg.options,
190
- ...newOptions
191
- })) {
192
- const next = new Map(prev);
193
- next.set(id, {
194
- ...reg,
195
- options: {
196
- ...reg.options,
197
- ...newOptions
198
- }
199
- });
200
- return next;
201
- }
202
- return prev;
238
+ ...optionsForKind(reg.kind, newOptions)
239
+ };
240
+ if (optionsEqual(reg.options, options)) return prev;
241
+ return new Map(prev).set(id, {
242
+ ...reg,
243
+ options
244
+ });
203
245
  });
204
246
  },
205
247
  get isActive() {
@@ -231,13 +273,13 @@ var HotkeyManager = class HotkeyManager {
231
273
  #ensureListenersForTarget(target) {
232
274
  if (typeof document === "undefined") return;
233
275
  if (this.#targetListeners.has(target)) return;
234
- const keydownHandler = this.#createTargetKeyDownHandler(target);
235
- const keyupHandler = this.#createTargetKeyUpHandler(target);
236
- target.addEventListener("keydown", keydownHandler);
237
- target.addEventListener("keyup", keyupHandler);
276
+ const keydown = (event) => this.#processTargetEvent(event, target, "keydown");
277
+ const keyup = (event) => this.#processTargetEvent(event, target, "keyup");
278
+ target.addEventListener("keydown", keydown);
279
+ target.addEventListener("keyup", keyup);
238
280
  this.#targetListeners.set(target, {
239
- keydown: keydownHandler,
240
- keyup: keyupHandler
281
+ keydown,
282
+ keyup
241
283
  });
242
284
  }
243
285
  /**
@@ -252,94 +294,146 @@ var HotkeyManager = class HotkeyManager {
252
294
  this.#targetListeners.delete(target);
253
295
  this.#targetRegistrations.delete(target);
254
296
  }
297
+ /** Registrations of one kind on a target, in registration order. */
298
+ #registrationsOn(target, kind) {
299
+ const result = [];
300
+ for (const id of this.#targetRegistrations.get(target) ?? []) {
301
+ const registration = this.registrations.state.get(id);
302
+ if (registration?.kind === kind) result.push(registration);
303
+ }
304
+ return result;
305
+ }
255
306
  /**
256
307
  * Processes keyboard events for a specific target and event type.
308
+ * Chords and sequences match independently; both may fire for one event.
257
309
  */
258
310
  #processTargetEvent(event, target, eventType) {
259
- if (eventType === "keyup") for (const id of this.#targetRegistrations.get(target) ?? []) {
260
- const registration = this.registrations.state.get(id);
261
- if (registration?.hasFired && this.#shouldResetRegistration(registration, event)) {
311
+ this.#processChords(event, target, eventType);
312
+ this.#processSequences(event, target, eventType);
313
+ }
314
+ #processChords(event, target, eventType) {
315
+ const registrations = this.#registrationsOn(target, "chord");
316
+ if (eventType === "keyup") {
317
+ for (const registration of registrations) if (registration.hasFired && this.#shouldResetRegistration(registration, event)) {
262
318
  registration.hasFired = false;
263
319
  registration.activeMatch = void 0;
264
320
  }
265
321
  }
266
322
  if (isRecordingEvent(event)) return;
267
- const targetRegs = this.#targetRegistrations.get(target);
268
- if (!targetRegs) return;
269
323
  const matches = /* @__PURE__ */ new Map();
270
324
  let bestScore = 0;
271
- for (const id of targetRegs) {
272
- const registration = this.registrations.state.get(id);
273
- if (!registration || !registration.options.enabled || registration.options.eventType !== eventType || !isEventForTarget(event, target) || registration.options.ignoreInputs !== false && shouldIgnoreInputEvent(event, target, registration.target)) continue;
325
+ for (const registration of registrations) {
326
+ if (!registration.options.enabled || registration.options.eventType !== eventType || !isEventForTarget(event, target) || registration.options.ignoreInputs !== false && shouldIgnoreInputEvent(event, target, registration.target)) continue;
274
327
  const result = matchKeyboardEvent(event, registration.parsedHotkey, registration.options.platform);
275
328
  if (result.matched) {
276
- matches.set(id, result);
329
+ matches.set(registration.id, result);
277
330
  bestScore = Math.max(bestScore, result.score);
278
331
  }
279
332
  }
280
- for (const id of targetRegs) {
281
- const registration = this.registrations.state.get(id);
282
- if (!registration) continue;
283
- if (!isEventForTarget(event, target)) continue;
284
- if (!registration.options.enabled) continue;
285
- if (registration.options.ignoreInputs !== false) {
286
- if (shouldIgnoreInputEvent(event, target, registration.target)) continue;
333
+ for (const registration of registrations) {
334
+ const match = matches.get(registration.id);
335
+ if (!match?.matched || match.score !== bestScore) continue;
336
+ if (eventType === "keyup") {
337
+ this.#executeCallback(registration, event);
338
+ continue;
287
339
  }
288
- if (eventType === "keydown") {
289
- if (registration.options.eventType !== "keydown") continue;
290
- const match = matches.get(id);
291
- if (match?.matched && match.score === bestScore) {
340
+ if (registration.options.preventDefault) event.preventDefault();
341
+ if (registration.options.stopPropagation) event.stopPropagation();
342
+ if (!registration.options.requireReset || !registration.hasFired) {
343
+ this.#executeCallback(registration, event);
344
+ if (registration.options.requireReset) {
345
+ registration.hasFired = true;
346
+ registration.activeMatch = match.identity;
347
+ }
348
+ }
349
+ }
350
+ }
351
+ #processSequences(event, target, eventType) {
352
+ const registrations = this.#registrationsOn(target, "sequence");
353
+ if (registrations.length === 0) return;
354
+ if (isRecordingEvent(event)) {
355
+ let changed = false;
356
+ for (const registration of registrations) if (registration.currentIndex > 0) {
357
+ registration.currentIndex = 0;
358
+ changed = true;
359
+ }
360
+ if (changed) this.#notify();
361
+ return;
362
+ }
363
+ if (event.type === "keydown" && event.repeat || normalizeKeyboardEvent(event).isComposing) return;
364
+ if (isModifierKey(normalizeKeyName(event.key))) return;
365
+ const now = Date.now();
366
+ let changed = false;
367
+ const candidates = /* @__PURE__ */ new Map();
368
+ let bestScore = 0;
369
+ for (const registration of registrations) {
370
+ if (!isEventForTarget(event, target) || !registration.options.enabled || registration.options.ignoreInputs !== false && shouldIgnoreInputEvent(event, target, registration.target) || registration.options.eventType !== eventType) continue;
371
+ const timeout = registration.options.timeout ?? 1e3;
372
+ if (registration.currentIndex > 0 && now - registration.lastKeyTime > timeout) {
373
+ registration.currentIndex = 0;
374
+ changed = true;
375
+ }
376
+ const expected = registration.parsedSequence[registration.currentIndex];
377
+ if (!expected) continue;
378
+ let match = matchKeyboardEvent(event, expected, registration.options.platform);
379
+ let nextIndex = registration.currentIndex + 1;
380
+ if (!match.matched && registration.currentIndex > 0) {
381
+ match = matchKeyboardEvent(event, registration.parsedSequence[0], registration.options.platform);
382
+ nextIndex = 1;
383
+ }
384
+ candidates.set(registration.id, {
385
+ score: match.score,
386
+ nextIndex
387
+ });
388
+ bestScore = Math.max(bestScore, match.score);
389
+ }
390
+ const completed = [];
391
+ for (const registration of registrations) {
392
+ const candidate = candidates.get(registration.id);
393
+ if (!candidate) continue;
394
+ if (candidate.score > 0 && candidate.score === bestScore) {
395
+ registration.lastKeyTime = now;
396
+ registration.currentIndex = candidate.nextIndex;
397
+ changed = true;
398
+ if (registration.currentIndex >= registration.parsedSequence.length) {
292
399
  if (registration.options.preventDefault) event.preventDefault();
293
400
  if (registration.options.stopPropagation) event.stopPropagation();
294
- if (!registration.options.requireReset || !registration.hasFired) {
295
- this.#executeHotkeyCallback(registration, event);
296
- if (registration.options.requireReset) {
297
- registration.hasFired = true;
298
- registration.activeMatch = match.identity;
299
- }
300
- }
401
+ registration.currentIndex = 0;
402
+ completed.push(registration);
301
403
  }
302
- } else if (registration.options.eventType === "keyup") {
303
- const match = matches.get(id);
304
- if (match?.matched && match.score === bestScore) this.#executeHotkeyCallback(registration, event);
404
+ } else if (registration.currentIndex > 0) {
405
+ registration.currentIndex = 0;
406
+ changed = true;
305
407
  }
306
408
  }
409
+ if (changed && completed.length === 0) this.#notify();
410
+ for (const registration of completed) this.#executeCallback(registration, event);
307
411
  }
308
412
  /**
309
413
  * Executes a hotkey callback with proper event handling.
310
414
  */
311
- #executeHotkeyCallback(registration, event) {
415
+ #executeCallback(registration, event) {
312
416
  if (registration.options.preventDefault) event.preventDefault();
313
417
  if (registration.options.stopPropagation) event.stopPropagation();
314
418
  registration.triggerCount++;
315
- this.registrations.setState((prev) => new Map(prev));
316
- const context = {
317
- hotkey: registration.hotkey,
318
- parsedHotkey: registration.parsedHotkey
319
- };
320
- registration.callback(event, context);
321
- }
322
- /**
323
- * Creates a keydown handler for a specific target.
324
- */
325
- #createTargetKeyDownHandler(target) {
326
- return (event) => {
327
- this.#processTargetEvent(event, target, "keydown");
328
- };
419
+ if (registration.kind === "sequence") registration.hasFired = true;
420
+ this.#notify();
421
+ registration.callback(event, this.#callbackContext(registration));
329
422
  }
330
- /**
331
- * Creates a keyup handler for a specific target.
332
- */
333
- #createTargetKeyUpHandler(target) {
334
- return (event) => {
335
- this.#processTargetEvent(event, target, "keyup");
423
+ #callbackContext(registration) {
424
+ return {
425
+ hotkey: registration.hotkey,
426
+ parsedHotkey: registration.kind === "chord" ? registration.parsedHotkey : registration.parsedSequence[registration.parsedSequence.length - 1]
336
427
  };
337
428
  }
338
429
  /**
339
- * Finds an existing registration with the same hotkey and target.
430
+ * Finds an existing registration on the same target whose steps resolve to
431
+ * the same identity. Each side keeps its own platform, so `Mod+S` on macOS
432
+ * and `Mod+S` on Windows are different bindings.
340
433
  */
341
- #findConflictingRegistration(hotkey, target) {
342
- for (const registration of this.registrations.state.values()) if (areHotkeysEqual(registration.hotkey, hotkey, registration.options.platform) && registration.target === target) return registration;
434
+ #findConflictingRegistration(candidate) {
435
+ const steps = resolvedSteps(candidate);
436
+ for (const registration of this.registrations.state.values()) if (registration.target === candidate.target && sameResolvedSteps(resolvedSteps(registration), steps)) return registration;
343
437
  return null;
344
438
  }
345
439
  /**
@@ -362,8 +456,19 @@ var HotkeyManager = class HotkeyManager {
362
456
  return false;
363
457
  }
364
458
  /**
459
+ * Resets the progress of every in-progress sequence.
460
+ */
461
+ resetSequenceProgress() {
462
+ for (const registration of this.registrations.state.values()) if (registration.kind === "sequence") {
463
+ registration.currentIndex = 0;
464
+ registration.lastKeyTime = 0;
465
+ }
466
+ this.#notify();
467
+ }
468
+ /**
365
469
  * Triggers a registration's callback programmatically from devtools.
366
- * Creates a synthetic KeyboardEvent and invokes the callback.
470
+ * Creates a synthetic KeyboardEvent from the chord (or a sequence's last step)
471
+ * and invokes the callback.
367
472
  *
368
473
  * @param id - The registration ID to trigger
369
474
  * @returns True if the registration was found and triggered
@@ -371,7 +476,8 @@ var HotkeyManager = class HotkeyManager {
371
476
  triggerRegistration(id) {
372
477
  const registration = this.registrations.state.get(id);
373
478
  if (!registration) return false;
374
- const parsed = registration.parsedHotkey;
479
+ const context = this.#callbackContext(registration);
480
+ const parsed = context.parsedHotkey;
375
481
  const syntheticEvent = new KeyboardEvent(registration.options.eventType ?? "keydown", {
376
482
  ...parsed.code !== void 0 ? { code: parsed.code } : { key: parsed.key },
377
483
  ctrlKey: parsed.ctrl,
@@ -382,15 +488,13 @@ var HotkeyManager = class HotkeyManager {
382
488
  cancelable: true
383
489
  });
384
490
  registration.triggerCount++;
385
- this.registrations.setState((prev) => new Map(prev));
386
- registration.callback(syntheticEvent, {
387
- hotkey: registration.hotkey,
388
- parsedHotkey: registration.parsedHotkey
389
- });
491
+ if (registration.kind === "sequence") registration.hasFired = true;
492
+ this.#notify();
493
+ registration.callback(syntheticEvent, context);
390
494
  return true;
391
495
  }
392
496
  /**
393
- * Gets the number of registered hotkeys.
497
+ * Gets the number of registered hotkeys, chords and sequences.
394
498
  */
395
499
  getRegistrationCount() {
396
500
  return this.registrations.state.size;
@@ -398,14 +502,12 @@ var HotkeyManager = class HotkeyManager {
398
502
  /**
399
503
  * Checks if a specific hotkey is registered.
400
504
  *
401
- * @param hotkey - The hotkey string to check
505
+ * @param hotkey - The chord or sequence to check
402
506
  * @param target - Optional target element to match (if provided, both hotkey and target must match)
403
507
  * @returns True if a matching registration exists
404
508
  */
405
509
  isRegistered(hotkey, target) {
406
- for (const registration of this.registrations.state.values()) if (areHotkeysEqual(registration.hotkey, hotkey, registration.options.platform)) {
407
- if (target === void 0 || registration.target === target) return true;
408
- }
510
+ for (const registration of this.registrations.state.values()) if (areHotkeysEqual(registration.hotkey, hotkey, registration.options.platform) && (target === void 0 || registration.target === target)) return true;
409
511
  return false;
410
512
  }
411
513
  /**
@@ -414,6 +516,7 @@ var HotkeyManager = class HotkeyManager {
414
516
  destroy() {
415
517
  for (const target of this.#targetListeners.keys()) this.#removeListenersForTarget(target);
416
518
  this.registrations.setState(() => /* @__PURE__ */ new Map());
519
+ this.appHotkeys.setState(() => null);
417
520
  this.#targetListeners.clear();
418
521
  this.#targetRegistrations.clear();
419
522
  }