'use client'; // Inner shell that runs inside . Owns variant selection, // keyboard shortcuts and MediaSession wiring; renders the picked layout. import { useCallback, useEffect, useImperativeHandle, useState } from 'react'; import { useIsPhone } from '@djangocfg/ui-core/hooks'; import { usePlayerAudio, usePlayerControls, usePlayerMeta } from './context/selectors'; import { useElementWidth } from './hooks/useResizeObserver'; import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; import { useMediaSession } from './hooks/useMediaSession'; import { CompactLayout, DefaultLayout } from './parts/Layout'; import type { PlayerHandle, PlayerProps, PlayerVariant } from './types'; const COMPACT_BREAKPOINT = 480; type Props = Pick< PlayerProps, | 'className' | 'variant' | 'waveform' | 'reactiveCover' | 'onPrev' | 'onNext' | 'enableKeyboardShortcuts' | 'ariaLabel' | 'seekStartsPlayback' | 'autoFocus' > & { handleRef?: React.Ref; }; export function PlayerShell({ className = '', variant = 'auto', waveform, reactiveCover = false, onPrev, onNext, enableKeyboardShortcuts = true, ariaLabel, seekStartsPlayback = true, autoFocus = false, handleRef, }: Props) { const [container, setContainer] = useState(null); const audio = usePlayerAudio(); const controls = usePlayerControls(); const meta = usePlayerMeta(); const width = useElementWidth(container); const isPhone = useIsPhone(); // `auto` resolves to the layout best for the available space: // - on a phone viewport we always use compact (regardless of container); // - otherwise compact only when the container itself is narrower than // COMPACT_BREAKPOINT. const resolvedVariant: PlayerVariant = variant === 'auto' ? isPhone || (width > 0 && width < COMPACT_BREAKPOINT) ? 'compact' : 'default' : variant; useMediaSession(audio, meta, controls, onPrev, onNext); const hotkeys = useKeyboardShortcuts({ audio, controls, enabled: enableKeyboardShortcuts, }); // Compose the container state-ref with the hotkey scoping ref. Both want // to see the same DOM node — one for layout/keyboard-focus management, the // other for react-hotkeys-hook's scoped listener. const setRootRef = useCallback( (node: HTMLDivElement | null) => { setContainer(node); hotkeys.ref(node); }, [hotkeys], ); useImperativeHandle( handleRef, (): PlayerHandle => ({ audio, play: () => controls.play(), pause: () => controls.pause(), seek: (s: number) => controls.seek(s), getCurrentTime: () => audio.currentTime, getDuration: () => (Number.isFinite(audio.duration) ? audio.duration : 0), focus: () => container?.focus(), }), [audio, controls, container], ); // Keyboard shortcuts work only when the container can take focus. useEffect(() => { if (!container || container.hasAttribute('tabindex')) return; container.setAttribute('tabindex', '0'); }, [container]); // Declarative autoFocus: focus the container once the DOM node is ready. // Parents that want a *fresh* focus per source remount us via `key={src}`. useEffect(() => { if (!autoFocus || !container) return; container.focus({ preventScroll: true }); }, [autoFocus, container]); return (
{resolvedVariant === 'compact' ? ( ) : ( )}
); }