@tanstack/react-hotkeys 0.5.0 → 0.6.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 +1 @@
1
- {"version":3,"file":"HotkeysProvider.cjs","names":[],"sources":["../src/HotkeysProvider.tsx"],"sourcesContent":["import React, { createContext, useContext, useMemo } from 'react'\nimport type { ReactNode } from 'react'\nimport type { HotkeyRecorderOptions } from '@tanstack/hotkeys'\nimport type { UseHotkeyOptions } from './useHotkey'\nimport type { UseHotkeySequenceOptions } from './useHotkeySequence'\n\nexport interface HotkeysProviderOptions {\n hotkey?: Partial<UseHotkeyOptions>\n hotkeyRecorder?: Partial<HotkeyRecorderOptions>\n hotkeySequence?: Partial<UseHotkeySequenceOptions>\n}\n\ninterface HotkeysContextValue {\n defaultOptions: HotkeysProviderOptions\n}\n\nconst HotkeysContext = createContext<HotkeysContextValue | null>(null)\n\nexport interface HotkeysProviderProps {\n children: ReactNode\n defaultOptions?: HotkeysProviderOptions\n}\n\nconst DEFAULT_OPTIONS: HotkeysProviderOptions = {}\n\nexport function HotkeysProvider({\n children,\n defaultOptions = DEFAULT_OPTIONS,\n}: HotkeysProviderProps) {\n const contextValue: HotkeysContextValue = useMemo(\n () => ({\n defaultOptions,\n }),\n [defaultOptions],\n )\n\n return (\n // eslint-disable-next-line @eslint-react/no-context-provider -- React 19+ only; we support React >=16.8\n <HotkeysContext.Provider value={contextValue}>\n {children}\n </HotkeysContext.Provider>\n )\n}\n\nexport function useHotkeysContext() {\n // eslint-disable-next-line @eslint-react/no-use-context -- React 19+ only; we support React >=16.8\n return useContext(HotkeysContext)\n}\n\nexport function useDefaultHotkeysOptions() {\n // eslint-disable-next-line @eslint-react/no-use-context -- React 19+ only; we support React >=16.8\n const context = useContext(HotkeysContext)\n return context?.defaultOptions ?? {}\n}\n"],"mappings":";;;;;AAgBA,MAAM,0CAA2D,KAAK;AAOtE,MAAM,kBAA0C,EAAE;AAElD,SAAgB,gBAAgB,EAC9B,UACA,iBAAiB,mBACM;CACvB,MAAM,yCACG,EACL,gBACD,GACD,CAAC,eAAe,CACjB;AAED,QAEE,4CAAC,eAAe,UAAhB,EAAyB,OAAO,cAEN,EADvB,SACuB;;AAI9B,SAAgB,oBAAoB;AAElC,8BAAkB,eAAe;;AAGnC,SAAgB,2BAA2B;AAGzC,8BAD2B,eAAe,EAC1B,kBAAkB,EAAE"}
1
+ {"version":3,"file":"HotkeysProvider.cjs","names":[],"sources":["../src/HotkeysProvider.tsx"],"sourcesContent":["import React, { createContext, useContext, useMemo } from 'react'\nimport type { ReactNode } from 'react'\nimport type {\n HotkeyRecorderOptions,\n HotkeySequenceRecorderOptions,\n} from '@tanstack/hotkeys'\nimport type { UseHotkeyOptions } from './useHotkey'\nimport type { UseHotkeySequenceOptions } from './useHotkeySequence'\n\nexport interface HotkeysProviderOptions {\n hotkey?: Partial<UseHotkeyOptions>\n hotkeyRecorder?: Partial<HotkeyRecorderOptions>\n hotkeySequenceRecorder?: Partial<HotkeySequenceRecorderOptions>\n hotkeySequence?: Partial<UseHotkeySequenceOptions>\n}\n\ninterface HotkeysContextValue {\n defaultOptions: HotkeysProviderOptions\n}\n\nconst HotkeysContext = createContext<HotkeysContextValue | null>(null)\n\nexport interface HotkeysProviderProps {\n children: ReactNode\n defaultOptions?: HotkeysProviderOptions\n}\n\nconst DEFAULT_OPTIONS: HotkeysProviderOptions = {}\n\nexport function HotkeysProvider({\n children,\n defaultOptions = DEFAULT_OPTIONS,\n}: HotkeysProviderProps) {\n const contextValue: HotkeysContextValue = useMemo(\n () => ({\n defaultOptions,\n }),\n [defaultOptions],\n )\n\n return (\n // eslint-disable-next-line @eslint-react/no-context-provider -- React 19+ only; we support React >=16.8\n <HotkeysContext.Provider value={contextValue}>\n {children}\n </HotkeysContext.Provider>\n )\n}\n\nexport function useHotkeysContext() {\n // eslint-disable-next-line @eslint-react/no-use-context -- React 19+ only; we support React >=16.8\n return useContext(HotkeysContext)\n}\n\nexport function useDefaultHotkeysOptions() {\n // eslint-disable-next-line @eslint-react/no-use-context -- React 19+ only; we support React >=16.8\n const context = useContext(HotkeysContext)\n return context?.defaultOptions ?? {}\n}\n"],"mappings":";;;;;AAoBA,MAAM,0CAA2D,KAAK;AAOtE,MAAM,kBAA0C,EAAE;AAElD,SAAgB,gBAAgB,EAC9B,UACA,iBAAiB,mBACM;CACvB,MAAM,yCACG,EACL,gBACD,GACD,CAAC,eAAe,CACjB;AAED,QAEE,4CAAC,eAAe,UAAhB,EAAyB,OAAO,cAEN,EADvB,SACuB;;AAI9B,SAAgB,oBAAoB;AAElC,8BAAkB,eAAe;;AAGnC,SAAgB,2BAA2B;AAGzC,8BAD2B,eAAe,EAC1B,kBAAkB,EAAE"}
@@ -1,12 +1,13 @@
1
1
  import { UseHotkeyOptions } from "./useHotkey.cjs";
2
2
  import { UseHotkeySequenceOptions } from "./useHotkeySequence.cjs";
