'use client'; // Player keyboard shortcuts via ui-core's useHotkey (react-hotkeys-hook). // // Idiomatic pattern: each useHotkey call returns a callback ref that scopes // the binding to the element it's attached to. We compose all of them into // one ref and hand it to the player container. Library handles focus gating; // no manual `document.activeElement` polling, no `data-scope` juggling. import { useCallback } from 'react'; import { useHotkey } from '@djangocfg/ui-core/hooks'; import { clamp } from '../utils/clamp'; import type { PlayerControls } from '../types'; export type HotkeyBinding = { /** Human label for help dialogs and tooltips. */ label: string; /** Pretty hint shown to users (e.g. "Space", "←", "M"). */ hint: string; }; export type UseKeyboardShortcutsOptions = { audio: HTMLAudioElement; controls: PlayerControls; enabled?: boolean; }; export type UseKeyboardShortcutsReturn = { /** Attach to the player container — react-hotkeys-hook only fires when * focus is within this element. Composes with other refs via callback. */ ref: (instance: HTMLElement | null) => void; /** All registered bindings — useful for rendering a help dialog. */ bindings: ReadonlyArray; }; const OPTS = { preventDefault: true } as const; const BINDINGS: ReadonlyArray = [ { label: 'Play / pause', hint: 'Space' }, { label: 'Seek +5s', hint: '→' }, { label: 'Seek −5s', hint: '←' }, { label: 'Volume up', hint: '↑' }, { label: 'Volume down', hint: '↓' }, { label: 'Toggle mute', hint: 'M' }, { label: 'Toggle loop', hint: 'L' }, ]; export function useKeyboardShortcuts({ audio, controls, enabled = true, }: UseKeyboardShortcutsOptions): UseKeyboardShortcutsReturn { // Each useHotkey returns a callback ref that scopes the binding to its // element. We must call them at the top level (rules of hooks). const refToggle = useHotkey(['space', 'k'], () => void controls.toggle(), { ...OPTS, enabled, description: 'Play / pause', }); const refForward = useHotkey('right', () => controls.seek(audio.currentTime + 5), { ...OPTS, enabled, description: 'Seek +5s', }); const refBackward = useHotkey('left', () => controls.seek(audio.currentTime - 5), { ...OPTS, enabled, description: 'Seek −5s', }); const refVolUp = useHotkey('up', () => controls.setVolume(clamp(audio.volume + 0.05, 0, 1)), { ...OPTS, enabled, description: 'Volume up', }); const refVolDown = useHotkey('down', () => controls.setVolume(clamp(audio.volume - 0.05, 0, 1)), { ...OPTS, enabled, description: 'Volume down', }); const refMute = useHotkey('m', () => controls.toggleMute(), { ...OPTS, enabled, description: 'Toggle mute', }); const refLoop = useHotkey('l', () => controls.toggleLoop(), { ...OPTS, enabled, description: 'Toggle loop', }); const ref = useCallback( (instance: HTMLElement | null) => { refToggle(instance); refForward(instance); refBackward(instance); refVolUp(instance); refVolDown(instance); refMute(instance); refLoop(instance); }, [refToggle, refForward, refBackward, refVolUp, refVolDown, refMute, refLoop], ); return { ref, bindings: BINDINGS }; }