{"version":3,"file":"key-state-tracker.cjs","names":["#instance","Store","#setupListeners","#keydownListener","normalizeKeyName","#heldKeysSet","#heldCodesMap","#syncState","#keyupListener","MODIFIER_KEYS","#blurListener","#removeListeners"],"sources":["../src/key-state-tracker.ts"],"sourcesContent":["import { Store } from '@tanstack/store'\nimport { MODIFIER_KEYS, normalizeKeyName } from './constants'\n\n/**\n * State interface for the KeyStateTracker.\n */\nexport interface KeyStateTrackerState {\n  /**\n   * Array of currently held key names (normalized, e.g. \"Control\", \"A\").\n   */\n  heldKeys: Array<string>\n  /**\n   * Map from normalized key name to the physical `event.code` (e.g. \"KeyA\", \"ShiftLeft\").\n   * Useful for debugging which physical key was pressed.\n   */\n  heldCodes: Record<string, string>\n}\n\n/**\n * Returns the default state for KeyStateTracker.\n */\nfunction getDefaultKeyStateTrackerState(): KeyStateTrackerState {\n  return {\n    heldKeys: [],\n    heldCodes: {},\n  }\n}\n\n/**\n * Singleton tracker for currently held keyboard keys.\n *\n * This class maintains a list of all keys currently being pressed,\n * which is useful for:\n * - Displaying currently held keys to users\n * - Custom shortcut recording for rebinding\n * - Complex chord detection\n *\n * State Management:\n * - Uses TanStack Store for reactive state management\n * - State can be accessed via `tracker.store.state` when using the class directly\n * - When using framework adapters (React), use `useHeldKeys` and `useHeldKeyCodes` hooks for reactive state\n *\n * @example\n * ```ts\n * const tracker = KeyStateTracker.getInstance()\n *\n * // Access state directly\n * console.log(tracker.store.state.heldKeys) // ['Control', 'Shift']\n *\n * // Subscribe to changes with TanStack Store\n * const unsubscribe = tracker.store.subscribe(() => {\n *   console.log('Currently held:', tracker.store.state.heldKeys)\n * })\n *\n * // Check current state\n * console.log(tracker.getHeldKeys()) // ['Control', 'Shift']\n * console.log(tracker.isKeyHeld('Control')) // true\n *\n * // Cleanup\n * unsubscribe()\n * ```\n */\nexport class KeyStateTracker {\n  static #instance: KeyStateTracker | null = null\n\n  /**\n   * The TanStack Store instance containing the tracker state.\n   * Use this to subscribe to state changes or access current state.\n   */\n  readonly store: Store<KeyStateTrackerState> = new Store(\n    getDefaultKeyStateTrackerState(),\n  )\n\n  #heldKeysSet: Set<string> = new Set()\n  #heldCodesMap: Map<string, string> = new Map()\n  #keydownListener: ((event: KeyboardEvent) => void) | null = null\n  #keyupListener: ((event: KeyboardEvent) => void) | null = null\n  #blurListener: (() => void) | null = null\n\n  private constructor() {\n    this.#setupListeners()\n  }\n\n  /**\n   * Gets the singleton instance of KeyStateTracker.\n   */\n  static getInstance(): KeyStateTracker {\n    if (!KeyStateTracker.#instance) {\n      KeyStateTracker.#instance = new KeyStateTracker()\n    }\n    return KeyStateTracker.#instance\n  }\n\n  /**\n   * Resets the singleton instance. Useful for testing.\n   */\n  static resetInstance(): void {\n    if (KeyStateTracker.#instance) {\n      KeyStateTracker.#instance.destroy()\n      KeyStateTracker.#instance = null\n    }\n  }\n\n  /**\n   * Sets up the keyboard event listeners.\n   */\n  #setupListeners(): void {\n    if (typeof document === 'undefined') {\n      return // SSR safety\n    }\n\n    this.#keydownListener = (event: KeyboardEvent) => {\n      const key = normalizeKeyName(event.key)\n      if (!this.#heldKeysSet.has(key)) {\n        this.#heldKeysSet.add(key)\n        this.#heldCodesMap.set(key, event.code)\n        this.#syncState()\n      }\n    }\n\n    this.#keyupListener = (event: KeyboardEvent) => {\n      const key = normalizeKeyName(event.key)\n      if (this.#heldKeysSet.has(key)) {\n        this.#heldKeysSet.delete(key)\n        this.#heldCodesMap.delete(key)\n      }\n\n      // When a modifier key is released, clear any non-modifier keys still\n      // marked as held. On macOS, the OS intercepts modifier+key combos\n      // (e.g. Cmd+S) and swallows the keyup event for the non-modifier key,\n      // leaving it permanently stuck in the held set.\n      if (MODIFIER_KEYS.has(key)) {\n        for (const heldKey of this.#heldKeysSet) {\n          if (!MODIFIER_KEYS.has(heldKey)) {\n            this.#heldKeysSet.delete(heldKey)\n            this.#heldCodesMap.delete(heldKey)\n          }\n        }\n      }\n\n      this.#syncState()\n    }\n\n    // Clear all keys when window loses focus (keys might be released while not focused)\n    this.#blurListener = () => {\n      if (this.#heldKeysSet.size > 0) {\n        this.#heldKeysSet.clear()\n        this.#heldCodesMap.clear()\n        this.#syncState()\n      }\n    }\n\n    document.addEventListener('keydown', this.#keydownListener)\n    document.addEventListener('keyup', this.#keyupListener)\n    window.addEventListener('blur', this.#blurListener)\n  }\n\n  /**\n   * Syncs the internal Set to the Store state.\n   */\n  #syncState(): void {\n    this.store.setState(() => ({\n      heldKeys: Array.from(this.#heldKeysSet),\n      heldCodes: Object.fromEntries(this.#heldCodesMap),\n    }))\n  }\n\n  /**\n   * Removes the keyboard event listeners.\n   */\n  #removeListeners(): void {\n    if (typeof document === 'undefined') {\n      return\n    }\n\n    if (this.#keydownListener) {\n      document.removeEventListener('keydown', this.#keydownListener)\n      this.#keydownListener = null\n    }\n\n    if (this.#keyupListener) {\n      document.removeEventListener('keyup', this.#keyupListener)\n      this.#keyupListener = null\n    }\n\n    if (this.#blurListener) {\n      window.removeEventListener('blur', this.#blurListener)\n      this.#blurListener = null\n    }\n  }\n\n  /**\n   * Gets an array of currently held key names.\n   *\n   * @returns Array of key names currently being pressed\n   */\n  getHeldKeys(): Array<string> {\n    return this.store.state.heldKeys\n  }\n\n  /**\n   * Checks if a specific key is currently being held.\n   *\n   * @param key - The key name to check (case-insensitive)\n   * @returns True if the key is currently held\n   */\n  isKeyHeld(key: string): boolean {\n    const normalizedKey = normalizeKeyName(key)\n    return this.#heldKeysSet.has(normalizedKey)\n  }\n\n  /**\n   * Checks if any of the given keys are currently held.\n   *\n   * @param keys - Array of key names to check\n   * @returns True if any of the keys are currently held\n   */\n  isAnyKeyHeld(keys: Array<string>): boolean {\n    return keys.some((key) => this.isKeyHeld(key))\n  }\n\n  /**\n   * Checks if all of the given keys are currently held.\n   *\n   * @param keys - Array of key names to check\n   * @returns True if all of the keys are currently held\n   */\n  areAllKeysHeld(keys: Array<string>): boolean {\n    return keys.every((key) => this.isKeyHeld(key))\n  }\n\n  /**\n   * Destroys the tracker and removes all listeners.\n   */\n  destroy(): void {\n    this.#removeListeners()\n    this.#heldKeysSet.clear()\n    this.#heldCodesMap.clear()\n    this.store.setState(() => getDefaultKeyStateTrackerState())\n  }\n}\n\n/**\n * Gets the singleton KeyStateTracker instance.\n * Convenience function for accessing the tracker.\n */\nexport function getKeyStateTracker(): KeyStateTracker {\n  return KeyStateTracker.getInstance()\n}\n"],"mappings":"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"}