3
- import { HotkeyRecorderOptions } from "@tanstack/hotkeys";
3
+ import { HotkeyRecorderOptions, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
4
4
  import React, { ReactNode } from "react";
5
5
 
6
6
  //#region src/HotkeysProvider.d.ts
7
7
  interface HotkeysProviderOptions {
8
8
  hotkey?: Partial<UseHotkeyOptions>;
9
9
  hotkeyRecorder?: Partial<HotkeyRecorderOptions>;
10
+ hotkeySequenceRecorder?: Partial<HotkeySequenceRecorderOptions>;
10
11
  hotkeySequence?: Partial<UseHotkeySequenceOptions>;
11
12
  }
12
13
  interface HotkeysContextValue {
@@ -1,12 +1,13 @@
1
1
  import { UseHotkeyOptions } from "./useHotkey.js";
2
2
  import { UseHotkeySequenceOptions } from "./useHotkeySequence.js";
3
- import { HotkeyRecorderOptions } from "@tanstack/hotkeys";
3
+ import { HotkeyRecorderOptions, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
4
4
  import React, { ReactNode } from "react";
5
5
 
6
6
  //#region src/HotkeysProvider.d.ts
7
7
  interface HotkeysProviderOptions {
8
8
  hotkey?: Partial<UseHotkeyOptions>;
9
9
  hotkeyRecorder?: Partial<HotkeyRecorderOptions>;
10
+ hotkeySequenceRecorder?: Partial<HotkeySequenceRecorderOptions>;
10
11
  hotkeySequence?: Partial<UseHotkeySequenceOptions>;
11
12
  }
12
13
  interface HotkeysContextValue {
@@ -1 +1 @@
1
- {"version":3,"file":"HotkeysProvider.js","names":[],"sources":["../src/HotkeysProvider.tsx"],"sourcesContent":["import React, { createContext, useContext, useMemo } from 'react'\nimport type { ReactNode } from 'react'\nimport type { HotkeyRecorderOptions } from '@tanstack/hotkeys'\nimport type { UseHotkeyOptions } from './useHotkey'\nimport type { UseHotkeySequenceOptions } from './useHotkeySequence'\n\nexport interface HotkeysProviderOptions {\n hotkey?: Partial<UseHotkeyOptions>\n hotkeyRecorder?: Partial<HotkeyRecorderOptions>\n hotkeySequence?: Partial<UseHotkeySequenceOptions>\n}\n\ninterface HotkeysContextValue {\n defaultOptions: HotkeysProviderOptions\n}\n\nconst HotkeysContext = createContext<HotkeysContextValue | null>(null)\n\nexport interface HotkeysProviderProps {\n children: ReactNode\n defaultOptions?: HotkeysProviderOptions\n}\n\nconst DEFAULT_OPTIONS: HotkeysProviderOptions = {}\n\nexport function HotkeysProvider({\n children,\n defaultOptions = DEFAULT_OPTIONS,\n}: HotkeysProviderProps) {\n const contextValue: HotkeysContextValue = useMemo(\n () => ({\n defaultOptions,\n }),\n [defaultOptions],\n )\n\n return (\n // eslint-disable-next-line @eslint-react/no-context-provider -- React 19+ only; we support React >=16.8\n <HotkeysContext.Provider value={contextValue}>\n {children}\n </HotkeysContext.Provider>\n )\n}\n\nexport function useHotkeysContext() {\n // eslint-disable-next-line @eslint-react/no-use-context -- React 19+ only; we support React >=16.8\n return useContext(HotkeysContext)\n}\n\nexport function useDefaultHotkeysOptions() {\n // eslint-disable-next-line @eslint-react/no-use-context -- React 19+ only; we support React >=16.8\n const context = useContext(HotkeysContext)\n return context?.defaultOptions ?? {}\n}\n"],"mappings":";;;AAgBA,MAAM,iBAAiB,cAA0C,KAAK;AAOtE,MAAM,kBAA0C,EAAE;AAElD,SAAgB,gBAAgB,EAC9B,UACA,iBAAiB,mBACM;CACvB,MAAM,eAAoC,eACjC,EACL,gBACD,GACD,CAAC,eAAe,CACjB;AAED,QAEE,oCAAC,eAAe,UAAhB,EAAyB,OAAO,cAEN,EADvB,SACuB;;AAI9B,SAAgB,oBAAoB;AAElC,QAAO,WAAW,eAAe;;AAGnC,SAAgB,2BAA2B;AAGzC,QADgB,WAAW,eAAe,EAC1B,kBAAkB,EAAE"}
1
+ {"version":3,"file":"HotkeysProvider.js","names":[],"sources":["../src/HotkeysProvider.tsx"],"sourcesContent":["import React, { createContext, useContext, useMemo } from 'react'\nimport type { ReactNode } from 'react'\nimport type {\n HotkeyRecorderOptions,\n HotkeySequenceRecorderOptions,\n} from '@tanstack/hotkeys'\nimport type { UseHotkeyOptions } from './useHotkey'\nimport type { UseHotkeySequenceOptions } from './useHotkeySequence'\n\nexport interface HotkeysProviderOptions {\n hotkey?: Partial<UseHotkeyOptions>\n hotkeyRecorder?: Partial<HotkeyRecorderOptions>\n hotkeySequenceRecorder?: Partial<HotkeySequenceRecorderOptions>\n hotkeySequence?: Partial<UseHotkeySequenceOptions>\n}\n\ninterface HotkeysContextValue {\n defaultOptions: HotkeysProviderOptions\n}\n\nconst HotkeysContext = createContext<HotkeysContextValue | null>(null)\n\nexport interface HotkeysProviderProps {\n children: ReactNode\n defaultOptions?: HotkeysProviderOptions\n}\n\nconst DEFAULT_OPTIONS: HotkeysProviderOptions = {}\n\nexport function HotkeysProvider({\n children,\n defaultOptions = DEFAULT_OPTIONS,\n}: HotkeysProviderProps) {\n const contextValue: HotkeysContextValue = useMemo(\n () => ({\n defaultOptions,\n }),\n [defaultOptions],\n )\n\n return (\n // eslint-disable-next-line @eslint-react/no-context-provider -- React 19+ only; we support React >=16.8\n <HotkeysContext.Provider value={contextValue}>\n {children}\n </HotkeysContext.Provider>\n )\n}\n\nexport function useHotkeysContext() {\n // eslint-disable-next-line @eslint-react/no-use-context -- React 19+ only; we support React >=16.8\n return useContext(HotkeysContext)\n}\n\nexport function useDefaultHotkeysOptions() {\n // eslint-disable-next-line @eslint-react/no-use-context -- React 19+ only; we support React >=16.8\n const context = useContext(HotkeysContext)\n return context?.defaultOptions ?? {}\n}\n"],"mappings":";;;AAoBA,MAAM,iBAAiB,cAA0C,KAAK;AAOtE,MAAM,kBAA0C,EAAE;AAElD,SAAgB,gBAAgB,EAC9B,UACA,iBAAiB,mBACM;CACvB,MAAM,eAAoC,eACjC,EACL,gBACD,GACD,CAAC,eAAe,CACjB;AAED,QAEE,oCAAC,eAAe,UAAhB,EAAyB,OAAO,cAEN,EADvB,SACuB;;AAI9B,SAAgB,oBAAoB;AAElC,QAAO,WAAW,eAAe;;AAGnC,SAAgB,2BAA2B;AAGzC,QADgB,WAAW,eAAe,EAC1B,kBAAkB,EAAE"}
package/dist/index.cjs CHANGED
@@ -7,6 +7,7 @@ const require_useHeldKeyCodes = require('./useHeldKeyCodes.cjs');
7
7
  const require_useKeyHold = require('./useKeyHold.cjs');
8
8
  const require_useHotkeySequence = require('./useHotkeySequence.cjs');
9
9
  const require_useHotkeyRecorder = require('./useHotkeyRecorder.cjs');
10
+ const require_useHotkeySequenceRecorder = require('./useHotkeySequenceRecorder.cjs');
10
11
 
11
12
  exports.HotkeysProvider = require_HotkeysProvider.HotkeysProvider;
12
13
  exports.useDefaultHotkeysOptions = require_HotkeysProvider.useDefaultHotkeysOptions;
@@ -15,6 +16,7 @@ exports.useHeldKeys = require_useHeldKeys.useHeldKeys;
15
16
  exports.useHotkey = require_useHotkey.useHotkey;
16
17
  exports.useHotkeyRecorder = require_useHotkeyRecorder.useHotkeyRecorder;
17
18
  exports.useHotkeySequence = require_useHotkeySequence.useHotkeySequence;
19
+ exports.useHotkeySequenceRecorder = require_useHotkeySequenceRecorder.useHotkeySequenceRecorder;
18
20
  exports.useHotkeys = require_useHotkeys.useHotkeys;
19
21
  exports.useHotkeysContext = require_HotkeysProvider.useHotkeysContext;
20
22
  exports.useKeyHold = require_useKeyHold.useKeyHold;
package/dist/index.d.cts CHANGED
@@ -6,5 +6,6 @@ import { useHeldKeys } from "./useHeldKeys.cjs";
6
6
  import { useHeldKeyCodes } from "./useHeldKeyCodes.cjs";
7
7
  import { useKeyHold } from "./useKeyHold.cjs";
8
8
  import { ReactHotkeyRecorder, useHotkeyRecorder } from "./useHotkeyRecorder.cjs";
9
+ import { ReactHotkeySequenceRecorder, useHotkeySequenceRecorder } from "./useHotkeySequenceRecorder.cjs";
9
10
  export * from "@tanstack/hotkeys";
10
- export { HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, ReactHotkeyRecorder, UseHotkeyDefinition, UseHotkeyOptions, UseHotkeySequenceOptions, useDefaultHotkeysOptions, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyRecorder, useHotkeySequence, useHotkeys, useHotkeysContext, useKeyHold };
11
+ export { HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, ReactHotkeyRecorder, ReactHotkeySequenceRecorder, UseHotkeyDefinition, UseHotkeyOptions, UseHotkeySequenceOptions, useDefaultHotkeysOptions, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyRecorder, useHotkeySequence, useHotkeySequenceRecorder, useHotkeys, useHotkeysContext, useKeyHold };
package/dist/index.d.ts CHANGED
@@ -6,5 +6,6 @@ import { useHeldKeys } from "./useHeldKeys.js";
6
6
  import { useHeldKeyCodes } from "./useHeldKeyCodes.js";
7
7
  import { useKeyHold } from "./useKeyHold.js";
8
8
  import { ReactHotkeyRecorder, useHotkeyRecorder } from "./useHotkeyRecorder.js";
9
+ import { ReactHotkeySequenceRecorder, useHotkeySequenceRecorder } from "./useHotkeySequenceRecorder.js";
9
10
  export * from "@tanstack/hotkeys";
10
- export { HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, ReactHotkeyRecorder, UseHotkeyDefinition, UseHotkeyOptions, UseHotkeySequenceOptions, useDefaultHotkeysOptions, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyRecorder, useHotkeySequence, useHotkeys, useHotkeysContext, useKeyHold };
11
+ export { HotkeysProvider, HotkeysProviderOptions, HotkeysProviderProps, ReactHotkeyRecorder, ReactHotkeySequenceRecorder, UseHotkeyDefinition, UseHotkeyOptions, UseHotkeySequenceOptions, useDefaultHotkeysOptions, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyRecorder, useHotkeySequence, useHotkeySequenceRecorder, useHotkeys, useHotkeysContext, useKeyHold };
package/dist/index.js CHANGED
@@ -6,7 +6,8 @@ import { useHeldKeyCodes } from "./useHeldKeyCodes.js";
6
6
  import { useKeyHold } from "./useKeyHold.js";
7
7
  import { useHotkeySequence } from "./useHotkeySequence.js";
8
8
  import { useHotkeyRecorder } from "./useHotkeyRecorder.js";
9
+ import { useHotkeySequenceRecorder } from "./useHotkeySequenceRecorder.js";
9
10
 
10
11
  export * from "@tanstack/hotkeys"
11
12
 
12
- export { HotkeysProvider, useDefaultHotkeysOptions, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyRecorder, useHotkeySequence, useHotkeys, useHotkeysContext, useKeyHold };
13
+ export { HotkeysProvider, useDefaultHotkeysOptions, useHeldKeyCodes, useHeldKeys, useHotkey, useHotkeyRecorder, useHotkeySequence, useHotkeySequenceRecorder, useHotkeys, useHotkeysContext, useKeyHold };
@@ -11,6 +11,11 @@ let react = require("react");
11
11
  * This hook allows you to register multi-key sequences like 'g g' or 'd d'
12
12
  * that trigger when the full sequence is pressed within a timeout.
13
13
  *
14
+ * Each step may include modifiers. You can chain the same modifier across
15
+ * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,
16
+ * Control, Alt, or Meta pressed alone) are ignored while matching—they do not
17
+ * advance the sequence or reset progress.
18
+ *
14
19
  * @param sequence - Array of hotkey strings that form the sequence
15
20
  * @param callback - Function to call when the sequence is completed
16
21
  * @param options - Options for the sequence behavior
@@ -33,6 +38,11 @@ let react = require("react");
33
38
  * deleteInnerWord()
34
39
  * }, { timeout: 500 })
35
40
  *
41
+ * // Same modifier on consecutive steps (bare Shift between chords is ignored)
42
+ * useHotkeySequence(['Shift+R', 'Shift+T'], () => {
43
+ * nextAction()
44
+ * })
45
+ *
36
46
  * return <div>...</div>
37
47
  * }
38
48
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"useHotkeySequence.cjs","names":["useDefaultHotkeysOptions","isRef"],"sources":["../src/useHotkeySequence.ts"],"sourcesContent":["import { useEffect, useRef } from 'react'\nimport { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport { isRef } from './utils'\nimport type {\n HotkeyCallback,\n HotkeyCallbackContext,\n HotkeySequence,\n SequenceOptions,\n SequenceRegistrationHandle,\n} from '@tanstack/hotkeys'\n\nexport interface UseHotkeySequenceOptions extends Omit<\n SequenceOptions,\n 'target'\n> {\n /**\n * The DOM element to attach the event listener to.\n * Can be a React ref, direct DOM element, or null.\n * Defaults to document.\n */\n target?:\n | React.RefObject<HTMLElement | null>\n | HTMLElement\n | Document\n | Window\n | null\n}\n\n/**\n * React hook for registering a keyboard shortcut sequence (Vim-style).\n *\n * This hook allows you to register multi-key sequences like 'g g' or 'd d'\n * that trigger when the full sequence is pressed within a timeout.\n *\n * @param sequence - Array of hotkey strings that form the sequence\n * @param callback - Function to call when the sequence is completed\n * @param options - Options for the sequence behavior\n *\n * @example\n * ```tsx\n * function VimEditor() {\n * // 'g g' to go to top\n * useHotkeySequence(['G', 'G'], () => {\n * scrollToTop()\n * })\n *\n * // 'd d' to delete line\n * useHotkeySequence(['D', 'D'], () => {\n * deleteLine()\n * })\n *\n * // 'd i w' to delete inner word\n * useHotkeySequence(['D', 'I', 'W'], () => {\n * deleteInnerWord()\n * }, { timeout: 500 })\n *\n * return <div>...</div>\n * }\n * ```\n */\nexport function useHotkeySequence(\n sequence: HotkeySequence,\n callback: HotkeyCallback,\n options: UseHotkeySequenceOptions = {},\n): void {\n const mergedOptions = {\n ...useDefaultHotkeysOptions().hotkeySequence,\n ...options,\n } as UseHotkeySequenceOptions\n\n const manager = getSequenceManager()\n\n // Stable ref for registration handle\n const registrationRef = useRef<SequenceRegistrationHandle | null>(null)\n\n // Refs to capture current values for use in effect without adding dependencies\n const callbackRef = useRef(callback)\n const optionsRef = useRef(mergedOptions)\n const managerRef = useRef(manager)\n const sequenceRef = useRef(sequence)\n\n // Update refs on every render\n callbackRef.current = callback\n optionsRef.current = mergedOptions\n managerRef.current = manager\n sequenceRef.current = sequence\n\n // Track previous target and sequence to detect changes requiring re-registration\n const prevTargetRef = useRef<HTMLElement | Document | Window | null>(null)\n const prevSequenceRef = useRef<string | null>(null)\n\n // Normalize to hotkey sequence string (join with spaces)\n const hotkeySequenceString = formatHotkeySequence(sequence)\n\n // Extract options without target (target is handled separately)\n const { target: _target, ...optionsWithoutTarget } = mergedOptions\n\n useEffect(() => {\n if (sequenceRef.current.length === 0) {\n return\n }\n\n // Resolve target inside the effect so refs are already attached after mount\n const resolvedTarget = isRef(optionsRef.current.target)\n ? optionsRef.current.target.current\n : (optionsRef.current.target ??\n (typeof document !== 'undefined' ? document : null))\n\n // Skip if no valid target (SSR or ref still null)\n if (!resolvedTarget) {\n return\n }\n\n // Check if we need to re-register (target or sequence changed)\n const targetChanged =\n prevTargetRef.current !== null && prevTargetRef.current !== resolvedTarget\n const sequenceChanged =\n prevSequenceRef.current !== null &&\n prevSequenceRef.current !== hotkeySequenceString\n\n // If we have an active registration and target/sequence changed, unregister first\n if (\n registrationRef.current?.isActive &&\n (targetChanged || sequenceChanged)\n ) {\n registrationRef.current.unregister()\n registrationRef.current = null\n }\n\n // Register if needed (no active registration)\n if (!registrationRef.current || !registrationRef.current.isActive) {\n registrationRef.current = managerRef.current.register(\n sequenceRef.current,\n (event, context) => callbackRef.current(event, context),\n {\n ...optionsRef.current,\n target: resolvedTarget,\n },\n )\n }\n\n // Update tracking refs\n prevTargetRef.current = resolvedTarget\n prevSequenceRef.current = hotkeySequenceString\n\n // Cleanup on unmount\n return () => {\n if (registrationRef.current?.isActive) {\n registrationRef.current.unregister()\n registrationRef.current = null\n }\n }\n }, [hotkeySequenceString, mergedOptions.enabled])\n\n // Sync callback and options on EVERY render (outside useEffect)\n if (registrationRef.current?.isActive) {\n registrationRef.current.callback = (\n event: KeyboardEvent,\n context: HotkeyCallbackContext,\n ) => callbackRef.current(event, context)\n registrationRef.current.setOptions(optionsWithoutTarget)\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,SAAgB,kBACd,UACA,UACA,UAAoC,EAAE,EAChC;CACN,MAAM,gBAAgB;EACpB,GAAGA,kDAA0B,CAAC;EAC9B,GAAG;EACJ;CAED,MAAM,qDAA8B;CAGpC,MAAM,oCAA4D,KAAK;CAGvE,MAAM,gCAAqB,SAAS;CACpC,MAAM,+BAAoB,cAAc;CACxC,MAAM,+BAAoB,QAAQ;CAClC,MAAM,gCAAqB,SAAS;AAGpC,aAAY,UAAU;AACtB,YAAW,UAAU;AACrB,YAAW,UAAU;AACrB,aAAY,UAAU;CAGtB,MAAM,kCAA+D,KAAK;CAC1E,MAAM,oCAAwC,KAAK;CAGnD,MAAM,mEAA4C,SAAS;CAG3D,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB;AAErD,4BAAgB;AACd,MAAI,YAAY,QAAQ,WAAW,EACjC;EAIF,MAAM,iBAAiBC,oBAAM,WAAW,QAAQ,OAAO,GACnD,WAAW,QAAQ,OAAO,UACzB,WAAW,QAAQ,WACnB,OAAO,aAAa,cAAc,WAAW;AAGlD,MAAI,CAAC,eACH;EAIF,MAAM,gBACJ,cAAc,YAAY,QAAQ,cAAc,YAAY;EAC9D,MAAM,kBACJ,gBAAgB,YAAY,QAC5B,gBAAgB,YAAY;AAG9B,MACE,gBAAgB,SAAS,aACxB,iBAAiB,kBAClB;AACA,mBAAgB,QAAQ,YAAY;AACpC,mBAAgB,UAAU;;AAI5B,MAAI,CAAC,gBAAgB,WAAW,CAAC,gBAAgB,QAAQ,SACvD,iBAAgB,UAAU,WAAW,QAAQ,SAC3C,YAAY,UACX,OAAO,YAAY,YAAY,QAAQ,OAAO,QAAQ,EACvD;GACE,GAAG,WAAW;GACd,QAAQ;GACT,CACF;AAIH,gBAAc,UAAU;AACxB,kBAAgB,UAAU;AAG1B,eAAa;AACX,OAAI,gBAAgB,SAAS,UAAU;AACrC,oBAAgB,QAAQ,YAAY;AACpC,oBAAgB,UAAU;;;IAG7B,CAAC,sBAAsB,cAAc,QAAQ,CAAC;AAGjD,KAAI,gBAAgB,SAAS,UAAU;AACrC,kBAAgB,QAAQ,YACtB,OACA,YACG,YAAY,QAAQ,OAAO,QAAQ;AACxC,kBAAgB,QAAQ,WAAW,qBAAqB"}
1
+ {"version":3,"file":"useHotkeySequence.cjs","names":["useDefaultHotkeysOptions","isRef"],"sources":["../src/useHotkeySequence.ts"],"sourcesContent":["import { useEffect, useRef } from 'react'\nimport { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport { isRef } from './utils'\nimport type {\n HotkeyCallback,\n HotkeyCallbackContext,\n HotkeySequence,\n SequenceOptions,\n SequenceRegistrationHandle,\n} from '@tanstack/hotkeys'\n\nexport interface UseHotkeySequenceOptions extends Omit<\n SequenceOptions,\n 'target'\n> {\n /**\n * The DOM element to attach the event listener to.\n * Can be a React ref, direct DOM element, or null.\n * Defaults to document.\n */\n target?:\n | React.RefObject<HTMLElement | null>\n | HTMLElement\n | Document\n | Window\n | null\n}\n\n/**\n * React hook for registering a keyboard shortcut sequence (Vim-style).\n *\n * This hook allows you to register multi-key sequences like 'g g' or 'd d'\n * that trigger when the full sequence is pressed within a timeout.\n *\n * Each step may include modifiers. You can chain the same modifier across\n * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,\n * Control, Alt, or Meta pressed alone) are ignored while matching—they do not\n * advance the sequence or reset progress.\n *\n * @param sequence - Array of hotkey strings that form the sequence\n * @param callback - Function to call when the sequence is completed\n * @param options - Options for the sequence behavior\n *\n * @example\n * ```tsx\n * function VimEditor() {\n * // 'g g' to go to top\n * useHotkeySequence(['G', 'G'], () => {\n * scrollToTop()\n * })\n *\n * // 'd d' to delete line\n * useHotkeySequence(['D', 'D'], () => {\n * deleteLine()\n * })\n *\n * // 'd i w' to delete inner word\n * useHotkeySequence(['D', 'I', 'W'], () => {\n * deleteInnerWord()\n * }, { timeout: 500 })\n *\n * // Same modifier on consecutive steps (bare Shift between chords is ignored)\n * useHotkeySequence(['Shift+R', 'Shift+T'], () => {\n * nextAction()\n * })\n *\n * return <div>...</div>\n * }\n * ```\n */\nexport function useHotkeySequence(\n sequence: HotkeySequence,\n callback: HotkeyCallback,\n options: UseHotkeySequenceOptions = {},\n): void {\n const mergedOptions = {\n ...useDefaultHotkeysOptions().hotkeySequence,\n ...options,\n } as UseHotkeySequenceOptions\n\n const manager = getSequenceManager()\n\n // Stable ref for registration handle\n const registrationRef = useRef<SequenceRegistrationHandle | null>(null)\n\n // Refs to capture current values for use in effect without adding dependencies\n const callbackRef = useRef(callback)\n const optionsRef = useRef(mergedOptions)\n const managerRef = useRef(manager)\n const sequenceRef = useRef(sequence)\n\n // Update refs on every render\n callbackRef.current = callback\n optionsRef.current = mergedOptions\n managerRef.current = manager\n sequenceRef.current = sequence\n\n // Track previous target and sequence to detect changes requiring re-registration\n const prevTargetRef = useRef<HTMLElement | Document | Window | null>(null)\n const prevSequenceRef = useRef<string | null>(null)\n\n // Normalize to hotkey sequence string (join with spaces)\n const hotkeySequenceString = formatHotkeySequence(sequence)\n\n // Extract options without target (target is handled separately)\n const { target: _target, ...optionsWithoutTarget } = mergedOptions\n\n useEffect(() => {\n if (sequenceRef.current.length === 0) {\n return\n }\n\n // Resolve target inside the effect so refs are already attached after mount\n const resolvedTarget = isRef(optionsRef.current.target)\n ? optionsRef.current.target.current\n : (optionsRef.current.target ??\n (typeof document !== 'undefined' ? document : null))\n\n // Skip if no valid target (SSR or ref still null)\n if (!resolvedTarget) {\n return\n }\n\n // Check if we need to re-register (target or sequence changed)\n const targetChanged =\n prevTargetRef.current !== null && prevTargetRef.current !== resolvedTarget\n const sequenceChanged =\n prevSequenceRef.current !== null &&\n prevSequenceRef.current !== hotkeySequenceString\n\n // If we have an active registration and target/sequence changed, unregister first\n if (\n registrationRef.current?.isActive &&\n (targetChanged || sequenceChanged)\n ) {\n registrationRef.current.unregister()\n registrationRef.current = null\n }\n\n // Register if needed (no active registration)\n if (!registrationRef.current || !registrationRef.current.isActive) {\n registrationRef.current = managerRef.current.register(\n sequenceRef.current,\n (event, context) => callbackRef.current(event, context),\n {\n ...optionsRef.current,\n target: resolvedTarget,\n },\n )\n }\n\n // Update tracking refs\n prevTargetRef.current = resolvedTarget\n prevSequenceRef.current = hotkeySequenceString\n\n // Cleanup on unmount\n return () => {\n if (registrationRef.current?.isActive) {\n registrationRef.current.unregister()\n registrationRef.current = null\n }\n }\n }, [hotkeySequenceString, mergedOptions.enabled])\n\n // Sync callback and options on EVERY render (outside useEffect)\n if (registrationRef.current?.isActive) {\n registrationRef.current.callback = (\n event: KeyboardEvent,\n context: HotkeyCallbackContext,\n ) => callbackRef.current(event, context)\n registrationRef.current.setOptions(optionsWithoutTarget)\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuEA,SAAgB,kBACd,UACA,UACA,UAAoC,EAAE,EAChC;CACN,MAAM,gBAAgB;EACpB,GAAGA,kDAA0B,CAAC;EAC9B,GAAG;EACJ;CAED,MAAM,qDAA8B;CAGpC,MAAM,oCAA4D,KAAK;CAGvE,MAAM,gCAAqB,SAAS;CACpC,MAAM,+BAAoB,cAAc;CACxC,MAAM,+BAAoB,QAAQ;CAClC,MAAM,gCAAqB,SAAS;AAGpC,aAAY,UAAU;AACtB,YAAW,UAAU;AACrB,YAAW,UAAU;AACrB,aAAY,UAAU;CAGtB,MAAM,kCAA+D,KAAK;CAC1E,MAAM,oCAAwC,KAAK;CAGnD,MAAM,mEAA4C,SAAS;CAG3D,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB;AAErD,4BAAgB;AACd,MAAI,YAAY,QAAQ,WAAW,EACjC;EAIF,MAAM,iBAAiBC,oBAAM,WAAW,QAAQ,OAAO,GACnD,WAAW,QAAQ,OAAO,UACzB,WAAW,QAAQ,WACnB,OAAO,aAAa,cAAc,WAAW;AAGlD,MAAI,CAAC,eACH;EAIF,MAAM,gBACJ,cAAc,YAAY,QAAQ,cAAc,YAAY;EAC9D,MAAM,kBACJ,gBAAgB,YAAY,QAC5B,gBAAgB,YAAY;AAG9B,MACE,gBAAgB,SAAS,aACxB,iBAAiB,kBAClB;AACA,mBAAgB,QAAQ,YAAY;AACpC,mBAAgB,UAAU;;AAI5B,MAAI,CAAC,gBAAgB,WAAW,CAAC,gBAAgB,QAAQ,SACvD,iBAAgB,UAAU,WAAW,QAAQ,SAC3C,YAAY,UACX,OAAO,YAAY,YAAY,QAAQ,OAAO,QAAQ,EACvD;GACE,GAAG,WAAW;GACd,QAAQ;GACT,CACF;AAIH,gBAAc,UAAU;AACxB,kBAAgB,UAAU;AAG1B,eAAa;AACX,OAAI,gBAAgB,SAAS,UAAU;AACrC,oBAAgB,QAAQ,YAAY;AACpC,oBAAgB,UAAU;;;IAG7B,CAAC,sBAAsB,cAAc,QAAQ,CAAC;AAGjD,KAAI,gBAAgB,SAAS,UAAU;AACrC,kBAAgB,QAAQ,YACtB,OACA,YACG,YAAY,QAAQ,OAAO,QAAQ;AACxC,kBAAgB,QAAQ,WAAW,qBAAqB"}
@@ -15,6 +15,11 @@ interface UseHotkeySequenceOptions extends Omit<SequenceOptions, 'target'> {
15
15
  * This hook allows you to register multi-key sequences like 'g g' or 'd d'
16
16
  * that trigger when the full sequence is pressed within a timeout.
17
17
  *
18
+ * Each step may include modifiers. You can chain the same modifier across
19
+ * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,
20
+ * Control, Alt, or Meta pressed alone) are ignored while matching—they do not
21
+ * advance the sequence or reset progress.
22
+ *
18
23
  * @param sequence - Array of hotkey strings that form the sequence
19
24
  * @param callback - Function to call when the sequence is completed
20
25
  * @param options - Options for the sequence behavior
@@ -37,6 +42,11 @@ interface UseHotkeySequenceOptions extends Omit<SequenceOptions, 'target'> {
37
42
  * deleteInnerWord()
38
43
  * }, { timeout: 500 })
39
44
  *
45
+ * // Same modifier on consecutive steps (bare Shift between chords is ignored)
46
+ * useHotkeySequence(['Shift+R', 'Shift+T'], () => {
47
+ * nextAction()
48
+ * })
49
+ *
40
50
  * return <div>...</div>
41
51
  * }
42
52
  * ```
@@ -15,6 +15,11 @@ interface UseHotkeySequenceOptions extends Omit<SequenceOptions, 'target'> {
15
15
  * This hook allows you to register multi-key sequences like 'g g' or 'd d'
16
16
  * that trigger when the full sequence is pressed within a timeout.
17
17
  *
18
+ * Each step may include modifiers. You can chain the same modifier across
19
+ * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,
20
+ * Control, Alt, or Meta pressed alone) are ignored while matching—they do not
21
+ * advance the sequence or reset progress.
22
+ *
18
23
  * @param sequence - Array of hotkey strings that form the sequence
19
24
  * @param callback - Function to call when the sequence is completed
20
25
  * @param options - Options for the sequence behavior
@@ -37,6 +42,11 @@ interface UseHotkeySequenceOptions extends Omit<SequenceOptions, 'target'> {
37
42
  * deleteInnerWord()
38
43
  * }, { timeout: 500 })
39
44
  *
45
+ * // Same modifier on consecutive steps (bare Shift between chords is ignored)
46
+ * useHotkeySequence(['Shift+R', 'Shift+T'], () => {
47
+ * nextAction()
48
+ * })
49
+ *
40
50
  * return <div>...</div>
41
51
  * }
42
52
  * ```
@@ -10,6 +10,11 @@ import { useEffect, useRef } from "react";
10
10
  * This hook allows you to register multi-key sequences like 'g g' or 'd d'
11
11
  * that trigger when the full sequence is pressed within a timeout.
12
12
  *
13
+ * Each step may include modifiers. You can chain the same modifier across
14
+ * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,
15
+ * Control, Alt, or Meta pressed alone) are ignored while matching—they do not
16
+ * advance the sequence or reset progress.
17
+ *
13
18
  * @param sequence - Array of hotkey strings that form the sequence
14
19
  * @param callback - Function to call when the sequence is completed
15
20
  * @param options - Options for the sequence behavior
@@ -32,6 +37,11 @@ import { useEffect, useRef } from "react";
32
37
  * deleteInnerWord()
33
38
  * }, { timeout: 500 })
34
39
  *
40
+ * // Same modifier on consecutive steps (bare Shift between chords is ignored)
41
+ * useHotkeySequence(['Shift+R', 'Shift+T'], () => {
42
+ * nextAction()
43
+ * })
44
+ *
35
45
  * return <div>...</div>
36
46
  * }
37
47
  * ```
@@ -1 +1 @@
1
- {"version":3,"file":"useHotkeySequence.js","names":[],"sources":["../src/useHotkeySequence.ts"],"sourcesContent":["import { useEffect, useRef } from 'react'\nimport { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport { isRef } from './utils'\nimport type {\n HotkeyCallback,\n HotkeyCallbackContext,\n HotkeySequence,\n SequenceOptions,\n SequenceRegistrationHandle,\n} from '@tanstack/hotkeys'\n\nexport interface UseHotkeySequenceOptions extends Omit<\n SequenceOptions,\n 'target'\n> {\n /**\n * The DOM element to attach the event listener to.\n * Can be a React ref, direct DOM element, or null.\n * Defaults to document.\n */\n target?:\n | React.RefObject<HTMLElement | null>\n | HTMLElement\n | Document\n | Window\n | null\n}\n\n/**\n * React hook for registering a keyboard shortcut sequence (Vim-style).\n *\n * This hook allows you to register multi-key sequences like 'g g' or 'd d'\n * that trigger when the full sequence is pressed within a timeout.\n *\n * @param sequence - Array of hotkey strings that form the sequence\n * @param callback - Function to call when the sequence is completed\n * @param options - Options for the sequence behavior\n *\n * @example\n * ```tsx\n * function VimEditor() {\n * // 'g g' to go to top\n * useHotkeySequence(['G', 'G'], () => {\n * scrollToTop()\n * })\n *\n * // 'd d' to delete line\n * useHotkeySequence(['D', 'D'], () => {\n * deleteLine()\n * })\n *\n * // 'd i w' to delete inner word\n * useHotkeySequence(['D', 'I', 'W'], () => {\n * deleteInnerWord()\n * }, { timeout: 500 })\n *\n * return <div>...</div>\n * }\n * ```\n */\nexport function useHotkeySequence(\n sequence: HotkeySequence,\n callback: HotkeyCallback,\n options: UseHotkeySequenceOptions = {},\n): void {\n const mergedOptions = {\n ...useDefaultHotkeysOptions().hotkeySequence,\n ...options,\n } as UseHotkeySequenceOptions\n\n const manager = getSequenceManager()\n\n // Stable ref for registration handle\n const registrationRef = useRef<SequenceRegistrationHandle | null>(null)\n\n // Refs to capture current values for use in effect without adding dependencies\n const callbackRef = useRef(callback)\n const optionsRef = useRef(mergedOptions)\n const managerRef = useRef(manager)\n const sequenceRef = useRef(sequence)\n\n // Update refs on every render\n callbackRef.current = callback\n optionsRef.current = mergedOptions\n managerRef.current = manager\n sequenceRef.current = sequence\n\n // Track previous target and sequence to detect changes requiring re-registration\n const prevTargetRef = useRef<HTMLElement | Document | Window | null>(null)\n const prevSequenceRef = useRef<string | null>(null)\n\n // Normalize to hotkey sequence string (join with spaces)\n const hotkeySequenceString = formatHotkeySequence(sequence)\n\n // Extract options without target (target is handled separately)\n const { target: _target, ...optionsWithoutTarget } = mergedOptions\n\n useEffect(() => {\n if (sequenceRef.current.length === 0) {\n return\n }\n\n // Resolve target inside the effect so refs are already attached after mount\n const resolvedTarget = isRef(optionsRef.current.target)\n ? optionsRef.current.target.current\n : (optionsRef.current.target ??\n (typeof document !== 'undefined' ? document : null))\n\n // Skip if no valid target (SSR or ref still null)\n if (!resolvedTarget) {\n return\n }\n\n // Check if we need to re-register (target or sequence changed)\n const targetChanged =\n prevTargetRef.current !== null && prevTargetRef.current !== resolvedTarget\n const sequenceChanged =\n prevSequenceRef.current !== null &&\n prevSequenceRef.current !== hotkeySequenceString\n\n // If we have an active registration and target/sequence changed, unregister first\n if (\n registrationRef.current?.isActive &&\n (targetChanged || sequenceChanged)\n ) {\n registrationRef.current.unregister()\n registrationRef.current = null\n }\n\n // Register if needed (no active registration)\n if (!registrationRef.current || !registrationRef.current.isActive) {\n registrationRef.current = managerRef.current.register(\n sequenceRef.current,\n (event, context) => callbackRef.current(event, context),\n {\n ...optionsRef.current,\n target: resolvedTarget,\n },\n )\n }\n\n // Update tracking refs\n prevTargetRef.current = resolvedTarget\n prevSequenceRef.current = hotkeySequenceString\n\n // Cleanup on unmount\n return () => {\n if (registrationRef.current?.isActive) {\n registrationRef.current.unregister()\n registrationRef.current = null\n }\n }\n }, [hotkeySequenceString, mergedOptions.enabled])\n\n // Sync callback and options on EVERY render (outside useEffect)\n if (registrationRef.current?.isActive) {\n registrationRef.current.callback = (\n event: KeyboardEvent,\n context: HotkeyCallbackContext,\n ) => callbackRef.current(event, context)\n registrationRef.current.setOptions(optionsWithoutTarget)\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6DA,SAAgB,kBACd,UACA,UACA,UAAoC,EAAE,EAChC;CACN,MAAM,gBAAgB;EACpB,GAAG,0BAA0B,CAAC;EAC9B,GAAG;EACJ;CAED,MAAM,UAAU,oBAAoB;CAGpC,MAAM,kBAAkB,OAA0C,KAAK;CAGvE,MAAM,cAAc,OAAO,SAAS;CACpC,MAAM,aAAa,OAAO,cAAc;CACxC,MAAM,aAAa,OAAO,QAAQ;CAClC,MAAM,cAAc,OAAO,SAAS;AAGpC,aAAY,UAAU;AACtB,YAAW,UAAU;AACrB,YAAW,UAAU;AACrB,aAAY,UAAU;CAGtB,MAAM,gBAAgB,OAA+C,KAAK;CAC1E,MAAM,kBAAkB,OAAsB,KAAK;CAGnD,MAAM,uBAAuB,qBAAqB,SAAS;CAG3D,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB;AAErD,iBAAgB;AACd,MAAI,YAAY,QAAQ,WAAW,EACjC;EAIF,MAAM,iBAAiB,MAAM,WAAW,QAAQ,OAAO,GACnD,WAAW,QAAQ,OAAO,UACzB,WAAW,QAAQ,WACnB,OAAO,aAAa,cAAc,WAAW;AAGlD,MAAI,CAAC,eACH;EAIF,MAAM,gBACJ,cAAc,YAAY,QAAQ,cAAc,YAAY;EAC9D,MAAM,kBACJ,gBAAgB,YAAY,QAC5B,gBAAgB,YAAY;AAG9B,MACE,gBAAgB,SAAS,aACxB,iBAAiB,kBAClB;AACA,mBAAgB,QAAQ,YAAY;AACpC,mBAAgB,UAAU;;AAI5B,MAAI,CAAC,gBAAgB,WAAW,CAAC,gBAAgB,QAAQ,SACvD,iBAAgB,UAAU,WAAW,QAAQ,SAC3C,YAAY,UACX,OAAO,YAAY,YAAY,QAAQ,OAAO,QAAQ,EACvD;GACE,GAAG,WAAW;GACd,QAAQ;GACT,CACF;AAIH,gBAAc,UAAU;AACxB,kBAAgB,UAAU;AAG1B,eAAa;AACX,OAAI,gBAAgB,SAAS,UAAU;AACrC,oBAAgB,QAAQ,YAAY;AACpC,oBAAgB,UAAU;;;IAG7B,CAAC,sBAAsB,cAAc,QAAQ,CAAC;AAGjD,KAAI,gBAAgB,SAAS,UAAU;AACrC,kBAAgB,QAAQ,YACtB,OACA,YACG,YAAY,QAAQ,OAAO,QAAQ;AACxC,kBAAgB,QAAQ,WAAW,qBAAqB"}
1
+ {"version":3,"file":"useHotkeySequence.js","names":[],"sources":["../src/useHotkeySequence.ts"],"sourcesContent":["import { useEffect, useRef } from 'react'\nimport { formatHotkeySequence, getSequenceManager } from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport { isRef } from './utils'\nimport type {\n HotkeyCallback,\n HotkeyCallbackContext,\n HotkeySequence,\n SequenceOptions,\n SequenceRegistrationHandle,\n} from '@tanstack/hotkeys'\n\nexport interface UseHotkeySequenceOptions extends Omit<\n SequenceOptions,\n 'target'\n> {\n /**\n * The DOM element to attach the event listener to.\n * Can be a React ref, direct DOM element, or null.\n * Defaults to document.\n */\n target?:\n | React.RefObject<HTMLElement | null>\n | HTMLElement\n | Document\n | Window\n | null\n}\n\n/**\n * React hook for registering a keyboard shortcut sequence (Vim-style).\n *\n * This hook allows you to register multi-key sequences like 'g g' or 'd d'\n * that trigger when the full sequence is pressed within a timeout.\n *\n * Each step may include modifiers. You can chain the same modifier across\n * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,\n * Control, Alt, or Meta pressed alone) are ignored while matching—they do not\n * advance the sequence or reset progress.\n *\n * @param sequence - Array of hotkey strings that form the sequence\n * @param callback - Function to call when the sequence is completed\n * @param options - Options for the sequence behavior\n *\n * @example\n * ```tsx\n * function VimEditor() {\n * // 'g g' to go to top\n * useHotkeySequence(['G', 'G'], () => {\n * scrollToTop()\n * })\n *\n * // 'd d' to delete line\n * useHotkeySequence(['D', 'D'], () => {\n * deleteLine()\n * })\n *\n * // 'd i w' to delete inner word\n * useHotkeySequence(['D', 'I', 'W'], () => {\n * deleteInnerWord()\n * }, { timeout: 500 })\n *\n * // Same modifier on consecutive steps (bare Shift between chords is ignored)\n * useHotkeySequence(['Shift+R', 'Shift+T'], () => {\n * nextAction()\n * })\n *\n * return <div>...</div>\n * }\n * ```\n */\nexport function useHotkeySequence(\n sequence: HotkeySequence,\n callback: HotkeyCallback,\n options: UseHotkeySequenceOptions = {},\n): void {\n const mergedOptions = {\n ...useDefaultHotkeysOptions().hotkeySequence,\n ...options,\n } as UseHotkeySequenceOptions\n\n const manager = getSequenceManager()\n\n // Stable ref for registration handle\n const registrationRef = useRef<SequenceRegistrationHandle | null>(null)\n\n // Refs to capture current values for use in effect without adding dependencies\n const callbackRef = useRef(callback)\n const optionsRef = useRef(mergedOptions)\n const managerRef = useRef(manager)\n const sequenceRef = useRef(sequence)\n\n // Update refs on every render\n callbackRef.current = callback\n optionsRef.current = mergedOptions\n managerRef.current = manager\n sequenceRef.current = sequence\n\n // Track previous target and sequence to detect changes requiring re-registration\n const prevTargetRef = useRef<HTMLElement | Document | Window | null>(null)\n const prevSequenceRef = useRef<string | null>(null)\n\n // Normalize to hotkey sequence string (join with spaces)\n const hotkeySequenceString = formatHotkeySequence(sequence)\n\n // Extract options without target (target is handled separately)\n const { target: _target, ...optionsWithoutTarget } = mergedOptions\n\n useEffect(() => {\n if (sequenceRef.current.length === 0) {\n return\n }\n\n // Resolve target inside the effect so refs are already attached after mount\n const resolvedTarget = isRef(optionsRef.current.target)\n ? optionsRef.current.target.current\n : (optionsRef.current.target ??\n (typeof document !== 'undefined' ? document : null))\n\n // Skip if no valid target (SSR or ref still null)\n if (!resolvedTarget) {\n return\n }\n\n // Check if we need to re-register (target or sequence changed)\n const targetChanged =\n prevTargetRef.current !== null && prevTargetRef.current !== resolvedTarget\n const sequenceChanged =\n prevSequenceRef.current !== null &&\n prevSequenceRef.current !== hotkeySequenceString\n\n // If we have an active registration and target/sequence changed, unregister first\n if (\n registrationRef.current?.isActive &&\n (targetChanged || sequenceChanged)\n ) {\n registrationRef.current.unregister()\n registrationRef.current = null\n }\n\n // Register if needed (no active registration)\n if (!registrationRef.current || !registrationRef.current.isActive) {\n registrationRef.current = managerRef.current.register(\n sequenceRef.current,\n (event, context) => callbackRef.current(event, context),\n {\n ...optionsRef.current,\n target: resolvedTarget,\n },\n )\n }\n\n // Update tracking refs\n prevTargetRef.current = resolvedTarget\n prevSequenceRef.current = hotkeySequenceString\n\n // Cleanup on unmount\n return () => {\n if (registrationRef.current?.isActive) {\n registrationRef.current.unregister()\n registrationRef.current = null\n }\n }\n }, [hotkeySequenceString, mergedOptions.enabled])\n\n // Sync callback and options on EVERY render (outside useEffect)\n if (registrationRef.current?.isActive) {\n registrationRef.current.callback = (\n event: KeyboardEvent,\n context: HotkeyCallbackContext,\n ) => callbackRef.current(event, context)\n registrationRef.current.setOptions(optionsWithoutTarget)\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuEA,SAAgB,kBACd,UACA,UACA,UAAoC,EAAE,EAChC;CACN,MAAM,gBAAgB;EACpB,GAAG,0BAA0B,CAAC;EAC9B,GAAG;EACJ;CAED,MAAM,UAAU,oBAAoB;CAGpC,MAAM,kBAAkB,OAA0C,KAAK;CAGvE,MAAM,cAAc,OAAO,SAAS;CACpC,MAAM,aAAa,OAAO,cAAc;CACxC,MAAM,aAAa,OAAO,QAAQ;CAClC,MAAM,cAAc,OAAO,SAAS;AAGpC,aAAY,UAAU;AACtB,YAAW,UAAU;AACrB,YAAW,UAAU;AACrB,aAAY,UAAU;CAGtB,MAAM,gBAAgB,OAA+C,KAAK;CAC1E,MAAM,kBAAkB,OAAsB,KAAK;CAGnD,MAAM,uBAAuB,qBAAqB,SAAS;CAG3D,MAAM,EAAE,QAAQ,SAAS,GAAG,yBAAyB;AAErD,iBAAgB;AACd,MAAI,YAAY,QAAQ,WAAW,EACjC;EAIF,MAAM,iBAAiB,MAAM,WAAW,QAAQ,OAAO,GACnD,WAAW,QAAQ,OAAO,UACzB,WAAW,QAAQ,WACnB,OAAO,aAAa,cAAc,WAAW;AAGlD,MAAI,CAAC,eACH;EAIF,MAAM,gBACJ,cAAc,YAAY,QAAQ,cAAc,YAAY;EAC9D,MAAM,kBACJ,gBAAgB,YAAY,QAC5B,gBAAgB,YAAY;AAG9B,MACE,gBAAgB,SAAS,aACxB,iBAAiB,kBAClB;AACA,mBAAgB,QAAQ,YAAY;AACpC,mBAAgB,UAAU;;AAI5B,MAAI,CAAC,gBAAgB,WAAW,CAAC,gBAAgB,QAAQ,SACvD,iBAAgB,UAAU,WAAW,QAAQ,SAC3C,YAAY,UACX,OAAO,YAAY,YAAY,QAAQ,OAAO,QAAQ,EACvD;GACE,GAAG,WAAW;GACd,QAAQ;GACT,CACF;AAIH,gBAAc,UAAU;AACxB,kBAAgB,UAAU;AAG1B,eAAa;AACX,OAAI,gBAAgB,SAAS,UAAU;AACrC,oBAAgB,QAAQ,YAAY;AACpC,oBAAgB,UAAU;;;IAG7B,CAAC,sBAAsB,cAAc,QAAQ,CAAC;AAGjD,KAAI,gBAAgB,SAAS,UAAU;AACrC,kBAAgB,QAAQ,YACtB,OACA,YACG,YAAY,QAAQ,OAAO,QAAQ;AACxC,kBAAgB,QAAQ,WAAW,qBAAqB"}
@@ -0,0 +1,42 @@
1
+ const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
+ const require_HotkeysProvider = require('./HotkeysProvider.cjs');
3
+ let _tanstack_hotkeys = require("@tanstack/hotkeys");
4
+ let react = require("react");
5
+ let _tanstack_react_store = require("@tanstack/react-store");
6
+
7
+ //#region src/useHotkeySequenceRecorder.ts
8
+ /**
9
+ * React hook for recording multi-chord sequences (Vim-style shortcuts).
10
+ *
11
+ * @param options - Configuration options for the hotkey sequence recorder
12
+ */
13
+ function useHotkeySequenceRecorder(options) {
14
+ const mergedOptions = {
15
+ ...require_HotkeysProvider.useDefaultHotkeysOptions().hotkeySequenceRecorder,
16
+ ...options
17
+ };
18
+ const recorderRef = (0, react.useRef)(null);
19
+ if (!recorderRef.current) recorderRef.current = new _tanstack_hotkeys.HotkeySequenceRecorder(mergedOptions);
20
+ recorderRef.current.setOptions(mergedOptions);
21
+ const isRecording = (0, _tanstack_react_store.useStore)(recorderRef.current.store, (state) => state.isRecording);
22
+ const steps = (0, _tanstack_react_store.useStore)(recorderRef.current.store, (state) => state.steps);
23
+ const recordedSequence = (0, _tanstack_react_store.useStore)(recorderRef.current.store, (state) => state.recordedSequence);
24
+ (0, react.useEffect)(() => {
25
+ return () => {
26
+ recorderRef.current?.destroy();
27
+ };
28
+ }, []);
29
+ return {
30
+ isRecording,
31
+ steps,
32
+ recordedSequence,
33
+ startRecording: () => recorderRef.current?.start(),
34
+ stopRecording: () => recorderRef.current?.stop(),
35
+ cancelRecording: () => recorderRef.current?.cancel(),
36
+ commitRecording: () => recorderRef.current?.commit()
37
+ };
38
+ }
39
+
40
+ //#endregion
41
+ exports.useHotkeySequenceRecorder = useHotkeySequenceRecorder;
42
+ //# sourceMappingURL=useHotkeySequenceRecorder.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useHotkeySequenceRecorder.cjs","names":["useDefaultHotkeysOptions","HotkeySequenceRecorder"],"sources":["../src/useHotkeySequenceRecorder.ts"],"sourcesContent":["import { useEffect, useRef } from 'react'\nimport { useStore } from '@tanstack/react-store'\nimport { HotkeySequenceRecorder } from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport type {\n HotkeySequence,\n HotkeySequenceRecorderOptions,\n} from '@tanstack/hotkeys'\n\nexport interface ReactHotkeySequenceRecorder {\n /** Whether recording is currently active */\n isRecording: boolean\n /** Chords captured in the current session */\n steps: HotkeySequence\n /** Last committed sequence */\n recordedSequence: HotkeySequence | null\n startRecording: () => void\n stopRecording: () => void\n cancelRecording: () => void\n /** Commit current steps (no-op if empty) */\n commitRecording: () => void\n}\n\n/**\n * React hook for recording multi-chord sequences (Vim-style shortcuts).\n *\n * @param options - Configuration options for the hotkey sequence recorder\n */\nexport function useHotkeySequenceRecorder(\n options: HotkeySequenceRecorderOptions,\n): ReactHotkeySequenceRecorder {\n const mergedOptions = {\n ...useDefaultHotkeysOptions().hotkeySequenceRecorder,\n ...options,\n } as HotkeySequenceRecorderOptions\n\n const recorderRef = useRef<HotkeySequenceRecorder | null>(null)\n\n if (!recorderRef.current) {\n recorderRef.current = new HotkeySequenceRecorder(mergedOptions)\n }\n\n recorderRef.current.setOptions(mergedOptions)\n\n const isRecording = useStore(\n recorderRef.current.store,\n (state) => state.isRecording,\n )\n const steps = useStore(recorderRef.current.store, (state) => state.steps)\n const recordedSequence = useStore(\n recorderRef.current.store,\n (state) => state.recordedSequence,\n )\n\n useEffect(() => {\n return () => {\n recorderRef.current?.destroy()\n }\n }, [])\n\n return {\n isRecording,\n steps,\n recordedSequence,\n startRecording: () => recorderRef.current?.start(),\n stopRecording: () => recorderRef.current?.stop(),\n cancelRecording: () => recorderRef.current?.cancel(),\n commitRecording: () => recorderRef.current?.commit(),\n }\n}\n"],"mappings":";;;;;;;;;;;;AA4BA,SAAgB,0BACd,SAC6B;CAC7B,MAAM,gBAAgB;EACpB,GAAGA,kDAA0B,CAAC;EAC9B,GAAG;EACJ;CAED,MAAM,gCAAoD,KAAK;AAE/D,KAAI,CAAC,YAAY,QACf,aAAY,UAAU,IAAIC,yCAAuB,cAAc;AAGjE,aAAY,QAAQ,WAAW,cAAc;CAE7C,MAAM,kDACJ,YAAY,QAAQ,QACnB,UAAU,MAAM,YAClB;CACD,MAAM,4CAAiB,YAAY,QAAQ,QAAQ,UAAU,MAAM,MAAM;CACzE,MAAM,uDACJ,YAAY,QAAQ,QACnB,UAAU,MAAM,iBAClB;AAED,4BAAgB;AACd,eAAa;AACX,eAAY,SAAS,SAAS;;IAE/B,EAAE,CAAC;AAEN,QAAO;EACL;EACA;EACA;EACA,sBAAsB,YAAY,SAAS,OAAO;EAClD,qBAAqB,YAAY,SAAS,MAAM;EAChD,uBAAuB,YAAY,SAAS,QAAQ;EACpD,uBAAuB,YAAY,SAAS,QAAQ;EACrD"}
@@ -0,0 +1,25 @@
1
+ import { HotkeySequence, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
2
+
3
+ //#region src/useHotkeySequenceRecorder.d.ts
4
+ interface ReactHotkeySequenceRecorder {
5
+ /** Whether recording is currently active */
6
+ isRecording: boolean;
7
+ /** Chords captured in the current session */
8
+ steps: HotkeySequence;
9
+ /** Last committed sequence */
10
+ recordedSequence: HotkeySequence | null;
11
+ startRecording: () => void;
12
+ stopRecording: () => void;
13
+ cancelRecording: () => void;
14
+ /** Commit current steps (no-op if empty) */
15
+ commitRecording: () => void;
16
+ }
17
+ /**
18
+ * React hook for recording multi-chord sequences (Vim-style shortcuts).
19
+ *
20
+ * @param options - Configuration options for the hotkey sequence recorder
21
+ */
22
+ declare function useHotkeySequenceRecorder(options: HotkeySequenceRecorderOptions): ReactHotkeySequenceRecorder;
23
+ //#endregion
24
+ export { ReactHotkeySequenceRecorder, useHotkeySequenceRecorder };
25
+ //# sourceMappingURL=useHotkeySequenceRecorder.d.cts.map
@@ -0,0 +1,25 @@
1
+ import { HotkeySequence, HotkeySequenceRecorderOptions } from "@tanstack/hotkeys";
2
+
3
+ //#region src/useHotkeySequenceRecorder.d.ts
4
+ interface ReactHotkeySequenceRecorder {
5
+ /** Whether recording is currently active */
6
+ isRecording: boolean;
7
+ /** Chords captured in the current session */
8
+ steps: HotkeySequence;
9
+ /** Last committed sequence */
10
+ recordedSequence: HotkeySequence | null;
11
+ startRecording: () => void;
12
+ stopRecording: () => void;
13
+ cancelRecording: () => void;
14
+ /** Commit current steps (no-op if empty) */
15
+ commitRecording: () => void;
16
+ }
17
+ /**
18
+ * React hook for recording multi-chord sequences (Vim-style shortcuts).
19
+ *
20
+ * @param options - Configuration options for the hotkey sequence recorder
21
+ */
22
+ declare function useHotkeySequenceRecorder(options: HotkeySequenceRecorderOptions): ReactHotkeySequenceRecorder;
23
+ //#endregion
24
+ export { ReactHotkeySequenceRecorder, useHotkeySequenceRecorder };
25
+ //# sourceMappingURL=useHotkeySequenceRecorder.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { useDefaultHotkeysOptions } from "./HotkeysProvider.js";
2
+ import { HotkeySequenceRecorder } from "@tanstack/hotkeys";
3
+ import { useEffect, useRef } from "react";
4
+ import { useStore } from "@tanstack/react-store";
5
+
6
+ //#region src/useHotkeySequenceRecorder.ts
7
+ /**
8
+ * React hook for recording multi-chord sequences (Vim-style shortcuts).
9
+ *
10
+ * @param options - Configuration options for the hotkey sequence recorder
11
+ */
12
+ function useHotkeySequenceRecorder(options) {
13
+ const mergedOptions = {
14
+ ...useDefaultHotkeysOptions().hotkeySequenceRecorder,
15
+ ...options
16
+ };
17
+ const recorderRef = useRef(null);
18
+ if (!recorderRef.current) recorderRef.current = new HotkeySequenceRecorder(mergedOptions);
19
+ recorderRef.current.setOptions(mergedOptions);
20
+ const isRecording = useStore(recorderRef.current.store, (state) => state.isRecording);
21
+ const steps = useStore(recorderRef.current.store, (state) => state.steps);
22
+ const recordedSequence = useStore(recorderRef.current.store, (state) => state.recordedSequence);
23
+ useEffect(() => {
24
+ return () => {
25
+ recorderRef.current?.destroy();
26
+ };
27
+ }, []);
28
+ return {
29
+ isRecording,
30
+ steps,
31
+ recordedSequence,
32
+ startRecording: () => recorderRef.current?.start(),
33
+ stopRecording: () => recorderRef.current?.stop(),
34
+ cancelRecording: () => recorderRef.current?.cancel(),
35
+ commitRecording: () => recorderRef.current?.commit()
36
+ };
37
+ }
38
+
39
+ //#endregion
40
+ export { useHotkeySequenceRecorder };
41
+ //# sourceMappingURL=useHotkeySequenceRecorder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useHotkeySequenceRecorder.js","names":[],"sources":["../src/useHotkeySequenceRecorder.ts"],"sourcesContent":["import { useEffect, useRef } from 'react'\nimport { useStore } from '@tanstack/react-store'\nimport { HotkeySequenceRecorder } from '@tanstack/hotkeys'\nimport { useDefaultHotkeysOptions } from './HotkeysProvider'\nimport type {\n HotkeySequence,\n HotkeySequenceRecorderOptions,\n} from '@tanstack/hotkeys'\n\nexport interface ReactHotkeySequenceRecorder {\n /** Whether recording is currently active */\n isRecording: boolean\n /** Chords captured in the current session */\n steps: HotkeySequence\n /** Last committed sequence */\n recordedSequence: HotkeySequence | null\n startRecording: () => void\n stopRecording: () => void\n cancelRecording: () => void\n /** Commit current steps (no-op if empty) */\n commitRecording: () => void\n}\n\n/**\n * React hook for recording multi-chord sequences (Vim-style shortcuts).\n *\n * @param options - Configuration options for the hotkey sequence recorder\n */\nexport function useHotkeySequenceRecorder(\n options: HotkeySequenceRecorderOptions,\n): ReactHotkeySequenceRecorder {\n const mergedOptions = {\n ...useDefaultHotkeysOptions().hotkeySequenceRecorder,\n ...options,\n } as HotkeySequenceRecorderOptions\n\n const recorderRef = useRef<HotkeySequenceRecorder | null>(null)\n\n if (!recorderRef.current) {\n recorderRef.current = new HotkeySequenceRecorder(mergedOptions)\n }\n\n recorderRef.current.setOptions(mergedOptions)\n\n const isRecording = useStore(\n recorderRef.current.store,\n (state) => state.isRecording,\n )\n const steps = useStore(recorderRef.current.store, (state) => state.steps)\n const recordedSequence = useStore(\n recorderRef.current.store,\n (state) => state.recordedSequence,\n )\n\n useEffect(() => {\n return () => {\n recorderRef.current?.destroy()\n }\n }, [])\n\n return {\n isRecording,\n steps,\n recordedSequence,\n startRecording: () => recorderRef.current?.start(),\n stopRecording: () => recorderRef.current?.stop(),\n cancelRecording: () => recorderRef.current?.cancel(),\n commitRecording: () => recorderRef.current?.commit(),\n }\n}\n"],"mappings":";;;;;;;;;;;AA4BA,SAAgB,0BACd,SAC6B;CAC7B,MAAM,gBAAgB;EACpB,GAAG,0BAA0B,CAAC;EAC9B,GAAG;EACJ;CAED,MAAM,cAAc,OAAsC,KAAK;AAE/D,KAAI,CAAC,YAAY,QACf,aAAY,UAAU,IAAI,uBAAuB,cAAc;AAGjE,aAAY,QAAQ,WAAW,cAAc;CAE7C,MAAM,cAAc,SAClB,YAAY,QAAQ,QACnB,UAAU,MAAM,YAClB;CACD,MAAM,QAAQ,SAAS,YAAY,QAAQ,QAAQ,UAAU,MAAM,MAAM;CACzE,MAAM,mBAAmB,SACvB,YAAY,QAAQ,QACnB,UAAU,MAAM,iBAClB;AAED,iBAAgB;AACd,eAAa;AACX,eAAY,SAAS,SAAS;;IAE/B,EAAE,CAAC;AAEN,QAAO;EACL;EACA;EACA;EACA,sBAAsB,YAAY,SAAS,OAAO;EAClD,qBAAqB,YAAY,SAAS,MAAM;EAChD,uBAAuB,YAAY,SAAS,QAAQ;EACpD,uBAAuB,YAAY,SAAS,QAAQ;EACrD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-hotkeys",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "React adapter for TanStack Hotkeys",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -40,7 +40,7 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "@tanstack/react-store": "^0.9.2",
43
- "@tanstack/hotkeys": "0.4.2"
43
+ "@tanstack/hotkeys": "0.5.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@eslint-react/eslint-plugin": "^3.0.0",
@@ -1,12 +1,16 @@
1
1
  import React, { createContext, useContext, useMemo } from 'react'
2
2
  import type { ReactNode } from 'react'
3
- import type { HotkeyRecorderOptions } from '@tanstack/hotkeys'
3
+ import type {
4
+ HotkeyRecorderOptions,
5
+ HotkeySequenceRecorderOptions,
6
+ } from '@tanstack/hotkeys'
4
7
  import type { UseHotkeyOptions } from './useHotkey'
5
8
  import type { UseHotkeySequenceOptions } from './useHotkeySequence'
6
9
 
7
10
  export interface HotkeysProviderOptions {
8
11
  hotkey?: Partial<UseHotkeyOptions>
9
12
  hotkeyRecorder?: Partial<HotkeyRecorderOptions>
13
+ hotkeySequenceRecorder?: Partial<HotkeySequenceRecorderOptions>
10
14
  hotkeySequence?: Partial<UseHotkeySequenceOptions>
11
15
  }
12
16
 
package/src/index.ts CHANGED
@@ -12,3 +12,4 @@ export * from './useHeldKeyCodes'
12
12
  export * from './useKeyHold'
13
13
  export * from './useHotkeySequence'
14
14
  export * from './useHotkeyRecorder'
15
+ export * from './useHotkeySequenceRecorder'
@@ -33,6 +33,11 @@ export interface UseHotkeySequenceOptions extends Omit<
33
33
  * This hook allows you to register multi-key sequences like 'g g' or 'd d'
34
34
  * that trigger when the full sequence is pressed within a timeout.
35
35
  *
36
+ * Each step may include modifiers. You can chain the same modifier across
37
+ * steps (e.g. `Shift+R` then `Shift+T`). Modifier-only keydown events (Shift,
38
+ * Control, Alt, or Meta pressed alone) are ignored while matching—they do not
39
+ * advance the sequence or reset progress.
40
+ *
36
41
  * @param sequence - Array of hotkey strings that form the sequence
37
42
  * @param callback - Function to call when the sequence is completed
38
43
  * @param options - Options for the sequence behavior
@@ -55,6 +60,11 @@ export interface UseHotkeySequenceOptions extends Omit<
55
60
  * deleteInnerWord()
56
61
  * }, { timeout: 500 })
57
62
  *
63
+ * // Same modifier on consecutive steps (bare Shift between chords is ignored)
64
+ * useHotkeySequence(['Shift+R', 'Shift+T'], () => {
65
+ * nextAction()
66
+ * })
67
+ *
58
68
  * return <div>...</div>
59
69
  * }
60
70
  * ```
@@ -0,0 +1,70 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import { useStore } from '@tanstack/react-store'
3
+ import { HotkeySequenceRecorder } from '@tanstack/hotkeys'
4
+ import { useDefaultHotkeysOptions } from './HotkeysProvider'
5
+ import type {
6
+ HotkeySequence,
7
+ HotkeySequenceRecorderOptions,
8
+ } from '@tanstack/hotkeys'
9
+
10
+ export interface ReactHotkeySequenceRecorder {
11
+ /** Whether recording is currently active */
12
+ isRecording: boolean
13
+ /** Chords captured in the current session */
14
+ steps: HotkeySequence
15
+ /** Last committed sequence */
16
+ recordedSequence: HotkeySequence | null
17
+ startRecording: () => void
18
+ stopRecording: () => void
19
+ cancelRecording: () => void
20
+ /** Commit current steps (no-op if empty) */
21
+ commitRecording: () => void
22
+ }
23
+
24
+ /**
25
+ * React hook for recording multi-chord sequences (Vim-style shortcuts).
26
+ *
27
+ * @param options - Configuration options for the hotkey sequence recorder
28
+ */
29
+ export function useHotkeySequenceRecorder(
30
+ options: HotkeySequenceRecorderOptions,
31
+ ): ReactHotkeySequenceRecorder {
32
+ const mergedOptions = {
33
+ ...useDefaultHotkeysOptions().hotkeySequenceRecorder,
34
+ ...options,
35
+ } as HotkeySequenceRecorderOptions
36
+
37
+ const recorderRef = useRef<HotkeySequenceRecorder | null>(null)
38
+
39
+ if (!recorderRef.current) {
40
+ recorderRef.current = new HotkeySequenceRecorder(mergedOptions)
41
+ }
42
+
43
+ recorderRef.current.setOptions(mergedOptions)
44
+
45
+ const isRecording = useStore(
46
+ recorderRef.current.store,
47
+ (state) => state.isRecording,
48
+ )
49
+ const steps = useStore(recorderRef.current.store, (state) => state.steps)
50
+ const recordedSequence = useStore(
51
+ recorderRef.current.store,
52
+ (state) => state.recordedSequence,
53
+ )
54
+
55
+ useEffect(() => {
56
+ return () => {
57
+ recorderRef.current?.destroy()
58
+ }
59
+ }, [])
60
+
61
+ return {
62
+ isRecording,
63
+ steps,
64
+ recordedSequence,
65
+ startRecording: () => recorderRef.current?.start(),
66
+ stopRecording: () => recorderRef.current?.stop(),
67
+ cancelRecording: () => recorderRef.current?.cancel(),
68
+ commitRecording: () => recorderRef.current?.commit(),
69
+ }
70
+ }