@lucaismyname/ginger 0.0.40 → 0.0.42

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.
Files changed (52) hide show
  1. package/README.md +8 -2
  2. package/dist/client.cjs +1 -1
  3. package/dist/client.js +4 -4
  4. package/dist/context/GingerProvider.d.ts.map +1 -1
  5. package/dist/crossfade/crossfadeGraph.d.ts +47 -0
  6. package/dist/crossfade/crossfadeGraph.d.ts.map +1 -0
  7. package/dist/crossfade/index.cjs +2 -0
  8. package/dist/crossfade/index.cjs.map +1 -0
  9. package/dist/crossfade/index.d.ts +5 -0
  10. package/dist/crossfade/index.d.ts.map +1 -0
  11. package/dist/crossfade/index.js +124 -0
  12. package/dist/crossfade/index.js.map +1 -0
  13. package/dist/crossfade/useGingerCrossfade.d.ts +66 -0
  14. package/dist/crossfade/useGingerCrossfade.d.ts.map +1 -0
  15. package/dist/equalizer/index.cjs +1 -1
  16. package/dist/equalizer/index.js +1 -1
  17. package/dist/{ginger-Ca8910_n.js → ginger-B2DgE-2a.js} +312 -306
  18. package/dist/{ginger-Ca8910_n.js.map → ginger-B2DgE-2a.js.map} +1 -1
  19. package/dist/ginger-Dv3iO_xQ.cjs +2 -0
  20. package/dist/{ginger-DrD8F4HX.cjs.map → ginger-Dv3iO_xQ.cjs.map} +1 -1
  21. package/dist/index.cjs +1 -1
  22. package/dist/index.js +4 -4
  23. package/dist/remote/index.cjs +1 -1
  24. package/dist/remote/index.js +1 -1
  25. package/dist/selectors-BT3WSsKN.js +42 -0
  26. package/dist/selectors-BT3WSsKN.js.map +1 -0
  27. package/dist/selectors-CEGlYoFu.cjs +2 -0
  28. package/dist/selectors-CEGlYoFu.cjs.map +1 -0
  29. package/dist/spatial/index.cjs +1 -1
  30. package/dist/spatial/index.js +1 -1
  31. package/dist/testing/index.cjs +1 -1
  32. package/dist/testing/index.js +1 -1
  33. package/dist/transitions-CmNkf3sd.js +90 -0
  34. package/dist/transitions-CmNkf3sd.js.map +1 -0
  35. package/dist/transitions-Dx08t68T.cjs +2 -0
  36. package/dist/transitions-Dx08t68T.cjs.map +1 -0
  37. package/dist/{useGinger-BXgia32v.cjs → useGinger-4uvPoChz.cjs} +2 -2
  38. package/dist/{useGinger-BXgia32v.cjs.map → useGinger-4uvPoChz.cjs.map} +1 -1
  39. package/dist/{useGinger-hpp2pAGY.js → useGinger-Dz0cPyD1.js} +2 -2
  40. package/dist/{useGinger-hpp2pAGY.js.map → useGinger-Dz0cPyD1.js.map} +1 -1
  41. package/dist/useGingerChapterProgress-D2pdmyjg.cjs +2 -0
  42. package/dist/{useGingerChapterProgress-TeWWJ8Fd.cjs.map → useGingerChapterProgress-D2pdmyjg.cjs.map} +1 -1
  43. package/dist/{useGingerChapterProgress-Dbwiwnko.js → useGingerChapterProgress-wxAmN_uo.js} +25 -24
  44. package/dist/{useGingerChapterProgress-Dbwiwnko.js.map → useGingerChapterProgress-wxAmN_uo.js.map} +1 -1
  45. package/package.json +7 -2
  46. package/CHANGELOG.md +0 -18
  47. package/dist/ginger-DrD8F4HX.cjs +0 -2
  48. package/dist/selectors-BalBCc7X.js +0 -127
  49. package/dist/selectors-BalBCc7X.js.map +0 -1
  50. package/dist/selectors-YXnP8Y8g.cjs +0 -2
  51. package/dist/selectors-YXnP8Y8g.cjs.map +0 -1
  52. package/dist/useGingerChapterProgress-TeWWJ8Fd.cjs +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"useGingerChapterProgress-TeWWJ8Fd.cjs","sources":["../src/analyzer/useGingerLiveAnalyzer.ts","../src/hooks/useGingerKeyboardShortcuts.ts","../src/hooks/useGingerSleepTimer.ts","../src/hooks/useGingerDebugLog.ts","../src/hooks/useSeekDrag.ts","../src/hooks/useNextTrackPrefetch.ts","../src/store.ts","../src/hooks/useGingerPlaybackHistory.ts","../src/hooks/useGingerVolumeFade.ts","../src/hooks/useGingerChapterProgress.ts"],"sourcesContent":["import { useCallback, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport { useGinger } from \"../hooks/useGinger\";\nimport { type LiveAnalyserOptions, attachLiveAnalyser, detachLiveAnalyser } from \"./liveAudioGraph\";\n\nexport type UseGingerLiveAnalyzerOptions = {\n /** When false, the analyser is detached and no frames are read. Default true. */\n enabled?: boolean;\n fftSize?: number;\n smoothingTimeConstant?: number;\n minDecibels?: number;\n maxDecibels?: number;\n};\n\nexport type UseGingerLiveAnalyzerResult = {\n /** Byte frequency data (0–255); length equals `frequencyBinCount`. Updated each animation frame while enabled. */\n frequencyData: Uint8Array;\n /** Byte time-domain data (0–255); length equals `fftSize`. */\n timeDomainData: Uint8Array;\n frequencyBinCount: number;\n sampleRate: number;\n isSuspended: boolean;\n error: string | null;\n resume: () => Promise<void>;\n /**\n * Monotonically increasing counter incremented each animation frame.\n * Use as a `useMemo` / `useEffect` dependency to respond to new audio data, since\n * `frequencyData` and `timeDomainData` are mutated in-place (their reference never changes).\n */\n frame: number;\n};\n\nconst emptyFreq = new Uint8Array(0);\nconst emptyTime = new Uint8Array(0);\n\nexport function useGingerLiveAnalyzer(\n options: UseGingerLiveAnalyzerOptions = {},\n): UseGingerLiveAnalyzerResult {\n const {\n enabled = true,\n fftSize = 2048,\n smoothingTimeConstant = 0.8,\n minDecibels = -100,\n maxDecibels = -30,\n } = options;\n\n const { audioRef, state } = useGinger();\n const opts = useMemo<LiveAnalyserOptions>(\n () => ({\n fftSize,\n smoothingTimeConstant,\n minDecibels,\n maxDecibels,\n }),\n [fftSize, smoothingTimeConstant, minDecibels, maxDecibels],\n );\n\n const [frame, setFrame] = useState(0);\n const [error, setError] = useState<string | null>(null);\n const [isSuspended, setIsSuspended] = useState(false);\n const [meta, setMeta] = useState({ frequencyBinCount: 0, sampleRate: 0 });\n\n const freqRef = useRef<Uint8Array>(emptyFreq);\n const timeRef = useRef<Uint8Array>(emptyTime);\n\n const resume = useCallback(async () => {\n const ctx = contextHolderRef.current;\n if (ctx && ctx.state === \"suspended\") {\n await ctx.resume();\n }\n }, []);\n\n const contextHolderRef = useRef<AudioContext | null>(null);\n const analyserHolderRef = useRef<AnalyserNode | null>(null);\n\n useLayoutEffect(() => {\n if (!enabled || typeof window === \"undefined\") {\n return;\n }\n\n let cancelled = false;\n let consumerId: number | null = null;\n let element: HTMLAudioElement | null = null;\n let rafId = 0;\n\n const onStateChange = () => {\n const ctx = contextHolderRef.current;\n if (ctx) setIsSuspended(ctx.state === \"suspended\");\n };\n\n const runLoop = () => {\n if (cancelled) return;\n const a = analyserHolderRef.current;\n const fq = freqRef.current;\n const td = timeRef.current;\n if (a && fq.length > 0 && td.length > 0) {\n a.getByteFrequencyData(fq as Uint8Array<ArrayBuffer>);\n a.getByteTimeDomainData(td as Uint8Array<ArrayBuffer>);\n setFrame((n) => n + 1);\n }\n rafId = requestAnimationFrame(runLoop);\n };\n\n type AttachOutcome = \"ok\" | \"no-element\" | \"error\";\n\n const attach = (): AttachOutcome => {\n const el = audioRef.current;\n if (!el || cancelled) return \"no-element\";\n try {\n const { id, context, analyser } = attachLiveAnalyser(el, opts);\n consumerId = id;\n element = el;\n contextHolderRef.current = context;\n analyserHolderRef.current = analyser;\n setIsSuspended(context.state === \"suspended\");\n setError(null);\n\n context.addEventListener(\"statechange\", onStateChange);\n\n const n = analyser.frequencyBinCount;\n const fft = analyser.fftSize;\n freqRef.current = new Uint8Array(n);\n timeRef.current = new Uint8Array(fft);\n setMeta({ frequencyBinCount: n, sampleRate: context.sampleRate });\n\n rafId = requestAnimationFrame(runLoop);\n return \"ok\";\n } catch (e) {\n const msg = e instanceof Error ? e.message : \"Failed to attach live analyser\";\n setError(msg);\n contextHolderRef.current = null;\n analyserHolderRef.current = null;\n freqRef.current = emptyFreq;\n timeRef.current = emptyTime;\n setMeta({ frequencyBinCount: 0, sampleRate: 0 });\n return \"error\";\n }\n };\n\n const first = attach();\n if (first !== \"ok\") {\n let retryRaf = 0;\n const maxAttempts = 120;\n let attempts = 0;\n\n const retryLoop = () => {\n if (cancelled) return;\n const out = attach();\n if (out === \"ok\" || out === \"error\") return;\n attempts += 1;\n if (attempts >= maxAttempts) return;\n retryRaf = requestAnimationFrame(retryLoop);\n };\n\n if (first === \"no-element\") {\n retryRaf = requestAnimationFrame(retryLoop);\n }\n\n return () => {\n cancelled = true;\n cancelAnimationFrame(retryRaf);\n cancelAnimationFrame(rafId);\n if (consumerId != null && element) {\n detachLiveAnalyser(element, consumerId);\n }\n contextHolderRef.current?.removeEventListener(\"statechange\", onStateChange);\n contextHolderRef.current = null;\n analyserHolderRef.current = null;\n freqRef.current = emptyFreq;\n timeRef.current = emptyTime;\n };\n }\n\n return () => {\n cancelled = true;\n cancelAnimationFrame(rafId);\n if (consumerId != null && element) {\n detachLiveAnalyser(element, consumerId);\n }\n contextHolderRef.current?.removeEventListener(\"statechange\", onStateChange);\n contextHolderRef.current = null;\n analyserHolderRef.current = null;\n freqRef.current = emptyFreq;\n timeRef.current = emptyTime;\n setMeta({ frequencyBinCount: 0, sampleRate: 0 });\n };\n }, [enabled, audioRef, opts, state.currentIndex]);\n\n return {\n frequencyData: freqRef.current,\n timeDomainData: timeRef.current,\n frequencyBinCount: meta.frequencyBinCount,\n sampleRate: meta.sampleRate,\n isSuspended,\n error,\n resume,\n frame,\n };\n}\n","import { useEffect } from \"react\";\nimport { useGingerMedia, useGingerPlayback } from \"../context/GingerSplitContexts\";\n\nexport type GingerKeyboardShortcutBindings = {\n playPause?: string;\n next?: string;\n previous?: string;\n mute?: string;\n /** Key to seek forward by `seekSeconds`. Default: undefined (disabled). */\n seekForward?: string;\n /** Key to seek backward by `seekSeconds`. Default: undefined (disabled). */\n seekBackward?: string;\n /** Seconds to seek per `seekForward` / `seekBackward` keypress. Default: 5. */\n seekSeconds?: number;\n};\n\nexport function useGingerKeyboardShortcuts(\n enabled = true,\n bindings: GingerKeyboardShortcutBindings = {},\n): void {\n const { togglePlayPause, next, prev } = useGingerPlayback();\n const { toggleMute, seek, currentTime, duration } = useGingerMedia();\n\n const {\n mute: muteBinding,\n seekForward: seekForwardBinding,\n seekBackward: seekBackwardBinding,\n } = bindings;\n\n useEffect(() => {\n if (!enabled || typeof window === \"undefined\") return;\n const playPause = (bindings.playPause ?? \" \").toLowerCase();\n const nextKey = (bindings.next ?? \"ArrowRight\").toLowerCase();\n const prevKey = (bindings.previous ?? \"ArrowLeft\").toLowerCase();\n const muteKey = muteBinding?.toLowerCase();\n const seekFwdKey = seekForwardBinding?.toLowerCase();\n const seekBwdKey = seekBackwardBinding?.toLowerCase();\n const seekSecs = bindings.seekSeconds ?? 5;\n\n const onKeyDown = (event: KeyboardEvent) => {\n const target = event.target as HTMLElement | null;\n if (\n target &&\n ([\"INPUT\", \"TEXTAREA\", \"SELECT\"].includes(target.tagName) || target.isContentEditable)\n )\n return;\n const key = event.key.toLowerCase();\n if (key === playPause) {\n event.preventDefault();\n togglePlayPause();\n } else if (key === nextKey) {\n event.preventDefault();\n next();\n } else if (key === prevKey) {\n event.preventDefault();\n prev();\n } else if (muteKey && key === muteKey) {\n event.preventDefault();\n toggleMute();\n } else if (seekFwdKey && key === seekFwdKey) {\n event.preventDefault();\n const dur = duration > 0 ? duration : Number.POSITIVE_INFINITY;\n seek(Math.min(dur, currentTime + seekSecs));\n } else if (seekBwdKey && key === seekBwdKey) {\n event.preventDefault();\n seek(Math.max(0, currentTime - seekSecs));\n }\n };\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [\n bindings.next,\n bindings.playPause,\n bindings.previous,\n bindings.seekSeconds,\n currentTime,\n duration,\n enabled,\n muteBinding,\n next,\n prev,\n seek,\n seekBackwardBinding,\n seekForwardBinding,\n toggleMute,\n togglePlayPause,\n ]);\n}\n","import { useEffect, useRef } from \"react\";\nimport { useGingerPlayback } from \"../context/GingerSplitContexts\";\n\nexport type GingerSleepTimerOptions = {\n durationMs?: number;\n stopAfterTracks?: number;\n respectPause?: boolean;\n enabled?: boolean;\n onFire?: () => void;\n};\n\nexport function useGingerSleepTimer(options: GingerSleepTimerOptions): void {\n const { durationMs, stopAfterTracks, respectPause = true, enabled = true, onFire } = options;\n const { currentIndex, pause, isPaused } = useGingerPlayback();\n const remainingTracksRef = useRef(stopAfterTracks ?? 0);\n const prevIndexRef = useRef(currentIndex);\n\n // Remaining milliseconds for the duration-based timer; carried across pause/resume cycles.\n const remainingMsRef = useRef(durationMs ?? 0);\n // Timestamp when the current timer segment started (set when playback resumes).\n const segmentStartRef = useRef<number | null>(null);\n\n useEffect(() => {\n remainingTracksRef.current = stopAfterTracks ?? 0;\n }, [stopAfterTracks]);\n\n // Keep remainingMsRef in sync when durationMs changes while the timer is inactive.\n const prevDurationMsRef = useRef(durationMs);\n useEffect(() => {\n if (prevDurationMsRef.current !== durationMs) {\n remainingMsRef.current = durationMs ?? 0;\n prevDurationMsRef.current = durationMs;\n }\n }, [durationMs]);\n\n useEffect(() => {\n if (!enabled || !durationMs || durationMs <= 0) {\n // Reset remaining when disabled or no duration\n remainingMsRef.current = durationMs ?? 0;\n segmentStartRef.current = null;\n return;\n }\n\n if (respectPause && isPaused) {\n // Snapshot how much time is left before pausing\n if (segmentStartRef.current !== null) {\n const elapsed = Date.now() - segmentStartRef.current;\n remainingMsRef.current = Math.max(0, remainingMsRef.current - elapsed);\n segmentStartRef.current = null;\n }\n return;\n }\n\n // Playing: start (or continue) the countdown from remainingMsRef\n segmentStartRef.current = Date.now();\n const id = setTimeout(() => {\n remainingMsRef.current = 0;\n segmentStartRef.current = null;\n pause();\n onFire?.();\n }, remainingMsRef.current);\n\n return () => {\n clearTimeout(id);\n // Snapshot remaining when effect cleans up (e.g. isPaused or deps changed)\n if (segmentStartRef.current !== null) {\n const elapsed = Date.now() - segmentStartRef.current;\n remainingMsRef.current = Math.max(0, remainingMsRef.current - elapsed);\n segmentStartRef.current = null;\n }\n };\n }, [durationMs, enabled, isPaused, onFire, pause, respectPause]);\n\n useEffect(() => {\n if (!enabled || !stopAfterTracks || stopAfterTracks <= 0) return;\n const prev = prevIndexRef.current;\n prevIndexRef.current = currentIndex;\n if (currentIndex === prev) return;\n remainingTracksRef.current -= 1;\n if (remainingTracksRef.current <= 0) {\n pause();\n onFire?.();\n }\n }, [currentIndex, enabled, onFire, pause, stopAfterTracks]);\n}\n","import { useEffect, useRef } from \"react\";\nimport { useGingerState } from \"../context/GingerSplitContexts\";\n\nexport function useGingerDebugLog(enabled = false): void {\n const state = useGingerState();\n const prevRef = useRef(state);\n\n useEffect(() => {\n if (!enabled || typeof console === \"undefined\") return;\n const prev = prevRef.current;\n if (prev !== state) {\n console.debug(\"[ginger]\", {\n from: {\n currentIndex: prev.currentIndex,\n isPaused: prev.isPaused,\n currentTime: prev.currentTime,\n repeatMode: prev.repeatMode,\n },\n to: {\n currentIndex: state.currentIndex,\n isPaused: state.isPaused,\n currentTime: state.currentTime,\n repeatMode: state.repeatMode,\n },\n });\n }\n prevRef.current = state;\n }, [enabled, state]);\n}\n","import { useCallback, useState } from \"react\";\nimport type { PointerEvent as ReactPointerEvent } from \"react\";\nimport {\n gingerStateFromContextValues,\n useGingerMedia,\n useGingerPlayback,\n} from \"../context/GingerSplitContexts\";\nimport { progressFraction } from \"../internal/selectors\";\n\nexport type SeekDragState = {\n /** Raw drag fraction — only updated during an active drag gesture. */\n fraction: number;\n /** Blended fraction: follows live playback when idle, drag position when dragging. */\n displayFraction: number;\n isDragging: boolean;\n onPointerDown: (event: ReactPointerEvent<HTMLElement>) => void;\n};\n\nfunction clamp01(value: number): number {\n return Math.max(0, Math.min(1, value));\n}\n\nexport function useSeekDrag(duration: number): SeekDragState {\n const media = useGingerMedia();\n const playback = useGingerPlayback();\n const { seek } = media;\n const [fraction, setFraction] = useState(0);\n const [isDragging, setIsDragging] = useState(false);\n\n const liveFraction = progressFraction(gingerStateFromContextValues(playback, media));\n const displayFraction = isDragging ? fraction : liveFraction;\n\n const onPointerDown = useCallback(\n (event: ReactPointerEvent<HTMLElement>) => {\n if (!(duration > 0)) return;\n const target = event.currentTarget;\n const rect = target.getBoundingClientRect();\n const update = (clientX: number) => {\n const ratio = clamp01((clientX - rect.left) / rect.width);\n setFraction(ratio);\n seek(ratio * duration);\n };\n setIsDragging(true);\n target.setPointerCapture(event.pointerId);\n update(event.clientX);\n const onMove = (moveEvent: PointerEvent) => update(moveEvent.clientX);\n const onUp = (upEvent: PointerEvent) => {\n update(upEvent.clientX);\n setIsDragging(false);\n target.releasePointerCapture(event.pointerId);\n target.removeEventListener(\"pointermove\", onMove);\n target.removeEventListener(\"pointerup\", onUp);\n target.removeEventListener(\"pointercancel\", onUp);\n };\n target.addEventListener(\"pointermove\", onMove);\n target.addEventListener(\"pointerup\", onUp);\n target.addEventListener(\"pointercancel\", onUp);\n },\n [duration, seek],\n );\n\n return { fraction, displayFraction, isDragging, onPointerDown };\n}\n","import { useEffect } from \"react\";\nimport { useGingerPlayback } from \"../context/GingerSplitContexts\";\nimport { computeNextIndex } from \"../core/transitions\";\n\nexport type UseNextTrackPrefetchOptions = {\n /** When false, no prefetch runs. Default true. */\n enabled?: boolean;\n /**\n * Match `crossOrigin` on `Ginger.Player` when `fileUrl` is cross-origin so the browser\n * can reuse cached media consistently.\n */\n crossOrigin?: \"\" | \"anonymous\" | \"use-credentials\" | undefined;\n};\n\n/**\n * Warms the browser cache for the **logical** next track (same rules as the Next control:\n * `computeNextIndex` from queue, repeat, and playback mode) using a detached `HTMLAudioElement`\n * with `preload=\"auto\"`. Safe to call alongside `Ginger.Player`; it does not replace main playback.\n *\n * The effect depends on the `tracks` array reference from context. If the parent recreates `tracks`\n * every render, prefetch will restart repeatedly; keep a stable queue reference (e.g. memoize) when possible.\n */\nexport function useNextTrackPrefetch(options: UseNextTrackPrefetchOptions = {}): void {\n const { enabled = true, crossOrigin } = options;\n const { tracks, currentIndex, repeatMode, playbackMode } = useGingerPlayback();\n\n useEffect(() => {\n if (!enabled || typeof document === \"undefined\") return;\n const nextIndex = computeNextIndex({ tracks, currentIndex, repeatMode, playbackMode });\n if (nextIndex === currentIndex) return;\n const nextUrl = tracks[nextIndex]?.fileUrl ?? \"\";\n if (!nextUrl) return;\n\n const audio = document.createElement(\"audio\");\n audio.preload = \"auto\";\n if (crossOrigin) audio.crossOrigin = crossOrigin;\n audio.src = nextUrl;\n audio.load();\n\n return () => {\n audio.removeAttribute(\"src\");\n audio.load();\n };\n }, [enabled, crossOrigin, tracks, currentIndex, repeatMode, playbackMode]);\n}\n","import {\n clampPlaybackRate,\n clampVolume,\n createInitialState,\n gingerReducer,\n} from \"./core/playbackReducer\";\nimport type {\n GingerAction,\n GingerInitPayload,\n GingerState,\n PlaylistMeta,\n RepeatMode,\n Track,\n} from \"./types\";\n\nexport type GingerStoreOptions = {\n tracks?: Track[];\n currentIndex?: number;\n playlistMeta?: PlaylistMeta | null;\n isPaused?: boolean;\n isShuffled?: boolean;\n repeatMode?: RepeatMode;\n playbackMode?: GingerState[\"playbackMode\"];\n volume?: number;\n muted?: boolean;\n playbackRate?: number;\n};\n\nexport type GingerStore = {\n /** Returns the current state snapshot. */\n getState: () => GingerState;\n /** Dispatch an action to update state. Synchronously updates state and notifies listeners. */\n dispatch: (action: GingerAction) => void;\n /**\n * Subscribe to state changes. The listener is called after every `dispatch` that produces\n * a new state object. Returns an unsubscribe function.\n */\n subscribe: (listener: (state: GingerState) => void) => () => void;\n /** Convenience: re-initialise with a new set of init options (equivalent to `dispatch({ type: \"INIT\", ... })`). */\n init: (payload: GingerInitPayload) => void;\n /** Clamp helpers re-exported for convenience. */\n clampVolume: typeof clampVolume;\n clampPlaybackRate: typeof clampPlaybackRate;\n};\n\n/**\n * Framework-agnostic store wrapping `gingerReducer`.\n * Usable outside React — e.g. in Svelte, Vue, Node.js testing, or server-side rendering contexts.\n *\n * @example\n * ```ts\n * import { createGingerStore } from \"@lucaismyname/ginger\";\n *\n * const store = createGingerStore({ tracks: myTracks });\n * const unsub = store.subscribe((state) => console.log(state.currentIndex));\n * store.dispatch({ type: \"NEXT\" });\n * unsub();\n * ```\n */\nexport function createGingerStore(options: GingerStoreOptions = {}): GingerStore {\n let state = createInitialState({\n tracks: options.tracks ?? [],\n currentIndex: options.currentIndex,\n playlistMeta: options.playlistMeta,\n isPaused: options.isPaused,\n isShuffled: options.isShuffled,\n repeatMode: options.repeatMode,\n playbackMode: options.playbackMode,\n volume: options.volume,\n muted: options.muted,\n playbackRate: options.playbackRate,\n });\n\n const listeners = new Set<(state: GingerState) => void>();\n\n const dispatch = (action: GingerAction): void => {\n const next = gingerReducer(state, action);\n if (next !== state) {\n state = next;\n for (const listener of listeners) {\n listener(state);\n }\n }\n };\n\n const subscribe = (listener: (state: GingerState) => void): (() => void) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n };\n\n const init = (payload: GingerInitPayload): void => {\n dispatch({ type: \"INIT\", payload });\n };\n\n return {\n getState: () => state,\n dispatch,\n subscribe,\n init,\n clampVolume,\n clampPlaybackRate,\n };\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useGingerPlayback } from \"../context/GingerSplitContexts\";\nimport type { Track } from \"../types\";\n\nexport type GingerPlaybackHistoryEntry = {\n track: Track;\n /** The track's index in the queue at the time it was played. */\n index: number;\n /** Unix timestamp (ms) when the track started playing. */\n playedAt: number;\n};\n\nexport type UseGingerPlaybackHistoryOptions = {\n /** Maximum number of entries to keep. Oldest entries are dropped first. Default: 50. */\n maxLength?: number;\n};\n\nexport type UseGingerPlaybackHistoryResult = {\n /** Chronological list of played tracks; most recent entry is last. */\n history: GingerPlaybackHistoryEntry[];\n clearHistory: () => void;\n};\n\n/**\n * Records a history of played tracks in chronological order.\n * Useful for \"what was playing before\" in shuffle mode or for analytics.\n *\n * The history is stored in component state and does not survive remounts.\n * In shuffle mode, `index` reflects the position in the current shuffled queue.\n */\nexport function useGingerPlaybackHistory(\n options: UseGingerPlaybackHistoryOptions = {},\n): UseGingerPlaybackHistoryResult {\n const { maxLength = 50 } = options;\n const { tracks, currentIndex } = useGingerPlayback();\n\n const [history, setHistory] = useState<GingerPlaybackHistoryEntry[]>([]);\n const prevIndexRef = useRef<number | null>(null);\n const prevTracksRef = useRef(tracks);\n prevTracksRef.current = tracks;\n\n useEffect(() => {\n const track = tracks[currentIndex];\n if (!track) return;\n\n if (prevIndexRef.current === currentIndex) return;\n prevIndexRef.current = currentIndex;\n\n const entry: GingerPlaybackHistoryEntry = {\n track,\n index: currentIndex,\n playedAt: Date.now(),\n };\n\n setHistory((prev) => {\n const next = [...prev, entry];\n return next.length > maxLength ? next.slice(next.length - maxLength) : next;\n });\n }, [currentIndex, tracks, maxLength]);\n\n const clearHistory = useCallback(() => setHistory([]), []);\n\n return { history, clearHistory };\n}\n","import { useCallback, useRef, useState } from \"react\";\nimport { useGingerMedia } from \"../context/GingerSplitContexts\";\n\nexport type UseGingerVolumeFadeOptions = {\n /** Target volume to fade to (0–1). */\n targetVolume: number;\n /** Duration of the fade in milliseconds. */\n durationMs: number;\n /** Called when the fade completes normally (not when cancelled). */\n onComplete?: () => void;\n};\n\nexport type UseGingerVolumeFadeResult = {\n /** Start a volume fade. Cancels any in-progress fade. */\n fadeVolumeTo: (options: UseGingerVolumeFadeOptions) => void;\n /** Cancel the current fade and hold at the current volume. */\n cancelFade: () => void;\n /** True while a fade is in progress. */\n isFading: boolean;\n};\n\n/**\n * Smoothly interpolates volume over a given duration using `requestAnimationFrame`.\n * Useful for fade-in on track start, fade-out before sleep timer fires, or crossfade prep.\n */\nexport function useGingerVolumeFade(): UseGingerVolumeFadeResult {\n const { setVolume, volume } = useGingerMedia();\n const [isFading, setIsFading] = useState(false);\n\n const rafRef = useRef<number>(0);\n const cancelledRef = useRef(false);\n\n const cancelFade = useCallback(() => {\n cancelAnimationFrame(rafRef.current);\n cancelledRef.current = true;\n setIsFading(false);\n }, []);\n\n const fadeVolumeTo = useCallback(\n ({ targetVolume, durationMs, onComplete }: UseGingerVolumeFadeOptions) => {\n cancelAnimationFrame(rafRef.current);\n cancelledRef.current = false;\n\n const clamp = (v: number) => Math.min(1, Math.max(0, v));\n const target = clamp(targetVolume);\n const startTime = performance.now();\n\n // Capture start volume at the moment the fade begins\n let startVolume = volume;\n\n setIsFading(true);\n\n const tick = (now: number) => {\n if (cancelledRef.current) return;\n const elapsed = now - startTime;\n const progress = Math.min(1, elapsed / Math.max(1, durationMs));\n const current = startVolume + (target - startVolume) * progress;\n setVolume(clamp(current));\n\n if (progress < 1) {\n rafRef.current = requestAnimationFrame(tick);\n } else {\n setIsFading(false);\n onComplete?.();\n }\n };\n\n // Use a small delay so `volume` from closure is the current value\n rafRef.current = requestAnimationFrame((now) => {\n startVolume = volume;\n tick(now);\n });\n },\n [setVolume, volume],\n );\n\n return { fadeVolumeTo, cancelFade, isFading };\n}\n","import { useMemo } from \"react\";\nimport { useGingerMedia, useGingerPlayback } from \"../context/GingerSplitContexts\";\n\nexport type GingerChapterProgress = {\n /** Fraction (0–1) of the way through the active chapter. 0 when no chapter is active. */\n progress: number;\n /** Seconds elapsed since the start of the active chapter. */\n elapsed: number;\n /** Seconds remaining until the end of the active chapter (or until end of track if last chapter). */\n remaining: number;\n};\n\n/**\n * Returns detailed progress information for the currently active chapter.\n * Complements `useGingerChapters` with per-chapter playback fractions.\n */\nexport function useGingerChapterProgress(): GingerChapterProgress {\n const { tracks, currentIndex } = useGingerPlayback();\n const { currentTime, duration } = useGingerMedia();\n\n const chapters = useMemo(() => {\n const raw = tracks[currentIndex]?.chapters ?? [];\n return [...raw]\n .filter((c) => c && Number.isFinite(c.startSeconds) && c.startSeconds >= 0)\n .sort((a, b) => a.startSeconds - b.startSeconds);\n }, [tracks, currentIndex]);\n\n return useMemo<GingerChapterProgress>(() => {\n if (chapters.length === 0) return { progress: 0, elapsed: 0, remaining: 0 };\n\n // Find active chapter (last one whose start is <= currentTime)\n let activeIdx = -1;\n for (let i = chapters.length - 1; i >= 0; i--) {\n if (currentTime >= chapters[i]!.startSeconds) {\n activeIdx = i;\n break;\n }\n }\n\n if (activeIdx === -1) return { progress: 0, elapsed: 0, remaining: 0 };\n\n const chapter = chapters[activeIdx]!;\n const nextChapter = chapters[activeIdx + 1];\n const chapterEnd = nextChapter?.startSeconds ?? (duration > 0 ? duration : currentTime);\n const chapterDuration = Math.max(0, chapterEnd - chapter.startSeconds);\n const elapsed = Math.max(0, currentTime - chapter.startSeconds);\n const remaining = Math.max(0, chapterEnd - currentTime);\n const progress = chapterDuration > 0 ? Math.min(1, elapsed / chapterDuration) : 0;\n\n return { progress, elapsed, remaining };\n }, [chapters, currentTime, duration]);\n}\n"],"names":["emptyFreq","emptyTime","useGingerLiveAnalyzer","options","enabled","fftSize","smoothingTimeConstant","minDecibels","maxDecibels","audioRef","state","useGinger","opts","useMemo","frame","setFrame","useState","error","setError","isSuspended","setIsSuspended","meta","setMeta","freqRef","useRef","timeRef","resume","useCallback","ctx","contextHolderRef","analyserHolderRef","useLayoutEffect","cancelled","consumerId","element","rafId","onStateChange","runLoop","a","fq","td","n","attach","el","id","context","analyser","attachLiveAnalyser","fft","e","msg","first","retryRaf","maxAttempts","attempts","retryLoop","out","detachLiveAnalyser","_a","useGingerKeyboardShortcuts","bindings","togglePlayPause","next","prev","useGingerPlayback","toggleMute","seek","currentTime","duration","useGingerMedia","muteBinding","seekForwardBinding","seekBackwardBinding","useEffect","playPause","nextKey","prevKey","muteKey","seekFwdKey","seekBwdKey","seekSecs","onKeyDown","event","target","key","dur","useGingerSleepTimer","durationMs","stopAfterTracks","respectPause","onFire","currentIndex","pause","isPaused","remainingTracksRef","prevIndexRef","remainingMsRef","segmentStartRef","prevDurationMsRef","elapsed","useGingerDebugLog","useGingerState","prevRef","clamp01","value","useSeekDrag","media","playback","fraction","setFraction","isDragging","setIsDragging","liveFraction","progressFraction","gingerStateFromContextValues","displayFraction","onPointerDown","rect","update","clientX","ratio","onMove","moveEvent","onUp","upEvent","useNextTrackPrefetch","crossOrigin","tracks","repeatMode","playbackMode","nextIndex","computeNextIndex","nextUrl","audio","createGingerStore","createInitialState","listeners","dispatch","action","gingerReducer","listener","payload","clampVolume","clampPlaybackRate","useGingerPlaybackHistory","maxLength","history","setHistory","prevTracksRef","track","entry","clearHistory","useGingerVolumeFade","setVolume","volume","isFading","setIsFading","rafRef","cancelledRef","cancelFade","targetVolume","onComplete","clamp","v","startTime","startVolume","tick","now","progress","current","useGingerChapterProgress","chapters","c","b","activeIdx","i","chapter","nextChapter","chapterEnd","chapterDuration","remaining"],"mappings":"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"}
1
+ {"version":3,"file":"useGingerChapterProgress-D2pdmyjg.cjs","sources":["../src/analyzer/useGingerLiveAnalyzer.ts","../src/hooks/useGingerKeyboardShortcuts.ts","../src/hooks/useGingerSleepTimer.ts","../src/hooks/useGingerDebugLog.ts","../src/hooks/useSeekDrag.ts","../src/hooks/useNextTrackPrefetch.ts","../src/store.ts","../src/hooks/useGingerPlaybackHistory.ts","../src/hooks/useGingerVolumeFade.ts","../src/hooks/useGingerChapterProgress.ts"],"sourcesContent":["import { useCallback, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport { useGinger } from \"../hooks/useGinger\";\nimport { type LiveAnalyserOptions, attachLiveAnalyser, detachLiveAnalyser } from \"./liveAudioGraph\";\n\nexport type UseGingerLiveAnalyzerOptions = {\n /** When false, the analyser is detached and no frames are read. Default true. */\n enabled?: boolean;\n fftSize?: number;\n smoothingTimeConstant?: number;\n minDecibels?: number;\n maxDecibels?: number;\n};\n\nexport type UseGingerLiveAnalyzerResult = {\n /** Byte frequency data (0–255); length equals `frequencyBinCount`. Updated each animation frame while enabled. */\n frequencyData: Uint8Array;\n /** Byte time-domain data (0–255); length equals `fftSize`. */\n timeDomainData: Uint8Array;\n frequencyBinCount: number;\n sampleRate: number;\n isSuspended: boolean;\n error: string | null;\n resume: () => Promise<void>;\n /**\n * Monotonically increasing counter incremented each animation frame.\n * Use as a `useMemo` / `useEffect` dependency to respond to new audio data, since\n * `frequencyData` and `timeDomainData` are mutated in-place (their reference never changes).\n */\n frame: number;\n};\n\nconst emptyFreq = new Uint8Array(0);\nconst emptyTime = new Uint8Array(0);\n\nexport function useGingerLiveAnalyzer(\n options: UseGingerLiveAnalyzerOptions = {},\n): UseGingerLiveAnalyzerResult {\n const {\n enabled = true,\n fftSize = 2048,\n smoothingTimeConstant = 0.8,\n minDecibels = -100,\n maxDecibels = -30,\n } = options;\n\n const { audioRef, state } = useGinger();\n const opts = useMemo<LiveAnalyserOptions>(\n () => ({\n fftSize,\n smoothingTimeConstant,\n minDecibels,\n maxDecibels,\n }),\n [fftSize, smoothingTimeConstant, minDecibels, maxDecibels],\n );\n\n const [frame, setFrame] = useState(0);\n const [error, setError] = useState<string | null>(null);\n const [isSuspended, setIsSuspended] = useState(false);\n const [meta, setMeta] = useState({ frequencyBinCount: 0, sampleRate: 0 });\n\n const freqRef = useRef<Uint8Array>(emptyFreq);\n const timeRef = useRef<Uint8Array>(emptyTime);\n\n const resume = useCallback(async () => {\n const ctx = contextHolderRef.current;\n if (ctx && ctx.state === \"suspended\") {\n await ctx.resume();\n }\n }, []);\n\n const contextHolderRef = useRef<AudioContext | null>(null);\n const analyserHolderRef = useRef<AnalyserNode | null>(null);\n\n useLayoutEffect(() => {\n if (!enabled || typeof window === \"undefined\") {\n return;\n }\n\n let cancelled = false;\n let consumerId: number | null = null;\n let element: HTMLAudioElement | null = null;\n let rafId = 0;\n\n const onStateChange = () => {\n const ctx = contextHolderRef.current;\n if (ctx) setIsSuspended(ctx.state === \"suspended\");\n };\n\n const runLoop = () => {\n if (cancelled) return;\n const a = analyserHolderRef.current;\n const fq = freqRef.current;\n const td = timeRef.current;\n if (a && fq.length > 0 && td.length > 0) {\n a.getByteFrequencyData(fq as Uint8Array<ArrayBuffer>);\n a.getByteTimeDomainData(td as Uint8Array<ArrayBuffer>);\n setFrame((n) => n + 1);\n }\n rafId = requestAnimationFrame(runLoop);\n };\n\n type AttachOutcome = \"ok\" | \"no-element\" | \"error\";\n\n const attach = (): AttachOutcome => {\n const el = audioRef.current;\n if (!el || cancelled) return \"no-element\";\n try {\n const { id, context, analyser } = attachLiveAnalyser(el, opts);\n consumerId = id;\n element = el;\n contextHolderRef.current = context;\n analyserHolderRef.current = analyser;\n setIsSuspended(context.state === \"suspended\");\n setError(null);\n\n context.addEventListener(\"statechange\", onStateChange);\n\n const n = analyser.frequencyBinCount;\n const fft = analyser.fftSize;\n freqRef.current = new Uint8Array(n);\n timeRef.current = new Uint8Array(fft);\n setMeta({ frequencyBinCount: n, sampleRate: context.sampleRate });\n\n rafId = requestAnimationFrame(runLoop);\n return \"ok\";\n } catch (e) {\n const msg = e instanceof Error ? e.message : \"Failed to attach live analyser\";\n setError(msg);\n contextHolderRef.current = null;\n analyserHolderRef.current = null;\n freqRef.current = emptyFreq;\n timeRef.current = emptyTime;\n setMeta({ frequencyBinCount: 0, sampleRate: 0 });\n return \"error\";\n }\n };\n\n const first = attach();\n if (first !== \"ok\") {\n let retryRaf = 0;\n const maxAttempts = 120;\n let attempts = 0;\n\n const retryLoop = () => {\n if (cancelled) return;\n const out = attach();\n if (out === \"ok\" || out === \"error\") return;\n attempts += 1;\n if (attempts >= maxAttempts) return;\n retryRaf = requestAnimationFrame(retryLoop);\n };\n\n if (first === \"no-element\") {\n retryRaf = requestAnimationFrame(retryLoop);\n }\n\n return () => {\n cancelled = true;\n cancelAnimationFrame(retryRaf);\n cancelAnimationFrame(rafId);\n if (consumerId != null && element) {\n detachLiveAnalyser(element, consumerId);\n }\n contextHolderRef.current?.removeEventListener(\"statechange\", onStateChange);\n contextHolderRef.current = null;\n analyserHolderRef.current = null;\n freqRef.current = emptyFreq;\n timeRef.current = emptyTime;\n };\n }\n\n return () => {\n cancelled = true;\n cancelAnimationFrame(rafId);\n if (consumerId != null && element) {\n detachLiveAnalyser(element, consumerId);\n }\n contextHolderRef.current?.removeEventListener(\"statechange\", onStateChange);\n contextHolderRef.current = null;\n analyserHolderRef.current = null;\n freqRef.current = emptyFreq;\n timeRef.current = emptyTime;\n setMeta({ frequencyBinCount: 0, sampleRate: 0 });\n };\n }, [enabled, audioRef, opts, state.currentIndex]);\n\n return {\n frequencyData: freqRef.current,\n timeDomainData: timeRef.current,\n frequencyBinCount: meta.frequencyBinCount,\n sampleRate: meta.sampleRate,\n isSuspended,\n error,\n resume,\n frame,\n };\n}\n","import { useEffect } from \"react\";\nimport { useGingerMedia, useGingerPlayback } from \"../context/GingerSplitContexts\";\n\nexport type GingerKeyboardShortcutBindings = {\n playPause?: string;\n next?: string;\n previous?: string;\n mute?: string;\n /** Key to seek forward by `seekSeconds`. Default: undefined (disabled). */\n seekForward?: string;\n /** Key to seek backward by `seekSeconds`. Default: undefined (disabled). */\n seekBackward?: string;\n /** Seconds to seek per `seekForward` / `seekBackward` keypress. Default: 5. */\n seekSeconds?: number;\n};\n\nexport function useGingerKeyboardShortcuts(\n enabled = true,\n bindings: GingerKeyboardShortcutBindings = {},\n): void {\n const { togglePlayPause, next, prev } = useGingerPlayback();\n const { toggleMute, seek, currentTime, duration } = useGingerMedia();\n\n const {\n mute: muteBinding,\n seekForward: seekForwardBinding,\n seekBackward: seekBackwardBinding,\n } = bindings;\n\n useEffect(() => {\n if (!enabled || typeof window === \"undefined\") return;\n const playPause = (bindings.playPause ?? \" \").toLowerCase();\n const nextKey = (bindings.next ?? \"ArrowRight\").toLowerCase();\n const prevKey = (bindings.previous ?? \"ArrowLeft\").toLowerCase();\n const muteKey = muteBinding?.toLowerCase();\n const seekFwdKey = seekForwardBinding?.toLowerCase();\n const seekBwdKey = seekBackwardBinding?.toLowerCase();\n const seekSecs = bindings.seekSeconds ?? 5;\n\n const onKeyDown = (event: KeyboardEvent) => {\n const target = event.target as HTMLElement | null;\n if (\n target &&\n ([\"INPUT\", \"TEXTAREA\", \"SELECT\"].includes(target.tagName) || target.isContentEditable)\n )\n return;\n const key = event.key.toLowerCase();\n if (key === playPause) {\n event.preventDefault();\n togglePlayPause();\n } else if (key === nextKey) {\n event.preventDefault();\n next();\n } else if (key === prevKey) {\n event.preventDefault();\n prev();\n } else if (muteKey && key === muteKey) {\n event.preventDefault();\n toggleMute();\n } else if (seekFwdKey && key === seekFwdKey) {\n event.preventDefault();\n const dur = duration > 0 ? duration : Number.POSITIVE_INFINITY;\n seek(Math.min(dur, currentTime + seekSecs));\n } else if (seekBwdKey && key === seekBwdKey) {\n event.preventDefault();\n seek(Math.max(0, currentTime - seekSecs));\n }\n };\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [\n bindings.next,\n bindings.playPause,\n bindings.previous,\n bindings.seekSeconds,\n currentTime,\n duration,\n enabled,\n muteBinding,\n next,\n prev,\n seek,\n seekBackwardBinding,\n seekForwardBinding,\n toggleMute,\n togglePlayPause,\n ]);\n}\n","import { useEffect, useRef } from \"react\";\nimport { useGingerPlayback } from \"../context/GingerSplitContexts\";\n\nexport type GingerSleepTimerOptions = {\n durationMs?: number;\n stopAfterTracks?: number;\n respectPause?: boolean;\n enabled?: boolean;\n onFire?: () => void;\n};\n\nexport function useGingerSleepTimer(options: GingerSleepTimerOptions): void {\n const { durationMs, stopAfterTracks, respectPause = true, enabled = true, onFire } = options;\n const { currentIndex, pause, isPaused } = useGingerPlayback();\n const remainingTracksRef = useRef(stopAfterTracks ?? 0);\n const prevIndexRef = useRef(currentIndex);\n\n // Remaining milliseconds for the duration-based timer; carried across pause/resume cycles.\n const remainingMsRef = useRef(durationMs ?? 0);\n // Timestamp when the current timer segment started (set when playback resumes).\n const segmentStartRef = useRef<number | null>(null);\n\n useEffect(() => {\n remainingTracksRef.current = stopAfterTracks ?? 0;\n }, [stopAfterTracks]);\n\n // Keep remainingMsRef in sync when durationMs changes while the timer is inactive.\n const prevDurationMsRef = useRef(durationMs);\n useEffect(() => {\n if (prevDurationMsRef.current !== durationMs) {\n remainingMsRef.current = durationMs ?? 0;\n prevDurationMsRef.current = durationMs;\n }\n }, [durationMs]);\n\n useEffect(() => {\n if (!enabled || !durationMs || durationMs <= 0) {\n // Reset remaining when disabled or no duration\n remainingMsRef.current = durationMs ?? 0;\n segmentStartRef.current = null;\n return;\n }\n\n if (respectPause && isPaused) {\n // Snapshot how much time is left before pausing\n if (segmentStartRef.current !== null) {\n const elapsed = Date.now() - segmentStartRef.current;\n remainingMsRef.current = Math.max(0, remainingMsRef.current - elapsed);\n segmentStartRef.current = null;\n }\n return;\n }\n\n // Playing: start (or continue) the countdown from remainingMsRef\n segmentStartRef.current = Date.now();\n const id = setTimeout(() => {\n remainingMsRef.current = 0;\n segmentStartRef.current = null;\n pause();\n onFire?.();\n }, remainingMsRef.current);\n\n return () => {\n clearTimeout(id);\n // Snapshot remaining when effect cleans up (e.g. isPaused or deps changed)\n if (segmentStartRef.current !== null) {\n const elapsed = Date.now() - segmentStartRef.current;\n remainingMsRef.current = Math.max(0, remainingMsRef.current - elapsed);\n segmentStartRef.current = null;\n }\n };\n }, [durationMs, enabled, isPaused, onFire, pause, respectPause]);\n\n useEffect(() => {\n if (!enabled || !stopAfterTracks || stopAfterTracks <= 0) return;\n const prev = prevIndexRef.current;\n prevIndexRef.current = currentIndex;\n if (currentIndex === prev) return;\n remainingTracksRef.current -= 1;\n if (remainingTracksRef.current <= 0) {\n pause();\n onFire?.();\n }\n }, [currentIndex, enabled, onFire, pause, stopAfterTracks]);\n}\n","import { useEffect, useRef } from \"react\";\nimport { useGingerState } from \"../context/GingerSplitContexts\";\n\nexport function useGingerDebugLog(enabled = false): void {\n const state = useGingerState();\n const prevRef = useRef(state);\n\n useEffect(() => {\n if (!enabled || typeof console === \"undefined\") return;\n const prev = prevRef.current;\n if (prev !== state) {\n console.debug(\"[ginger]\", {\n from: {\n currentIndex: prev.currentIndex,\n isPaused: prev.isPaused,\n currentTime: prev.currentTime,\n repeatMode: prev.repeatMode,\n },\n to: {\n currentIndex: state.currentIndex,\n isPaused: state.isPaused,\n currentTime: state.currentTime,\n repeatMode: state.repeatMode,\n },\n });\n }\n prevRef.current = state;\n }, [enabled, state]);\n}\n","import { useCallback, useState } from \"react\";\nimport type { PointerEvent as ReactPointerEvent } from \"react\";\nimport {\n gingerStateFromContextValues,\n useGingerMedia,\n useGingerPlayback,\n} from \"../context/GingerSplitContexts\";\nimport { progressFraction } from \"../internal/selectors\";\n\nexport type SeekDragState = {\n /** Raw drag fraction — only updated during an active drag gesture. */\n fraction: number;\n /** Blended fraction: follows live playback when idle, drag position when dragging. */\n displayFraction: number;\n isDragging: boolean;\n onPointerDown: (event: ReactPointerEvent<HTMLElement>) => void;\n};\n\nfunction clamp01(value: number): number {\n return Math.max(0, Math.min(1, value));\n}\n\nexport function useSeekDrag(duration: number): SeekDragState {\n const media = useGingerMedia();\n const playback = useGingerPlayback();\n const { seek } = media;\n const [fraction, setFraction] = useState(0);\n const [isDragging, setIsDragging] = useState(false);\n\n const liveFraction = progressFraction(gingerStateFromContextValues(playback, media));\n const displayFraction = isDragging ? fraction : liveFraction;\n\n const onPointerDown = useCallback(\n (event: ReactPointerEvent<HTMLElement>) => {\n if (!(duration > 0)) return;\n const target = event.currentTarget;\n const rect = target.getBoundingClientRect();\n const update = (clientX: number) => {\n const ratio = clamp01((clientX - rect.left) / rect.width);\n setFraction(ratio);\n seek(ratio * duration);\n };\n setIsDragging(true);\n target.setPointerCapture(event.pointerId);\n update(event.clientX);\n const onMove = (moveEvent: PointerEvent) => update(moveEvent.clientX);\n const onUp = (upEvent: PointerEvent) => {\n update(upEvent.clientX);\n setIsDragging(false);\n target.releasePointerCapture(event.pointerId);\n target.removeEventListener(\"pointermove\", onMove);\n target.removeEventListener(\"pointerup\", onUp);\n target.removeEventListener(\"pointercancel\", onUp);\n };\n target.addEventListener(\"pointermove\", onMove);\n target.addEventListener(\"pointerup\", onUp);\n target.addEventListener(\"pointercancel\", onUp);\n },\n [duration, seek],\n );\n\n return { fraction, displayFraction, isDragging, onPointerDown };\n}\n","import { useEffect } from \"react\";\nimport { useGingerPlayback } from \"../context/GingerSplitContexts\";\nimport { computeNextIndex } from \"../core/transitions\";\n\nexport type UseNextTrackPrefetchOptions = {\n /** When false, no prefetch runs. Default true. */\n enabled?: boolean;\n /**\n * Match `crossOrigin` on `Ginger.Player` when `fileUrl` is cross-origin so the browser\n * can reuse cached media consistently.\n */\n crossOrigin?: \"\" | \"anonymous\" | \"use-credentials\" | undefined;\n};\n\n/**\n * Warms the browser cache for the **logical** next track (same rules as the Next control:\n * `computeNextIndex` from queue, repeat, and playback mode) using a detached `HTMLAudioElement`\n * with `preload=\"auto\"`. Safe to call alongside `Ginger.Player`; it does not replace main playback.\n *\n * The effect depends on the `tracks` array reference from context. If the parent recreates `tracks`\n * every render, prefetch will restart repeatedly; keep a stable queue reference (e.g. memoize) when possible.\n */\nexport function useNextTrackPrefetch(options: UseNextTrackPrefetchOptions = {}): void {\n const { enabled = true, crossOrigin } = options;\n const { tracks, currentIndex, repeatMode, playbackMode } = useGingerPlayback();\n\n useEffect(() => {\n if (!enabled || typeof document === \"undefined\") return;\n const nextIndex = computeNextIndex({ tracks, currentIndex, repeatMode, playbackMode });\n if (nextIndex === currentIndex) return;\n const nextUrl = tracks[nextIndex]?.fileUrl ?? \"\";\n if (!nextUrl) return;\n\n const audio = document.createElement(\"audio\");\n audio.preload = \"auto\";\n if (crossOrigin) audio.crossOrigin = crossOrigin;\n audio.src = nextUrl;\n audio.load();\n\n return () => {\n audio.removeAttribute(\"src\");\n audio.load();\n };\n }, [enabled, crossOrigin, tracks, currentIndex, repeatMode, playbackMode]);\n}\n","import {\n clampPlaybackRate,\n clampVolume,\n createInitialState,\n gingerReducer,\n} from \"./core/playbackReducer\";\nimport type {\n GingerAction,\n GingerInitPayload,\n GingerState,\n PlaylistMeta,\n RepeatMode,\n Track,\n} from \"./types\";\n\nexport type GingerStoreOptions = {\n tracks?: Track[];\n currentIndex?: number;\n playlistMeta?: PlaylistMeta | null;\n isPaused?: boolean;\n isShuffled?: boolean;\n repeatMode?: RepeatMode;\n playbackMode?: GingerState[\"playbackMode\"];\n volume?: number;\n muted?: boolean;\n playbackRate?: number;\n};\n\nexport type GingerStore = {\n /** Returns the current state snapshot. */\n getState: () => GingerState;\n /** Dispatch an action to update state. Synchronously updates state and notifies listeners. */\n dispatch: (action: GingerAction) => void;\n /**\n * Subscribe to state changes. The listener is called after every `dispatch` that produces\n * a new state object. Returns an unsubscribe function.\n */\n subscribe: (listener: (state: GingerState) => void) => () => void;\n /** Convenience: re-initialise with a new set of init options (equivalent to `dispatch({ type: \"INIT\", ... })`). */\n init: (payload: GingerInitPayload) => void;\n /** Clamp helpers re-exported for convenience. */\n clampVolume: typeof clampVolume;\n clampPlaybackRate: typeof clampPlaybackRate;\n};\n\n/**\n * Framework-agnostic store wrapping `gingerReducer`.\n * Usable outside React — e.g. in Svelte, Vue, Node.js testing, or server-side rendering contexts.\n *\n * @example\n * ```ts\n * import { createGingerStore } from \"@lucaismyname/ginger\";\n *\n * const store = createGingerStore({ tracks: myTracks });\n * const unsub = store.subscribe((state) => console.log(state.currentIndex));\n * store.dispatch({ type: \"NEXT\" });\n * unsub();\n * ```\n */\nexport function createGingerStore(options: GingerStoreOptions = {}): GingerStore {\n let state = createInitialState({\n tracks: options.tracks ?? [],\n currentIndex: options.currentIndex,\n playlistMeta: options.playlistMeta,\n isPaused: options.isPaused,\n isShuffled: options.isShuffled,\n repeatMode: options.repeatMode,\n playbackMode: options.playbackMode,\n volume: options.volume,\n muted: options.muted,\n playbackRate: options.playbackRate,\n });\n\n const listeners = new Set<(state: GingerState) => void>();\n\n const dispatch = (action: GingerAction): void => {\n const next = gingerReducer(state, action);\n if (next !== state) {\n state = next;\n for (const listener of listeners) {\n listener(state);\n }\n }\n };\n\n const subscribe = (listener: (state: GingerState) => void): (() => void) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n };\n\n const init = (payload: GingerInitPayload): void => {\n dispatch({ type: \"INIT\", payload });\n };\n\n return {\n getState: () => state,\n dispatch,\n subscribe,\n init,\n clampVolume,\n clampPlaybackRate,\n };\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useGingerPlayback } from \"../context/GingerSplitContexts\";\nimport type { Track } from \"../types\";\n\nexport type GingerPlaybackHistoryEntry = {\n track: Track;\n /** The track's index in the queue at the time it was played. */\n index: number;\n /** Unix timestamp (ms) when the track started playing. */\n playedAt: number;\n};\n\nexport type UseGingerPlaybackHistoryOptions = {\n /** Maximum number of entries to keep. Oldest entries are dropped first. Default: 50. */\n maxLength?: number;\n};\n\nexport type UseGingerPlaybackHistoryResult = {\n /** Chronological list of played tracks; most recent entry is last. */\n history: GingerPlaybackHistoryEntry[];\n clearHistory: () => void;\n};\n\n/**\n * Records a history of played tracks in chronological order.\n * Useful for \"what was playing before\" in shuffle mode or for analytics.\n *\n * The history is stored in component state and does not survive remounts.\n * In shuffle mode, `index` reflects the position in the current shuffled queue.\n */\nexport function useGingerPlaybackHistory(\n options: UseGingerPlaybackHistoryOptions = {},\n): UseGingerPlaybackHistoryResult {\n const { maxLength = 50 } = options;\n const { tracks, currentIndex } = useGingerPlayback();\n\n const [history, setHistory] = useState<GingerPlaybackHistoryEntry[]>([]);\n const prevIndexRef = useRef<number | null>(null);\n const prevTracksRef = useRef(tracks);\n prevTracksRef.current = tracks;\n\n useEffect(() => {\n const track = tracks[currentIndex];\n if (!track) return;\n\n if (prevIndexRef.current === currentIndex) return;\n prevIndexRef.current = currentIndex;\n\n const entry: GingerPlaybackHistoryEntry = {\n track,\n index: currentIndex,\n playedAt: Date.now(),\n };\n\n setHistory((prev) => {\n const next = [...prev, entry];\n return next.length > maxLength ? next.slice(next.length - maxLength) : next;\n });\n }, [currentIndex, tracks, maxLength]);\n\n const clearHistory = useCallback(() => setHistory([]), []);\n\n return { history, clearHistory };\n}\n","import { useCallback, useRef, useState } from \"react\";\nimport { useGingerMedia } from \"../context/GingerSplitContexts\";\n\nexport type UseGingerVolumeFadeOptions = {\n /** Target volume to fade to (0–1). */\n targetVolume: number;\n /** Duration of the fade in milliseconds. */\n durationMs: number;\n /** Called when the fade completes normally (not when cancelled). */\n onComplete?: () => void;\n};\n\nexport type UseGingerVolumeFadeResult = {\n /** Start a volume fade. Cancels any in-progress fade. */\n fadeVolumeTo: (options: UseGingerVolumeFadeOptions) => void;\n /** Cancel the current fade and hold at the current volume. */\n cancelFade: () => void;\n /** True while a fade is in progress. */\n isFading: boolean;\n};\n\n/**\n * Smoothly interpolates volume over a given duration using `requestAnimationFrame`.\n * Useful for fade-in on track start, fade-out before sleep timer fires, or crossfade prep.\n */\nexport function useGingerVolumeFade(): UseGingerVolumeFadeResult {\n const { setVolume, volume } = useGingerMedia();\n const [isFading, setIsFading] = useState(false);\n\n const rafRef = useRef<number>(0);\n const cancelledRef = useRef(false);\n\n const cancelFade = useCallback(() => {\n cancelAnimationFrame(rafRef.current);\n cancelledRef.current = true;\n setIsFading(false);\n }, []);\n\n const fadeVolumeTo = useCallback(\n ({ targetVolume, durationMs, onComplete }: UseGingerVolumeFadeOptions) => {\n cancelAnimationFrame(rafRef.current);\n cancelledRef.current = false;\n\n const clamp = (v: number) => Math.min(1, Math.max(0, v));\n const target = clamp(targetVolume);\n const startTime = performance.now();\n\n // Capture start volume at the moment the fade begins\n let startVolume = volume;\n\n setIsFading(true);\n\n const tick = (now: number) => {\n if (cancelledRef.current) return;\n const elapsed = now - startTime;\n const progress = Math.min(1, elapsed / Math.max(1, durationMs));\n const current = startVolume + (target - startVolume) * progress;\n setVolume(clamp(current));\n\n if (progress < 1) {\n rafRef.current = requestAnimationFrame(tick);\n } else {\n setIsFading(false);\n onComplete?.();\n }\n };\n\n // Use a small delay so `volume` from closure is the current value\n rafRef.current = requestAnimationFrame((now) => {\n startVolume = volume;\n tick(now);\n });\n },\n [setVolume, volume],\n );\n\n return { fadeVolumeTo, cancelFade, isFading };\n}\n","import { useMemo } from \"react\";\nimport { useGingerMedia, useGingerPlayback } from \"../context/GingerSplitContexts\";\n\nexport type GingerChapterProgress = {\n /** Fraction (0–1) of the way through the active chapter. 0 when no chapter is active. */\n progress: number;\n /** Seconds elapsed since the start of the active chapter. */\n elapsed: number;\n /** Seconds remaining until the end of the active chapter (or until end of track if last chapter). */\n remaining: number;\n};\n\n/**\n * Returns detailed progress information for the currently active chapter.\n * Complements `useGingerChapters` with per-chapter playback fractions.\n */\nexport function useGingerChapterProgress(): GingerChapterProgress {\n const { tracks, currentIndex } = useGingerPlayback();\n const { currentTime, duration } = useGingerMedia();\n\n const chapters = useMemo(() => {\n const raw = tracks[currentIndex]?.chapters ?? [];\n return [...raw]\n .filter((c) => c && Number.isFinite(c.startSeconds) && c.startSeconds >= 0)\n .sort((a, b) => a.startSeconds - b.startSeconds);\n }, [tracks, currentIndex]);\n\n return useMemo<GingerChapterProgress>(() => {\n if (chapters.length === 0) return { progress: 0, elapsed: 0, remaining: 0 };\n\n // Find active chapter (last one whose start is <= currentTime)\n let activeIdx = -1;\n for (let i = chapters.length - 1; i >= 0; i--) {\n if (currentTime >= chapters[i]!.startSeconds) {\n activeIdx = i;\n break;\n }\n }\n\n if (activeIdx === -1) return { progress: 0, elapsed: 0, remaining: 0 };\n\n const chapter = chapters[activeIdx]!;\n const nextChapter = chapters[activeIdx + 1];\n const chapterEnd = nextChapter?.startSeconds ?? (duration > 0 ? duration : currentTime);\n const chapterDuration = Math.max(0, chapterEnd - chapter.startSeconds);\n const elapsed = Math.max(0, currentTime - chapter.startSeconds);\n const remaining = Math.max(0, chapterEnd - currentTime);\n const progress = chapterDuration > 0 ? Math.min(1, elapsed / chapterDuration) : 0;\n\n return { progress, elapsed, remaining };\n }, [chapters, currentTime, duration]);\n}\n"],"names":["emptyFreq","emptyTime","useGingerLiveAnalyzer","options","enabled","fftSize","smoothingTimeConstant","minDecibels","maxDecibels","audioRef","state","useGinger","opts","useMemo","frame","setFrame","useState","error","setError","isSuspended","setIsSuspended","meta","setMeta","freqRef","useRef","timeRef","resume","useCallback","ctx","contextHolderRef","analyserHolderRef","useLayoutEffect","cancelled","consumerId","element","rafId","onStateChange","runLoop","a","fq","td","n","attach","el","id","context","analyser","attachLiveAnalyser","fft","e","msg","first","retryRaf","maxAttempts","attempts","retryLoop","out","detachLiveAnalyser","_a","useGingerKeyboardShortcuts","bindings","togglePlayPause","next","prev","useGingerPlayback","toggleMute","seek","currentTime","duration","useGingerMedia","muteBinding","seekForwardBinding","seekBackwardBinding","useEffect","playPause","nextKey","prevKey","muteKey","seekFwdKey","seekBwdKey","seekSecs","onKeyDown","event","target","key","dur","useGingerSleepTimer","durationMs","stopAfterTracks","respectPause","onFire","currentIndex","pause","isPaused","remainingTracksRef","prevIndexRef","remainingMsRef","segmentStartRef","prevDurationMsRef","elapsed","useGingerDebugLog","useGingerState","prevRef","clamp01","value","useSeekDrag","media","playback","fraction","setFraction","isDragging","setIsDragging","liveFraction","progressFraction","gingerStateFromContextValues","displayFraction","onPointerDown","rect","update","clientX","ratio","onMove","moveEvent","onUp","upEvent","useNextTrackPrefetch","crossOrigin","tracks","repeatMode","playbackMode","nextIndex","computeNextIndex","nextUrl","audio","createGingerStore","createInitialState","listeners","dispatch","action","gingerReducer","listener","payload","clampVolume","clampPlaybackRate","useGingerPlaybackHistory","maxLength","history","setHistory","prevTracksRef","track","entry","clearHistory","useGingerVolumeFade","setVolume","volume","isFading","setIsFading","rafRef","cancelledRef","cancelFade","targetVolume","onComplete","clamp","v","startTime","startVolume","tick","now","progress","current","useGingerChapterProgress","chapters","c","b","activeIdx","i","chapter","nextChapter","chapterEnd","chapterDuration","remaining"],"mappings":"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"}
@@ -1,11 +1,12 @@
1
1
  import { useMemo as V, useState as A, useRef as M, useCallback as C, useLayoutEffect as Y, useEffect as D } from "react";
2
- import { u as _ } from "./useGinger-hpp2pAGY.js";
2
+ import { u as _ } from "./useGinger-Dz0cPyD1.js";
3
3
  import { a as j, d as z } from "./liveAudioGraph-DvPaxBCP.js";
4
4
  import { b as P, u as B, c as J, g as Q } from "./GingerSplitContexts-BzBExb95.js";
5
- import { p as W, c as Z } from "./selectors-BalBCc7X.js";
6
- import { m as $, e as ee, f as te, n as re } from "./ginger-Ca8910_n.js";
5
+ import { p as W } from "./selectors-BT3WSsKN.js";
6
+ import { b as Z } from "./transitions-CmNkf3sd.js";
7
+ import { m as $, e as ee, f as te, n as re } from "./ginger-B2DgE-2a.js";
7
8
  const G = new Uint8Array(0), N = new Uint8Array(0);
8
- function le(n = {}) {
9
+ function fe(n = {}) {
9
10
  const {
10
11
  enabled: e = !0,
11
12
  fftSize: t = 2048,
@@ -77,7 +78,7 @@ function le(n = {}) {
77
78
  frame: i
78
79
  };
79
80
  }
80
- function fe(n = !0, e = {}) {
81
+ function me(n = !0, e = {}) {
81
82
  const { togglePlayPause: t, next: r, prev: c } = P(), { toggleMute: a, seek: s, currentTime: o, duration: f } = B(), {
82
83
  mute: i,
83
84
  seekForward: m,
@@ -123,7 +124,7 @@ function fe(n = !0, e = {}) {
123
124
  t
124
125
  ]);
125
126
  }
126
- function me(n) {
127
+ function de(n) {
127
128
  const { durationMs: e, stopAfterTracks: t, respectPause: r = !0, enabled: c = !0, onFire: a } = n, { currentIndex: s, pause: o, isPaused: f } = P(), i = M(t ?? 0), m = M(s), l = M(e ?? 0), u = M(null);
128
129
  D(() => {
129
130
  i.current = t ?? 0;
@@ -159,7 +160,7 @@ function me(n) {
159
160
  m.current = s, s !== x && (i.current -= 1, i.current <= 0 && (o(), a == null || a()));
160
161
  }, [s, c, a, o, t]);
161
162
  }
162
- function de(n = !1) {
163
+ function pe(n = !1) {
163
164
  const e = J(), t = M(e);
164
165
  D(() => {
165
166
  if (!n || typeof console > "u") return;
@@ -183,7 +184,7 @@ function de(n = !1) {
183
184
  function ne(n) {
184
185
  return Math.max(0, Math.min(1, n));
185
186
  }
186
- function pe(n) {
187
+ function ge(n) {
187
188
  const e = B(), t = P(), { seek: r } = e, [c, a] = A(0), [s, o] = A(!1), f = W(Q(t, e)), i = s ? c : f, m = C(
188
189
  (l) => {
189
190
  if (!(n > 0)) return;
@@ -201,7 +202,7 @@ function pe(n) {
201
202
  );
202
203
  return { fraction: c, displayFraction: i, isDragging: s, onPointerDown: m };
203
204
  }
204
- function ge(n = {}) {
205
+ function ye(n = {}) {
205
206
  const { enabled: e = !0, crossOrigin: t } = n, { tracks: r, currentIndex: c, repeatMode: a, playbackMode: s } = P();
206
207
  D(() => {
207
208
  var m;
@@ -216,7 +217,7 @@ function ge(n = {}) {
216
217
  };
217
218
  }, [e, t, r, c, a, s]);
218
219
  }
219
- function ye(n = {}) {
220
+ function he(n = {}) {
220
221
  let e = $({
221
222
  tracks: n.tracks ?? [],
222
223
  currentIndex: n.currentIndex,
@@ -248,7 +249,7 @@ function ye(n = {}) {
248
249
  clampPlaybackRate: ee
249
250
  };
250
251
  }
251
- function he(n = {}) {
252
+ function xe(n = {}) {
252
253
  const { maxLength: e = 50 } = n, { tracks: t, currentIndex: r } = P(), [c, a] = A([]), s = M(null), o = M(t);
253
254
  o.current = t, D(() => {
254
255
  const i = t[r];
@@ -267,7 +268,7 @@ function he(n = {}) {
267
268
  const f = C(() => a([]), []);
268
269
  return { history: c, clearHistory: f };
269
270
  }
270
- function xe() {
271
+ function ke() {
271
272
  const { setVolume: n, volume: e } = B(), [t, r] = A(!1), c = M(0), a = M(!1), s = C(() => {
272
273
  cancelAnimationFrame(c.current), a.current = !0, r(!1);
273
274
  }, []);
@@ -289,7 +290,7 @@ function xe() {
289
290
  [n, e]
290
291
  ), cancelFade: s, isFading: t };
291
292
  }
292
- function ke() {
293
+ function ve() {
293
294
  const { tracks: n, currentIndex: e } = P(), { currentTime: t, duration: r } = B(), c = V(() => {
294
295
  var s;
295
296
  return [...((s = n[e]) == null ? void 0 : s.chapters) ?? []].filter((o) => o && Number.isFinite(o.startSeconds) && o.startSeconds >= 0).sort((o, f) => o.startSeconds - f.startSeconds);
@@ -308,15 +309,15 @@ function ke() {
308
309
  }, [c, t, r]);
309
310
  }
310
311
  export {
311
- de as a,
312
- fe as b,
313
- ye as c,
314
- le as d,
315
- he as e,
316
- me as f,
317
- xe as g,
318
- ge as h,
319
- pe as i,
320
- ke as u
312
+ pe as a,
313
+ me as b,
314
+ he as c,
315
+ fe as d,
316
+ xe as e,
317
+ de as f,
318
+ ke as g,
319
+ ye as h,
320
+ ge as i,
321
+ ve as u
321
322
  };
322
- //# sourceMappingURL=useGingerChapterProgress-Dbwiwnko.js.map
323
+ //# sourceMappingURL=useGingerChapterProgress-wxAmN_uo.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useGingerChapterProgress-Dbwiwnko.js","sources":["../src/analyzer/useGingerLiveAnalyzer.ts","../src/hooks/useGingerKeyboardShortcuts.ts","../src/hooks/useGingerSleepTimer.ts","../src/hooks/useGingerDebugLog.ts","../src/hooks/useSeekDrag.ts","../src/hooks/useNextTrackPrefetch.ts","../src/store.ts","../src/hooks/useGingerPlaybackHistory.ts","../src/hooks/useGingerVolumeFade.ts","../src/hooks/useGingerChapterProgress.ts"],"sourcesContent":["import { useCallback, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport { useGinger } from \"../hooks/useGinger\";\nimport { type LiveAnalyserOptions, attachLiveAnalyser, detachLiveAnalyser } from \"./liveAudioGraph\";\n\nexport type UseGingerLiveAnalyzerOptions = {\n /** When false, the analyser is detached and no frames are read. Default true. */\n enabled?: boolean;\n fftSize?: number;\n smoothingTimeConstant?: number;\n minDecibels?: number;\n maxDecibels?: number;\n};\n\nexport type UseGingerLiveAnalyzerResult = {\n /** Byte frequency data (0–255); length equals `frequencyBinCount`. Updated each animation frame while enabled. */\n frequencyData: Uint8Array;\n /** Byte time-domain data (0–255); length equals `fftSize`. */\n timeDomainData: Uint8Array;\n frequencyBinCount: number;\n sampleRate: number;\n isSuspended: boolean;\n error: string | null;\n resume: () => Promise<void>;\n /**\n * Monotonically increasing counter incremented each animation frame.\n * Use as a `useMemo` / `useEffect` dependency to respond to new audio data, since\n * `frequencyData` and `timeDomainData` are mutated in-place (their reference never changes).\n */\n frame: number;\n};\n\nconst emptyFreq = new Uint8Array(0);\nconst emptyTime = new Uint8Array(0);\n\nexport function useGingerLiveAnalyzer(\n options: UseGingerLiveAnalyzerOptions = {},\n): UseGingerLiveAnalyzerResult {\n const {\n enabled = true,\n fftSize = 2048,\n smoothingTimeConstant = 0.8,\n minDecibels = -100,\n maxDecibels = -30,\n } = options;\n\n const { audioRef, state } = useGinger();\n const opts = useMemo<LiveAnalyserOptions>(\n () => ({\n fftSize,\n smoothingTimeConstant,\n minDecibels,\n maxDecibels,\n }),\n [fftSize, smoothingTimeConstant, minDecibels, maxDecibels],\n );\n\n const [frame, setFrame] = useState(0);\n const [error, setError] = useState<string | null>(null);\n const [isSuspended, setIsSuspended] = useState(false);\n const [meta, setMeta] = useState({ frequencyBinCount: 0, sampleRate: 0 });\n\n const freqRef = useRef<Uint8Array>(emptyFreq);\n const timeRef = useRef<Uint8Array>(emptyTime);\n\n const resume = useCallback(async () => {\n const ctx = contextHolderRef.current;\n if (ctx && ctx.state === \"suspended\") {\n await ctx.resume();\n }\n }, []);\n\n const contextHolderRef = useRef<AudioContext | null>(null);\n const analyserHolderRef = useRef<AnalyserNode | null>(null);\n\n useLayoutEffect(() => {\n if (!enabled || typeof window === \"undefined\") {\n return;\n }\n\n let cancelled = false;\n let consumerId: number | null = null;\n let element: HTMLAudioElement | null = null;\n let rafId = 0;\n\n const onStateChange = () => {\n const ctx = contextHolderRef.current;\n if (ctx) setIsSuspended(ctx.state === \"suspended\");\n };\n\n const runLoop = () => {\n if (cancelled) return;\n const a = analyserHolderRef.current;\n const fq = freqRef.current;\n const td = timeRef.current;\n if (a && fq.length > 0 && td.length > 0) {\n a.getByteFrequencyData(fq as Uint8Array<ArrayBuffer>);\n a.getByteTimeDomainData(td as Uint8Array<ArrayBuffer>);\n setFrame((n) => n + 1);\n }\n rafId = requestAnimationFrame(runLoop);\n };\n\n type AttachOutcome = \"ok\" | \"no-element\" | \"error\";\n\n const attach = (): AttachOutcome => {\n const el = audioRef.current;\n if (!el || cancelled) return \"no-element\";\n try {\n const { id, context, analyser } = attachLiveAnalyser(el, opts);\n consumerId = id;\n element = el;\n contextHolderRef.current = context;\n analyserHolderRef.current = analyser;\n setIsSuspended(context.state === \"suspended\");\n setError(null);\n\n context.addEventListener(\"statechange\", onStateChange);\n\n const n = analyser.frequencyBinCount;\n const fft = analyser.fftSize;\n freqRef.current = new Uint8Array(n);\n timeRef.current = new Uint8Array(fft);\n setMeta({ frequencyBinCount: n, sampleRate: context.sampleRate });\n\n rafId = requestAnimationFrame(runLoop);\n return \"ok\";\n } catch (e) {\n const msg = e instanceof Error ? e.message : \"Failed to attach live analyser\";\n setError(msg);\n contextHolderRef.current = null;\n analyserHolderRef.current = null;\n freqRef.current = emptyFreq;\n timeRef.current = emptyTime;\n setMeta({ frequencyBinCount: 0, sampleRate: 0 });\n return \"error\";\n }\n };\n\n const first = attach();\n if (first !== \"ok\") {\n let retryRaf = 0;\n const maxAttempts = 120;\n let attempts = 0;\n\n const retryLoop = () => {\n if (cancelled) return;\n const out = attach();\n if (out === \"ok\" || out === \"error\") return;\n attempts += 1;\n if (attempts >= maxAttempts) return;\n retryRaf = requestAnimationFrame(retryLoop);\n };\n\n if (first === \"no-element\") {\n retryRaf = requestAnimationFrame(retryLoop);\n }\n\n return () => {\n cancelled = true;\n cancelAnimationFrame(retryRaf);\n cancelAnimationFrame(rafId);\n if (consumerId != null && element) {\n detachLiveAnalyser(element, consumerId);\n }\n contextHolderRef.current?.removeEventListener(\"statechange\", onStateChange);\n contextHolderRef.current = null;\n analyserHolderRef.current = null;\n freqRef.current = emptyFreq;\n timeRef.current = emptyTime;\n };\n }\n\n return () => {\n cancelled = true;\n cancelAnimationFrame(rafId);\n if (consumerId != null && element) {\n detachLiveAnalyser(element, consumerId);\n }\n contextHolderRef.current?.removeEventListener(\"statechange\", onStateChange);\n contextHolderRef.current = null;\n analyserHolderRef.current = null;\n freqRef.current = emptyFreq;\n timeRef.current = emptyTime;\n setMeta({ frequencyBinCount: 0, sampleRate: 0 });\n };\n }, [enabled, audioRef, opts, state.currentIndex]);\n\n return {\n frequencyData: freqRef.current,\n timeDomainData: timeRef.current,\n frequencyBinCount: meta.frequencyBinCount,\n sampleRate: meta.sampleRate,\n isSuspended,\n error,\n resume,\n frame,\n };\n}\n","import { useEffect } from \"react\";\nimport { useGingerMedia, useGingerPlayback } from \"../context/GingerSplitContexts\";\n\nexport type GingerKeyboardShortcutBindings = {\n playPause?: string;\n next?: string;\n previous?: string;\n mute?: string;\n /** Key to seek forward by `seekSeconds`. Default: undefined (disabled). */\n seekForward?: string;\n /** Key to seek backward by `seekSeconds`. Default: undefined (disabled). */\n seekBackward?: string;\n /** Seconds to seek per `seekForward` / `seekBackward` keypress. Default: 5. */\n seekSeconds?: number;\n};\n\nexport function useGingerKeyboardShortcuts(\n enabled = true,\n bindings: GingerKeyboardShortcutBindings = {},\n): void {\n const { togglePlayPause, next, prev } = useGingerPlayback();\n const { toggleMute, seek, currentTime, duration } = useGingerMedia();\n\n const {\n mute: muteBinding,\n seekForward: seekForwardBinding,\n seekBackward: seekBackwardBinding,\n } = bindings;\n\n useEffect(() => {\n if (!enabled || typeof window === \"undefined\") return;\n const playPause = (bindings.playPause ?? \" \").toLowerCase();\n const nextKey = (bindings.next ?? \"ArrowRight\").toLowerCase();\n const prevKey = (bindings.previous ?? \"ArrowLeft\").toLowerCase();\n const muteKey = muteBinding?.toLowerCase();\n const seekFwdKey = seekForwardBinding?.toLowerCase();\n const seekBwdKey = seekBackwardBinding?.toLowerCase();\n const seekSecs = bindings.seekSeconds ?? 5;\n\n const onKeyDown = (event: KeyboardEvent) => {\n const target = event.target as HTMLElement | null;\n if (\n target &&\n ([\"INPUT\", \"TEXTAREA\", \"SELECT\"].includes(target.tagName) || target.isContentEditable)\n )\n return;\n const key = event.key.toLowerCase();\n if (key === playPause) {\n event.preventDefault();\n togglePlayPause();\n } else if (key === nextKey) {\n event.preventDefault();\n next();\n } else if (key === prevKey) {\n event.preventDefault();\n prev();\n } else if (muteKey && key === muteKey) {\n event.preventDefault();\n toggleMute();\n } else if (seekFwdKey && key === seekFwdKey) {\n event.preventDefault();\n const dur = duration > 0 ? duration : Number.POSITIVE_INFINITY;\n seek(Math.min(dur, currentTime + seekSecs));\n } else if (seekBwdKey && key === seekBwdKey) {\n event.preventDefault();\n seek(Math.max(0, currentTime - seekSecs));\n }\n };\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [\n bindings.next,\n bindings.playPause,\n bindings.previous,\n bindings.seekSeconds,\n currentTime,\n duration,\n enabled,\n muteBinding,\n next,\n prev,\n seek,\n seekBackwardBinding,\n seekForwardBinding,\n toggleMute,\n togglePlayPause,\n ]);\n}\n","import { useEffect, useRef } from \"react\";\nimport { useGingerPlayback } from \"../context/GingerSplitContexts\";\n\nexport type GingerSleepTimerOptions = {\n durationMs?: number;\n stopAfterTracks?: number;\n respectPause?: boolean;\n enabled?: boolean;\n onFire?: () => void;\n};\n\nexport function useGingerSleepTimer(options: GingerSleepTimerOptions): void {\n const { durationMs, stopAfterTracks, respectPause = true, enabled = true, onFire } = options;\n const { currentIndex, pause, isPaused } = useGingerPlayback();\n const remainingTracksRef = useRef(stopAfterTracks ?? 0);\n const prevIndexRef = useRef(currentIndex);\n\n // Remaining milliseconds for the duration-based timer; carried across pause/resume cycles.\n const remainingMsRef = useRef(durationMs ?? 0);\n // Timestamp when the current timer segment started (set when playback resumes).\n const segmentStartRef = useRef<number | null>(null);\n\n useEffect(() => {\n remainingTracksRef.current = stopAfterTracks ?? 0;\n }, [stopAfterTracks]);\n\n // Keep remainingMsRef in sync when durationMs changes while the timer is inactive.\n const prevDurationMsRef = useRef(durationMs);\n useEffect(() => {\n if (prevDurationMsRef.current !== durationMs) {\n remainingMsRef.current = durationMs ?? 0;\n prevDurationMsRef.current = durationMs;\n }\n }, [durationMs]);\n\n useEffect(() => {\n if (!enabled || !durationMs || durationMs <= 0) {\n // Reset remaining when disabled or no duration\n remainingMsRef.current = durationMs ?? 0;\n segmentStartRef.current = null;\n return;\n }\n\n if (respectPause && isPaused) {\n // Snapshot how much time is left before pausing\n if (segmentStartRef.current !== null) {\n const elapsed = Date.now() - segmentStartRef.current;\n remainingMsRef.current = Math.max(0, remainingMsRef.current - elapsed);\n segmentStartRef.current = null;\n }\n return;\n }\n\n // Playing: start (or continue) the countdown from remainingMsRef\n segmentStartRef.current = Date.now();\n const id = setTimeout(() => {\n remainingMsRef.current = 0;\n segmentStartRef.current = null;\n pause();\n onFire?.();\n }, remainingMsRef.current);\n\n return () => {\n clearTimeout(id);\n // Snapshot remaining when effect cleans up (e.g. isPaused or deps changed)\n if (segmentStartRef.current !== null) {\n const elapsed = Date.now() - segmentStartRef.current;\n remainingMsRef.current = Math.max(0, remainingMsRef.current - elapsed);\n segmentStartRef.current = null;\n }\n };\n }, [durationMs, enabled, isPaused, onFire, pause, respectPause]);\n\n useEffect(() => {\n if (!enabled || !stopAfterTracks || stopAfterTracks <= 0) return;\n const prev = prevIndexRef.current;\n prevIndexRef.current = currentIndex;\n if (currentIndex === prev) return;\n remainingTracksRef.current -= 1;\n if (remainingTracksRef.current <= 0) {\n pause();\n onFire?.();\n }\n }, [currentIndex, enabled, onFire, pause, stopAfterTracks]);\n}\n","import { useEffect, useRef } from \"react\";\nimport { useGingerState } from \"../context/GingerSplitContexts\";\n\nexport function useGingerDebugLog(enabled = false): void {\n const state = useGingerState();\n const prevRef = useRef(state);\n\n useEffect(() => {\n if (!enabled || typeof console === \"undefined\") return;\n const prev = prevRef.current;\n if (prev !== state) {\n console.debug(\"[ginger]\", {\n from: {\n currentIndex: prev.currentIndex,\n isPaused: prev.isPaused,\n currentTime: prev.currentTime,\n repeatMode: prev.repeatMode,\n },\n to: {\n currentIndex: state.currentIndex,\n isPaused: state.isPaused,\n currentTime: state.currentTime,\n repeatMode: state.repeatMode,\n },\n });\n }\n prevRef.current = state;\n }, [enabled, state]);\n}\n","import { useCallback, useState } from \"react\";\nimport type { PointerEvent as ReactPointerEvent } from \"react\";\nimport {\n gingerStateFromContextValues,\n useGingerMedia,\n useGingerPlayback,\n} from \"../context/GingerSplitContexts\";\nimport { progressFraction } from \"../internal/selectors\";\n\nexport type SeekDragState = {\n /** Raw drag fraction — only updated during an active drag gesture. */\n fraction: number;\n /** Blended fraction: follows live playback when idle, drag position when dragging. */\n displayFraction: number;\n isDragging: boolean;\n onPointerDown: (event: ReactPointerEvent<HTMLElement>) => void;\n};\n\nfunction clamp01(value: number): number {\n return Math.max(0, Math.min(1, value));\n}\n\nexport function useSeekDrag(duration: number): SeekDragState {\n const media = useGingerMedia();\n const playback = useGingerPlayback();\n const { seek } = media;\n const [fraction, setFraction] = useState(0);\n const [isDragging, setIsDragging] = useState(false);\n\n const liveFraction = progressFraction(gingerStateFromContextValues(playback, media));\n const displayFraction = isDragging ? fraction : liveFraction;\n\n const onPointerDown = useCallback(\n (event: ReactPointerEvent<HTMLElement>) => {\n if (!(duration > 0)) return;\n const target = event.currentTarget;\n const rect = target.getBoundingClientRect();\n const update = (clientX: number) => {\n const ratio = clamp01((clientX - rect.left) / rect.width);\n setFraction(ratio);\n seek(ratio * duration);\n };\n setIsDragging(true);\n target.setPointerCapture(event.pointerId);\n update(event.clientX);\n const onMove = (moveEvent: PointerEvent) => update(moveEvent.clientX);\n const onUp = (upEvent: PointerEvent) => {\n update(upEvent.clientX);\n setIsDragging(false);\n target.releasePointerCapture(event.pointerId);\n target.removeEventListener(\"pointermove\", onMove);\n target.removeEventListener(\"pointerup\", onUp);\n target.removeEventListener(\"pointercancel\", onUp);\n };\n target.addEventListener(\"pointermove\", onMove);\n target.addEventListener(\"pointerup\", onUp);\n target.addEventListener(\"pointercancel\", onUp);\n },\n [duration, seek],\n );\n\n return { fraction, displayFraction, isDragging, onPointerDown };\n}\n","import { useEffect } from \"react\";\nimport { useGingerPlayback } from \"../context/GingerSplitContexts\";\nimport { computeNextIndex } from \"../core/transitions\";\n\nexport type UseNextTrackPrefetchOptions = {\n /** When false, no prefetch runs. Default true. */\n enabled?: boolean;\n /**\n * Match `crossOrigin` on `Ginger.Player` when `fileUrl` is cross-origin so the browser\n * can reuse cached media consistently.\n */\n crossOrigin?: \"\" | \"anonymous\" | \"use-credentials\" | undefined;\n};\n\n/**\n * Warms the browser cache for the **logical** next track (same rules as the Next control:\n * `computeNextIndex` from queue, repeat, and playback mode) using a detached `HTMLAudioElement`\n * with `preload=\"auto\"`. Safe to call alongside `Ginger.Player`; it does not replace main playback.\n *\n * The effect depends on the `tracks` array reference from context. If the parent recreates `tracks`\n * every render, prefetch will restart repeatedly; keep a stable queue reference (e.g. memoize) when possible.\n */\nexport function useNextTrackPrefetch(options: UseNextTrackPrefetchOptions = {}): void {\n const { enabled = true, crossOrigin } = options;\n const { tracks, currentIndex, repeatMode, playbackMode } = useGingerPlayback();\n\n useEffect(() => {\n if (!enabled || typeof document === \"undefined\") return;\n const nextIndex = computeNextIndex({ tracks, currentIndex, repeatMode, playbackMode });\n if (nextIndex === currentIndex) return;\n const nextUrl = tracks[nextIndex]?.fileUrl ?? \"\";\n if (!nextUrl) return;\n\n const audio = document.createElement(\"audio\");\n audio.preload = \"auto\";\n if (crossOrigin) audio.crossOrigin = crossOrigin;\n audio.src = nextUrl;\n audio.load();\n\n return () => {\n audio.removeAttribute(\"src\");\n audio.load();\n };\n }, [enabled, crossOrigin, tracks, currentIndex, repeatMode, playbackMode]);\n}\n","import {\n clampPlaybackRate,\n clampVolume,\n createInitialState,\n gingerReducer,\n} from \"./core/playbackReducer\";\nimport type {\n GingerAction,\n GingerInitPayload,\n GingerState,\n PlaylistMeta,\n RepeatMode,\n Track,\n} from \"./types\";\n\nexport type GingerStoreOptions = {\n tracks?: Track[];\n currentIndex?: number;\n playlistMeta?: PlaylistMeta | null;\n isPaused?: boolean;\n isShuffled?: boolean;\n repeatMode?: RepeatMode;\n playbackMode?: GingerState[\"playbackMode\"];\n volume?: number;\n muted?: boolean;\n playbackRate?: number;\n};\n\nexport type GingerStore = {\n /** Returns the current state snapshot. */\n getState: () => GingerState;\n /** Dispatch an action to update state. Synchronously updates state and notifies listeners. */\n dispatch: (action: GingerAction) => void;\n /**\n * Subscribe to state changes. The listener is called after every `dispatch` that produces\n * a new state object. Returns an unsubscribe function.\n */\n subscribe: (listener: (state: GingerState) => void) => () => void;\n /** Convenience: re-initialise with a new set of init options (equivalent to `dispatch({ type: \"INIT\", ... })`). */\n init: (payload: GingerInitPayload) => void;\n /** Clamp helpers re-exported for convenience. */\n clampVolume: typeof clampVolume;\n clampPlaybackRate: typeof clampPlaybackRate;\n};\n\n/**\n * Framework-agnostic store wrapping `gingerReducer`.\n * Usable outside React — e.g. in Svelte, Vue, Node.js testing, or server-side rendering contexts.\n *\n * @example\n * ```ts\n * import { createGingerStore } from \"@lucaismyname/ginger\";\n *\n * const store = createGingerStore({ tracks: myTracks });\n * const unsub = store.subscribe((state) => console.log(state.currentIndex));\n * store.dispatch({ type: \"NEXT\" });\n * unsub();\n * ```\n */\nexport function createGingerStore(options: GingerStoreOptions = {}): GingerStore {\n let state = createInitialState({\n tracks: options.tracks ?? [],\n currentIndex: options.currentIndex,\n playlistMeta: options.playlistMeta,\n isPaused: options.isPaused,\n isShuffled: options.isShuffled,\n repeatMode: options.repeatMode,\n playbackMode: options.playbackMode,\n volume: options.volume,\n muted: options.muted,\n playbackRate: options.playbackRate,\n });\n\n const listeners = new Set<(state: GingerState) => void>();\n\n const dispatch = (action: GingerAction): void => {\n const next = gingerReducer(state, action);\n if (next !== state) {\n state = next;\n for (const listener of listeners) {\n listener(state);\n }\n }\n };\n\n const subscribe = (listener: (state: GingerState) => void): (() => void) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n };\n\n const init = (payload: GingerInitPayload): void => {\n dispatch({ type: \"INIT\", payload });\n };\n\n return {\n getState: () => state,\n dispatch,\n subscribe,\n init,\n clampVolume,\n clampPlaybackRate,\n };\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useGingerPlayback } from \"../context/GingerSplitContexts\";\nimport type { Track } from \"../types\";\n\nexport type GingerPlaybackHistoryEntry = {\n track: Track;\n /** The track's index in the queue at the time it was played. */\n index: number;\n /** Unix timestamp (ms) when the track started playing. */\n playedAt: number;\n};\n\nexport type UseGingerPlaybackHistoryOptions = {\n /** Maximum number of entries to keep. Oldest entries are dropped first. Default: 50. */\n maxLength?: number;\n};\n\nexport type UseGingerPlaybackHistoryResult = {\n /** Chronological list of played tracks; most recent entry is last. */\n history: GingerPlaybackHistoryEntry[];\n clearHistory: () => void;\n};\n\n/**\n * Records a history of played tracks in chronological order.\n * Useful for \"what was playing before\" in shuffle mode or for analytics.\n *\n * The history is stored in component state and does not survive remounts.\n * In shuffle mode, `index` reflects the position in the current shuffled queue.\n */\nexport function useGingerPlaybackHistory(\n options: UseGingerPlaybackHistoryOptions = {},\n): UseGingerPlaybackHistoryResult {\n const { maxLength = 50 } = options;\n const { tracks, currentIndex } = useGingerPlayback();\n\n const [history, setHistory] = useState<GingerPlaybackHistoryEntry[]>([]);\n const prevIndexRef = useRef<number | null>(null);\n const prevTracksRef = useRef(tracks);\n prevTracksRef.current = tracks;\n\n useEffect(() => {\n const track = tracks[currentIndex];\n if (!track) return;\n\n if (prevIndexRef.current === currentIndex) return;\n prevIndexRef.current = currentIndex;\n\n const entry: GingerPlaybackHistoryEntry = {\n track,\n index: currentIndex,\n playedAt: Date.now(),\n };\n\n setHistory((prev) => {\n const next = [...prev, entry];\n return next.length > maxLength ? next.slice(next.length - maxLength) : next;\n });\n }, [currentIndex, tracks, maxLength]);\n\n const clearHistory = useCallback(() => setHistory([]), []);\n\n return { history, clearHistory };\n}\n","import { useCallback, useRef, useState } from \"react\";\nimport { useGingerMedia } from \"../context/GingerSplitContexts\";\n\nexport type UseGingerVolumeFadeOptions = {\n /** Target volume to fade to (0–1). */\n targetVolume: number;\n /** Duration of the fade in milliseconds. */\n durationMs: number;\n /** Called when the fade completes normally (not when cancelled). */\n onComplete?: () => void;\n};\n\nexport type UseGingerVolumeFadeResult = {\n /** Start a volume fade. Cancels any in-progress fade. */\n fadeVolumeTo: (options: UseGingerVolumeFadeOptions) => void;\n /** Cancel the current fade and hold at the current volume. */\n cancelFade: () => void;\n /** True while a fade is in progress. */\n isFading: boolean;\n};\n\n/**\n * Smoothly interpolates volume over a given duration using `requestAnimationFrame`.\n * Useful for fade-in on track start, fade-out before sleep timer fires, or crossfade prep.\n */\nexport function useGingerVolumeFade(): UseGingerVolumeFadeResult {\n const { setVolume, volume } = useGingerMedia();\n const [isFading, setIsFading] = useState(false);\n\n const rafRef = useRef<number>(0);\n const cancelledRef = useRef(false);\n\n const cancelFade = useCallback(() => {\n cancelAnimationFrame(rafRef.current);\n cancelledRef.current = true;\n setIsFading(false);\n }, []);\n\n const fadeVolumeTo = useCallback(\n ({ targetVolume, durationMs, onComplete }: UseGingerVolumeFadeOptions) => {\n cancelAnimationFrame(rafRef.current);\n cancelledRef.current = false;\n\n const clamp = (v: number) => Math.min(1, Math.max(0, v));\n const target = clamp(targetVolume);\n const startTime = performance.now();\n\n // Capture start volume at the moment the fade begins\n let startVolume = volume;\n\n setIsFading(true);\n\n const tick = (now: number) => {\n if (cancelledRef.current) return;\n const elapsed = now - startTime;\n const progress = Math.min(1, elapsed / Math.max(1, durationMs));\n const current = startVolume + (target - startVolume) * progress;\n setVolume(clamp(current));\n\n if (progress < 1) {\n rafRef.current = requestAnimationFrame(tick);\n } else {\n setIsFading(false);\n onComplete?.();\n }\n };\n\n // Use a small delay so `volume` from closure is the current value\n rafRef.current = requestAnimationFrame((now) => {\n startVolume = volume;\n tick(now);\n });\n },\n [setVolume, volume],\n );\n\n return { fadeVolumeTo, cancelFade, isFading };\n}\n","import { useMemo } from \"react\";\nimport { useGingerMedia, useGingerPlayback } from \"../context/GingerSplitContexts\";\n\nexport type GingerChapterProgress = {\n /** Fraction (0–1) of the way through the active chapter. 0 when no chapter is active. */\n progress: number;\n /** Seconds elapsed since the start of the active chapter. */\n elapsed: number;\n /** Seconds remaining until the end of the active chapter (or until end of track if last chapter). */\n remaining: number;\n};\n\n/**\n * Returns detailed progress information for the currently active chapter.\n * Complements `useGingerChapters` with per-chapter playback fractions.\n */\nexport function useGingerChapterProgress(): GingerChapterProgress {\n const { tracks, currentIndex } = useGingerPlayback();\n const { currentTime, duration } = useGingerMedia();\n\n const chapters = useMemo(() => {\n const raw = tracks[currentIndex]?.chapters ?? [];\n return [...raw]\n .filter((c) => c && Number.isFinite(c.startSeconds) && c.startSeconds >= 0)\n .sort((a, b) => a.startSeconds - b.startSeconds);\n }, [tracks, currentIndex]);\n\n return useMemo<GingerChapterProgress>(() => {\n if (chapters.length === 0) return { progress: 0, elapsed: 0, remaining: 0 };\n\n // Find active chapter (last one whose start is <= currentTime)\n let activeIdx = -1;\n for (let i = chapters.length - 1; i >= 0; i--) {\n if (currentTime >= chapters[i]!.startSeconds) {\n activeIdx = i;\n break;\n }\n }\n\n if (activeIdx === -1) return { progress: 0, elapsed: 0, remaining: 0 };\n\n const chapter = chapters[activeIdx]!;\n const nextChapter = chapters[activeIdx + 1];\n const chapterEnd = nextChapter?.startSeconds ?? (duration > 0 ? duration : currentTime);\n const chapterDuration = Math.max(0, chapterEnd - chapter.startSeconds);\n const elapsed = Math.max(0, currentTime - chapter.startSeconds);\n const remaining = Math.max(0, chapterEnd - currentTime);\n const progress = chapterDuration > 0 ? Math.min(1, elapsed / chapterDuration) : 0;\n\n return { progress, elapsed, remaining };\n }, [chapters, currentTime, duration]);\n}\n"],"names":["emptyFreq","emptyTime","useGingerLiveAnalyzer","options","enabled","fftSize","smoothingTimeConstant","minDecibels","maxDecibels","audioRef","state","useGinger","opts","useMemo","frame","setFrame","useState","error","setError","isSuspended","setIsSuspended","meta","setMeta","freqRef","useRef","timeRef","resume","useCallback","ctx","contextHolderRef","analyserHolderRef","useLayoutEffect","cancelled","consumerId","element","rafId","onStateChange","runLoop","a","fq","td","n","attach","el","id","context","analyser","attachLiveAnalyser","fft","e","msg","first","retryRaf","maxAttempts","attempts","retryLoop","out","detachLiveAnalyser","_a","useGingerKeyboardShortcuts","bindings","togglePlayPause","next","prev","useGingerPlayback","toggleMute","seek","currentTime","duration","useGingerMedia","muteBinding","seekForwardBinding","seekBackwardBinding","useEffect","playPause","nextKey","prevKey","muteKey","seekFwdKey","seekBwdKey","seekSecs","onKeyDown","event","target","key","dur","useGingerSleepTimer","durationMs","stopAfterTracks","respectPause","onFire","currentIndex","pause","isPaused","remainingTracksRef","prevIndexRef","remainingMsRef","segmentStartRef","prevDurationMsRef","elapsed","useGingerDebugLog","useGingerState","prevRef","clamp01","value","useSeekDrag","media","playback","fraction","setFraction","isDragging","setIsDragging","liveFraction","progressFraction","gingerStateFromContextValues","displayFraction","onPointerDown","rect","update","clientX","ratio","onMove","moveEvent","onUp","upEvent","useNextTrackPrefetch","crossOrigin","tracks","repeatMode","playbackMode","nextIndex","computeNextIndex","nextUrl","audio","createGingerStore","createInitialState","listeners","dispatch","action","gingerReducer","listener","payload","clampVolume","clampPlaybackRate","useGingerPlaybackHistory","maxLength","history","setHistory","prevTracksRef","track","entry","clearHistory","useGingerVolumeFade","setVolume","volume","isFading","setIsFading","rafRef","cancelledRef","cancelFade","targetVolume","onComplete","clamp","v","startTime","startVolume","tick","now","progress","current","useGingerChapterProgress","chapters","c","b","activeIdx","i","chapter","nextChapter","chapterEnd","chapterDuration","remaining"],"mappings":"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"}
1
+ {"version":3,"file":"useGingerChapterProgress-wxAmN_uo.js","sources":["../src/analyzer/useGingerLiveAnalyzer.ts","../src/hooks/useGingerKeyboardShortcuts.ts","../src/hooks/useGingerSleepTimer.ts","../src/hooks/useGingerDebugLog.ts","../src/hooks/useSeekDrag.ts","../src/hooks/useNextTrackPrefetch.ts","../src/store.ts","../src/hooks/useGingerPlaybackHistory.ts","../src/hooks/useGingerVolumeFade.ts","../src/hooks/useGingerChapterProgress.ts"],"sourcesContent":["import { useCallback, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport { useGinger } from \"../hooks/useGinger\";\nimport { type LiveAnalyserOptions, attachLiveAnalyser, detachLiveAnalyser } from \"./liveAudioGraph\";\n\nexport type UseGingerLiveAnalyzerOptions = {\n /** When false, the analyser is detached and no frames are read. Default true. */\n enabled?: boolean;\n fftSize?: number;\n smoothingTimeConstant?: number;\n minDecibels?: number;\n maxDecibels?: number;\n};\n\nexport type UseGingerLiveAnalyzerResult = {\n /** Byte frequency data (0–255); length equals `frequencyBinCount`. Updated each animation frame while enabled. */\n frequencyData: Uint8Array;\n /** Byte time-domain data (0–255); length equals `fftSize`. */\n timeDomainData: Uint8Array;\n frequencyBinCount: number;\n sampleRate: number;\n isSuspended: boolean;\n error: string | null;\n resume: () => Promise<void>;\n /**\n * Monotonically increasing counter incremented each animation frame.\n * Use as a `useMemo` / `useEffect` dependency to respond to new audio data, since\n * `frequencyData` and `timeDomainData` are mutated in-place (their reference never changes).\n */\n frame: number;\n};\n\nconst emptyFreq = new Uint8Array(0);\nconst emptyTime = new Uint8Array(0);\n\nexport function useGingerLiveAnalyzer(\n options: UseGingerLiveAnalyzerOptions = {},\n): UseGingerLiveAnalyzerResult {\n const {\n enabled = true,\n fftSize = 2048,\n smoothingTimeConstant = 0.8,\n minDecibels = -100,\n maxDecibels = -30,\n } = options;\n\n const { audioRef, state } = useGinger();\n const opts = useMemo<LiveAnalyserOptions>(\n () => ({\n fftSize,\n smoothingTimeConstant,\n minDecibels,\n maxDecibels,\n }),\n [fftSize, smoothingTimeConstant, minDecibels, maxDecibels],\n );\n\n const [frame, setFrame] = useState(0);\n const [error, setError] = useState<string | null>(null);\n const [isSuspended, setIsSuspended] = useState(false);\n const [meta, setMeta] = useState({ frequencyBinCount: 0, sampleRate: 0 });\n\n const freqRef = useRef<Uint8Array>(emptyFreq);\n const timeRef = useRef<Uint8Array>(emptyTime);\n\n const resume = useCallback(async () => {\n const ctx = contextHolderRef.current;\n if (ctx && ctx.state === \"suspended\") {\n await ctx.resume();\n }\n }, []);\n\n const contextHolderRef = useRef<AudioContext | null>(null);\n const analyserHolderRef = useRef<AnalyserNode | null>(null);\n\n useLayoutEffect(() => {\n if (!enabled || typeof window === \"undefined\") {\n return;\n }\n\n let cancelled = false;\n let consumerId: number | null = null;\n let element: HTMLAudioElement | null = null;\n let rafId = 0;\n\n const onStateChange = () => {\n const ctx = contextHolderRef.current;\n if (ctx) setIsSuspended(ctx.state === \"suspended\");\n };\n\n const runLoop = () => {\n if (cancelled) return;\n const a = analyserHolderRef.current;\n const fq = freqRef.current;\n const td = timeRef.current;\n if (a && fq.length > 0 && td.length > 0) {\n a.getByteFrequencyData(fq as Uint8Array<ArrayBuffer>);\n a.getByteTimeDomainData(td as Uint8Array<ArrayBuffer>);\n setFrame((n) => n + 1);\n }\n rafId = requestAnimationFrame(runLoop);\n };\n\n type AttachOutcome = \"ok\" | \"no-element\" | \"error\";\n\n const attach = (): AttachOutcome => {\n const el = audioRef.current;\n if (!el || cancelled) return \"no-element\";\n try {\n const { id, context, analyser } = attachLiveAnalyser(el, opts);\n consumerId = id;\n element = el;\n contextHolderRef.current = context;\n analyserHolderRef.current = analyser;\n setIsSuspended(context.state === \"suspended\");\n setError(null);\n\n context.addEventListener(\"statechange\", onStateChange);\n\n const n = analyser.frequencyBinCount;\n const fft = analyser.fftSize;\n freqRef.current = new Uint8Array(n);\n timeRef.current = new Uint8Array(fft);\n setMeta({ frequencyBinCount: n, sampleRate: context.sampleRate });\n\n rafId = requestAnimationFrame(runLoop);\n return \"ok\";\n } catch (e) {\n const msg = e instanceof Error ? e.message : \"Failed to attach live analyser\";\n setError(msg);\n contextHolderRef.current = null;\n analyserHolderRef.current = null;\n freqRef.current = emptyFreq;\n timeRef.current = emptyTime;\n setMeta({ frequencyBinCount: 0, sampleRate: 0 });\n return \"error\";\n }\n };\n\n const first = attach();\n if (first !== \"ok\") {\n let retryRaf = 0;\n const maxAttempts = 120;\n let attempts = 0;\n\n const retryLoop = () => {\n if (cancelled) return;\n const out = attach();\n if (out === \"ok\" || out === \"error\") return;\n attempts += 1;\n if (attempts >= maxAttempts) return;\n retryRaf = requestAnimationFrame(retryLoop);\n };\n\n if (first === \"no-element\") {\n retryRaf = requestAnimationFrame(retryLoop);\n }\n\n return () => {\n cancelled = true;\n cancelAnimationFrame(retryRaf);\n cancelAnimationFrame(rafId);\n if (consumerId != null && element) {\n detachLiveAnalyser(element, consumerId);\n }\n contextHolderRef.current?.removeEventListener(\"statechange\", onStateChange);\n contextHolderRef.current = null;\n analyserHolderRef.current = null;\n freqRef.current = emptyFreq;\n timeRef.current = emptyTime;\n };\n }\n\n return () => {\n cancelled = true;\n cancelAnimationFrame(rafId);\n if (consumerId != null && element) {\n detachLiveAnalyser(element, consumerId);\n }\n contextHolderRef.current?.removeEventListener(\"statechange\", onStateChange);\n contextHolderRef.current = null;\n analyserHolderRef.current = null;\n freqRef.current = emptyFreq;\n timeRef.current = emptyTime;\n setMeta({ frequencyBinCount: 0, sampleRate: 0 });\n };\n }, [enabled, audioRef, opts, state.currentIndex]);\n\n return {\n frequencyData: freqRef.current,\n timeDomainData: timeRef.current,\n frequencyBinCount: meta.frequencyBinCount,\n sampleRate: meta.sampleRate,\n isSuspended,\n error,\n resume,\n frame,\n };\n}\n","import { useEffect } from \"react\";\nimport { useGingerMedia, useGingerPlayback } from \"../context/GingerSplitContexts\";\n\nexport type GingerKeyboardShortcutBindings = {\n playPause?: string;\n next?: string;\n previous?: string;\n mute?: string;\n /** Key to seek forward by `seekSeconds`. Default: undefined (disabled). */\n seekForward?: string;\n /** Key to seek backward by `seekSeconds`. Default: undefined (disabled). */\n seekBackward?: string;\n /** Seconds to seek per `seekForward` / `seekBackward` keypress. Default: 5. */\n seekSeconds?: number;\n};\n\nexport function useGingerKeyboardShortcuts(\n enabled = true,\n bindings: GingerKeyboardShortcutBindings = {},\n): void {\n const { togglePlayPause, next, prev } = useGingerPlayback();\n const { toggleMute, seek, currentTime, duration } = useGingerMedia();\n\n const {\n mute: muteBinding,\n seekForward: seekForwardBinding,\n seekBackward: seekBackwardBinding,\n } = bindings;\n\n useEffect(() => {\n if (!enabled || typeof window === \"undefined\") return;\n const playPause = (bindings.playPause ?? \" \").toLowerCase();\n const nextKey = (bindings.next ?? \"ArrowRight\").toLowerCase();\n const prevKey = (bindings.previous ?? \"ArrowLeft\").toLowerCase();\n const muteKey = muteBinding?.toLowerCase();\n const seekFwdKey = seekForwardBinding?.toLowerCase();\n const seekBwdKey = seekBackwardBinding?.toLowerCase();\n const seekSecs = bindings.seekSeconds ?? 5;\n\n const onKeyDown = (event: KeyboardEvent) => {\n const target = event.target as HTMLElement | null;\n if (\n target &&\n ([\"INPUT\", \"TEXTAREA\", \"SELECT\"].includes(target.tagName) || target.isContentEditable)\n )\n return;\n const key = event.key.toLowerCase();\n if (key === playPause) {\n event.preventDefault();\n togglePlayPause();\n } else if (key === nextKey) {\n event.preventDefault();\n next();\n } else if (key === prevKey) {\n event.preventDefault();\n prev();\n } else if (muteKey && key === muteKey) {\n event.preventDefault();\n toggleMute();\n } else if (seekFwdKey && key === seekFwdKey) {\n event.preventDefault();\n const dur = duration > 0 ? duration : Number.POSITIVE_INFINITY;\n seek(Math.min(dur, currentTime + seekSecs));\n } else if (seekBwdKey && key === seekBwdKey) {\n event.preventDefault();\n seek(Math.max(0, currentTime - seekSecs));\n }\n };\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [\n bindings.next,\n bindings.playPause,\n bindings.previous,\n bindings.seekSeconds,\n currentTime,\n duration,\n enabled,\n muteBinding,\n next,\n prev,\n seek,\n seekBackwardBinding,\n seekForwardBinding,\n toggleMute,\n togglePlayPause,\n ]);\n}\n","import { useEffect, useRef } from \"react\";\nimport { useGingerPlayback } from \"../context/GingerSplitContexts\";\n\nexport type GingerSleepTimerOptions = {\n durationMs?: number;\n stopAfterTracks?: number;\n respectPause?: boolean;\n enabled?: boolean;\n onFire?: () => void;\n};\n\nexport function useGingerSleepTimer(options: GingerSleepTimerOptions): void {\n const { durationMs, stopAfterTracks, respectPause = true, enabled = true, onFire } = options;\n const { currentIndex, pause, isPaused } = useGingerPlayback();\n const remainingTracksRef = useRef(stopAfterTracks ?? 0);\n const prevIndexRef = useRef(currentIndex);\n\n // Remaining milliseconds for the duration-based timer; carried across pause/resume cycles.\n const remainingMsRef = useRef(durationMs ?? 0);\n // Timestamp when the current timer segment started (set when playback resumes).\n const segmentStartRef = useRef<number | null>(null);\n\n useEffect(() => {\n remainingTracksRef.current = stopAfterTracks ?? 0;\n }, [stopAfterTracks]);\n\n // Keep remainingMsRef in sync when durationMs changes while the timer is inactive.\n const prevDurationMsRef = useRef(durationMs);\n useEffect(() => {\n if (prevDurationMsRef.current !== durationMs) {\n remainingMsRef.current = durationMs ?? 0;\n prevDurationMsRef.current = durationMs;\n }\n }, [durationMs]);\n\n useEffect(() => {\n if (!enabled || !durationMs || durationMs <= 0) {\n // Reset remaining when disabled or no duration\n remainingMsRef.current = durationMs ?? 0;\n segmentStartRef.current = null;\n return;\n }\n\n if (respectPause && isPaused) {\n // Snapshot how much time is left before pausing\n if (segmentStartRef.current !== null) {\n const elapsed = Date.now() - segmentStartRef.current;\n remainingMsRef.current = Math.max(0, remainingMsRef.current - elapsed);\n segmentStartRef.current = null;\n }\n return;\n }\n\n // Playing: start (or continue) the countdown from remainingMsRef\n segmentStartRef.current = Date.now();\n const id = setTimeout(() => {\n remainingMsRef.current = 0;\n segmentStartRef.current = null;\n pause();\n onFire?.();\n }, remainingMsRef.current);\n\n return () => {\n clearTimeout(id);\n // Snapshot remaining when effect cleans up (e.g. isPaused or deps changed)\n if (segmentStartRef.current !== null) {\n const elapsed = Date.now() - segmentStartRef.current;\n remainingMsRef.current = Math.max(0, remainingMsRef.current - elapsed);\n segmentStartRef.current = null;\n }\n };\n }, [durationMs, enabled, isPaused, onFire, pause, respectPause]);\n\n useEffect(() => {\n if (!enabled || !stopAfterTracks || stopAfterTracks <= 0) return;\n const prev = prevIndexRef.current;\n prevIndexRef.current = currentIndex;\n if (currentIndex === prev) return;\n remainingTracksRef.current -= 1;\n if (remainingTracksRef.current <= 0) {\n pause();\n onFire?.();\n }\n }, [currentIndex, enabled, onFire, pause, stopAfterTracks]);\n}\n","import { useEffect, useRef } from \"react\";\nimport { useGingerState } from \"../context/GingerSplitContexts\";\n\nexport function useGingerDebugLog(enabled = false): void {\n const state = useGingerState();\n const prevRef = useRef(state);\n\n useEffect(() => {\n if (!enabled || typeof console === \"undefined\") return;\n const prev = prevRef.current;\n if (prev !== state) {\n console.debug(\"[ginger]\", {\n from: {\n currentIndex: prev.currentIndex,\n isPaused: prev.isPaused,\n currentTime: prev.currentTime,\n repeatMode: prev.repeatMode,\n },\n to: {\n currentIndex: state.currentIndex,\n isPaused: state.isPaused,\n currentTime: state.currentTime,\n repeatMode: state.repeatMode,\n },\n });\n }\n prevRef.current = state;\n }, [enabled, state]);\n}\n","import { useCallback, useState } from \"react\";\nimport type { PointerEvent as ReactPointerEvent } from \"react\";\nimport {\n gingerStateFromContextValues,\n useGingerMedia,\n useGingerPlayback,\n} from \"../context/GingerSplitContexts\";\nimport { progressFraction } from \"../internal/selectors\";\n\nexport type SeekDragState = {\n /** Raw drag fraction — only updated during an active drag gesture. */\n fraction: number;\n /** Blended fraction: follows live playback when idle, drag position when dragging. */\n displayFraction: number;\n isDragging: boolean;\n onPointerDown: (event: ReactPointerEvent<HTMLElement>) => void;\n};\n\nfunction clamp01(value: number): number {\n return Math.max(0, Math.min(1, value));\n}\n\nexport function useSeekDrag(duration: number): SeekDragState {\n const media = useGingerMedia();\n const playback = useGingerPlayback();\n const { seek } = media;\n const [fraction, setFraction] = useState(0);\n const [isDragging, setIsDragging] = useState(false);\n\n const liveFraction = progressFraction(gingerStateFromContextValues(playback, media));\n const displayFraction = isDragging ? fraction : liveFraction;\n\n const onPointerDown = useCallback(\n (event: ReactPointerEvent<HTMLElement>) => {\n if (!(duration > 0)) return;\n const target = event.currentTarget;\n const rect = target.getBoundingClientRect();\n const update = (clientX: number) => {\n const ratio = clamp01((clientX - rect.left) / rect.width);\n setFraction(ratio);\n seek(ratio * duration);\n };\n setIsDragging(true);\n target.setPointerCapture(event.pointerId);\n update(event.clientX);\n const onMove = (moveEvent: PointerEvent) => update(moveEvent.clientX);\n const onUp = (upEvent: PointerEvent) => {\n update(upEvent.clientX);\n setIsDragging(false);\n target.releasePointerCapture(event.pointerId);\n target.removeEventListener(\"pointermove\", onMove);\n target.removeEventListener(\"pointerup\", onUp);\n target.removeEventListener(\"pointercancel\", onUp);\n };\n target.addEventListener(\"pointermove\", onMove);\n target.addEventListener(\"pointerup\", onUp);\n target.addEventListener(\"pointercancel\", onUp);\n },\n [duration, seek],\n );\n\n return { fraction, displayFraction, isDragging, onPointerDown };\n}\n","import { useEffect } from \"react\";\nimport { useGingerPlayback } from \"../context/GingerSplitContexts\";\nimport { computeNextIndex } from \"../core/transitions\";\n\nexport type UseNextTrackPrefetchOptions = {\n /** When false, no prefetch runs. Default true. */\n enabled?: boolean;\n /**\n * Match `crossOrigin` on `Ginger.Player` when `fileUrl` is cross-origin so the browser\n * can reuse cached media consistently.\n */\n crossOrigin?: \"\" | \"anonymous\" | \"use-credentials\" | undefined;\n};\n\n/**\n * Warms the browser cache for the **logical** next track (same rules as the Next control:\n * `computeNextIndex` from queue, repeat, and playback mode) using a detached `HTMLAudioElement`\n * with `preload=\"auto\"`. Safe to call alongside `Ginger.Player`; it does not replace main playback.\n *\n * The effect depends on the `tracks` array reference from context. If the parent recreates `tracks`\n * every render, prefetch will restart repeatedly; keep a stable queue reference (e.g. memoize) when possible.\n */\nexport function useNextTrackPrefetch(options: UseNextTrackPrefetchOptions = {}): void {\n const { enabled = true, crossOrigin } = options;\n const { tracks, currentIndex, repeatMode, playbackMode } = useGingerPlayback();\n\n useEffect(() => {\n if (!enabled || typeof document === \"undefined\") return;\n const nextIndex = computeNextIndex({ tracks, currentIndex, repeatMode, playbackMode });\n if (nextIndex === currentIndex) return;\n const nextUrl = tracks[nextIndex]?.fileUrl ?? \"\";\n if (!nextUrl) return;\n\n const audio = document.createElement(\"audio\");\n audio.preload = \"auto\";\n if (crossOrigin) audio.crossOrigin = crossOrigin;\n audio.src = nextUrl;\n audio.load();\n\n return () => {\n audio.removeAttribute(\"src\");\n audio.load();\n };\n }, [enabled, crossOrigin, tracks, currentIndex, repeatMode, playbackMode]);\n}\n","import {\n clampPlaybackRate,\n clampVolume,\n createInitialState,\n gingerReducer,\n} from \"./core/playbackReducer\";\nimport type {\n GingerAction,\n GingerInitPayload,\n GingerState,\n PlaylistMeta,\n RepeatMode,\n Track,\n} from \"./types\";\n\nexport type GingerStoreOptions = {\n tracks?: Track[];\n currentIndex?: number;\n playlistMeta?: PlaylistMeta | null;\n isPaused?: boolean;\n isShuffled?: boolean;\n repeatMode?: RepeatMode;\n playbackMode?: GingerState[\"playbackMode\"];\n volume?: number;\n muted?: boolean;\n playbackRate?: number;\n};\n\nexport type GingerStore = {\n /** Returns the current state snapshot. */\n getState: () => GingerState;\n /** Dispatch an action to update state. Synchronously updates state and notifies listeners. */\n dispatch: (action: GingerAction) => void;\n /**\n * Subscribe to state changes. The listener is called after every `dispatch` that produces\n * a new state object. Returns an unsubscribe function.\n */\n subscribe: (listener: (state: GingerState) => void) => () => void;\n /** Convenience: re-initialise with a new set of init options (equivalent to `dispatch({ type: \"INIT\", ... })`). */\n init: (payload: GingerInitPayload) => void;\n /** Clamp helpers re-exported for convenience. */\n clampVolume: typeof clampVolume;\n clampPlaybackRate: typeof clampPlaybackRate;\n};\n\n/**\n * Framework-agnostic store wrapping `gingerReducer`.\n * Usable outside React — e.g. in Svelte, Vue, Node.js testing, or server-side rendering contexts.\n *\n * @example\n * ```ts\n * import { createGingerStore } from \"@lucaismyname/ginger\";\n *\n * const store = createGingerStore({ tracks: myTracks });\n * const unsub = store.subscribe((state) => console.log(state.currentIndex));\n * store.dispatch({ type: \"NEXT\" });\n * unsub();\n * ```\n */\nexport function createGingerStore(options: GingerStoreOptions = {}): GingerStore {\n let state = createInitialState({\n tracks: options.tracks ?? [],\n currentIndex: options.currentIndex,\n playlistMeta: options.playlistMeta,\n isPaused: options.isPaused,\n isShuffled: options.isShuffled,\n repeatMode: options.repeatMode,\n playbackMode: options.playbackMode,\n volume: options.volume,\n muted: options.muted,\n playbackRate: options.playbackRate,\n });\n\n const listeners = new Set<(state: GingerState) => void>();\n\n const dispatch = (action: GingerAction): void => {\n const next = gingerReducer(state, action);\n if (next !== state) {\n state = next;\n for (const listener of listeners) {\n listener(state);\n }\n }\n };\n\n const subscribe = (listener: (state: GingerState) => void): (() => void) => {\n listeners.add(listener);\n return () => listeners.delete(listener);\n };\n\n const init = (payload: GingerInitPayload): void => {\n dispatch({ type: \"INIT\", payload });\n };\n\n return {\n getState: () => state,\n dispatch,\n subscribe,\n init,\n clampVolume,\n clampPlaybackRate,\n };\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useGingerPlayback } from \"../context/GingerSplitContexts\";\nimport type { Track } from \"../types\";\n\nexport type GingerPlaybackHistoryEntry = {\n track: Track;\n /** The track's index in the queue at the time it was played. */\n index: number;\n /** Unix timestamp (ms) when the track started playing. */\n playedAt: number;\n};\n\nexport type UseGingerPlaybackHistoryOptions = {\n /** Maximum number of entries to keep. Oldest entries are dropped first. Default: 50. */\n maxLength?: number;\n};\n\nexport type UseGingerPlaybackHistoryResult = {\n /** Chronological list of played tracks; most recent entry is last. */\n history: GingerPlaybackHistoryEntry[];\n clearHistory: () => void;\n};\n\n/**\n * Records a history of played tracks in chronological order.\n * Useful for \"what was playing before\" in shuffle mode or for analytics.\n *\n * The history is stored in component state and does not survive remounts.\n * In shuffle mode, `index` reflects the position in the current shuffled queue.\n */\nexport function useGingerPlaybackHistory(\n options: UseGingerPlaybackHistoryOptions = {},\n): UseGingerPlaybackHistoryResult {\n const { maxLength = 50 } = options;\n const { tracks, currentIndex } = useGingerPlayback();\n\n const [history, setHistory] = useState<GingerPlaybackHistoryEntry[]>([]);\n const prevIndexRef = useRef<number | null>(null);\n const prevTracksRef = useRef(tracks);\n prevTracksRef.current = tracks;\n\n useEffect(() => {\n const track = tracks[currentIndex];\n if (!track) return;\n\n if (prevIndexRef.current === currentIndex) return;\n prevIndexRef.current = currentIndex;\n\n const entry: GingerPlaybackHistoryEntry = {\n track,\n index: currentIndex,\n playedAt: Date.now(),\n };\n\n setHistory((prev) => {\n const next = [...prev, entry];\n return next.length > maxLength ? next.slice(next.length - maxLength) : next;\n });\n }, [currentIndex, tracks, maxLength]);\n\n const clearHistory = useCallback(() => setHistory([]), []);\n\n return { history, clearHistory };\n}\n","import { useCallback, useRef, useState } from \"react\";\nimport { useGingerMedia } from \"../context/GingerSplitContexts\";\n\nexport type UseGingerVolumeFadeOptions = {\n /** Target volume to fade to (0–1). */\n targetVolume: number;\n /** Duration of the fade in milliseconds. */\n durationMs: number;\n /** Called when the fade completes normally (not when cancelled). */\n onComplete?: () => void;\n};\n\nexport type UseGingerVolumeFadeResult = {\n /** Start a volume fade. Cancels any in-progress fade. */\n fadeVolumeTo: (options: UseGingerVolumeFadeOptions) => void;\n /** Cancel the current fade and hold at the current volume. */\n cancelFade: () => void;\n /** True while a fade is in progress. */\n isFading: boolean;\n};\n\n/**\n * Smoothly interpolates volume over a given duration using `requestAnimationFrame`.\n * Useful for fade-in on track start, fade-out before sleep timer fires, or crossfade prep.\n */\nexport function useGingerVolumeFade(): UseGingerVolumeFadeResult {\n const { setVolume, volume } = useGingerMedia();\n const [isFading, setIsFading] = useState(false);\n\n const rafRef = useRef<number>(0);\n const cancelledRef = useRef(false);\n\n const cancelFade = useCallback(() => {\n cancelAnimationFrame(rafRef.current);\n cancelledRef.current = true;\n setIsFading(false);\n }, []);\n\n const fadeVolumeTo = useCallback(\n ({ targetVolume, durationMs, onComplete }: UseGingerVolumeFadeOptions) => {\n cancelAnimationFrame(rafRef.current);\n cancelledRef.current = false;\n\n const clamp = (v: number) => Math.min(1, Math.max(0, v));\n const target = clamp(targetVolume);\n const startTime = performance.now();\n\n // Capture start volume at the moment the fade begins\n let startVolume = volume;\n\n setIsFading(true);\n\n const tick = (now: number) => {\n if (cancelledRef.current) return;\n const elapsed = now - startTime;\n const progress = Math.min(1, elapsed / Math.max(1, durationMs));\n const current = startVolume + (target - startVolume) * progress;\n setVolume(clamp(current));\n\n if (progress < 1) {\n rafRef.current = requestAnimationFrame(tick);\n } else {\n setIsFading(false);\n onComplete?.();\n }\n };\n\n // Use a small delay so `volume` from closure is the current value\n rafRef.current = requestAnimationFrame((now) => {\n startVolume = volume;\n tick(now);\n });\n },\n [setVolume, volume],\n );\n\n return { fadeVolumeTo, cancelFade, isFading };\n}\n","import { useMemo } from \"react\";\nimport { useGingerMedia, useGingerPlayback } from \"../context/GingerSplitContexts\";\n\nexport type GingerChapterProgress = {\n /** Fraction (0–1) of the way through the active chapter. 0 when no chapter is active. */\n progress: number;\n /** Seconds elapsed since the start of the active chapter. */\n elapsed: number;\n /** Seconds remaining until the end of the active chapter (or until end of track if last chapter). */\n remaining: number;\n};\n\n/**\n * Returns detailed progress information for the currently active chapter.\n * Complements `useGingerChapters` with per-chapter playback fractions.\n */\nexport function useGingerChapterProgress(): GingerChapterProgress {\n const { tracks, currentIndex } = useGingerPlayback();\n const { currentTime, duration } = useGingerMedia();\n\n const chapters = useMemo(() => {\n const raw = tracks[currentIndex]?.chapters ?? [];\n return [...raw]\n .filter((c) => c && Number.isFinite(c.startSeconds) && c.startSeconds >= 0)\n .sort((a, b) => a.startSeconds - b.startSeconds);\n }, [tracks, currentIndex]);\n\n return useMemo<GingerChapterProgress>(() => {\n if (chapters.length === 0) return { progress: 0, elapsed: 0, remaining: 0 };\n\n // Find active chapter (last one whose start is <= currentTime)\n let activeIdx = -1;\n for (let i = chapters.length - 1; i >= 0; i--) {\n if (currentTime >= chapters[i]!.startSeconds) {\n activeIdx = i;\n break;\n }\n }\n\n if (activeIdx === -1) return { progress: 0, elapsed: 0, remaining: 0 };\n\n const chapter = chapters[activeIdx]!;\n const nextChapter = chapters[activeIdx + 1];\n const chapterEnd = nextChapter?.startSeconds ?? (duration > 0 ? duration : currentTime);\n const chapterDuration = Math.max(0, chapterEnd - chapter.startSeconds);\n const elapsed = Math.max(0, currentTime - chapter.startSeconds);\n const remaining = Math.max(0, chapterEnd - currentTime);\n const progress = chapterDuration > 0 ? Math.min(1, elapsed / chapterDuration) : 0;\n\n return { progress, elapsed, remaining };\n }, [chapters, currentTime, duration]);\n}\n"],"names":["emptyFreq","emptyTime","useGingerLiveAnalyzer","options","enabled","fftSize","smoothingTimeConstant","minDecibels","maxDecibels","audioRef","state","useGinger","opts","useMemo","frame","setFrame","useState","error","setError","isSuspended","setIsSuspended","meta","setMeta","freqRef","useRef","timeRef","resume","useCallback","ctx","contextHolderRef","analyserHolderRef","useLayoutEffect","cancelled","consumerId","element","rafId","onStateChange","runLoop","a","fq","td","n","attach","el","id","context","analyser","attachLiveAnalyser","fft","e","msg","first","retryRaf","maxAttempts","attempts","retryLoop","out","detachLiveAnalyser","_a","useGingerKeyboardShortcuts","bindings","togglePlayPause","next","prev","useGingerPlayback","toggleMute","seek","currentTime","duration","useGingerMedia","muteBinding","seekForwardBinding","seekBackwardBinding","useEffect","playPause","nextKey","prevKey","muteKey","seekFwdKey","seekBwdKey","seekSecs","onKeyDown","event","target","key","dur","useGingerSleepTimer","durationMs","stopAfterTracks","respectPause","onFire","currentIndex","pause","isPaused","remainingTracksRef","prevIndexRef","remainingMsRef","segmentStartRef","prevDurationMsRef","elapsed","useGingerDebugLog","useGingerState","prevRef","clamp01","value","useSeekDrag","media","playback","fraction","setFraction","isDragging","setIsDragging","liveFraction","progressFraction","gingerStateFromContextValues","displayFraction","onPointerDown","rect","update","clientX","ratio","onMove","moveEvent","onUp","upEvent","useNextTrackPrefetch","crossOrigin","tracks","repeatMode","playbackMode","nextIndex","computeNextIndex","nextUrl","audio","createGingerStore","createInitialState","listeners","dispatch","action","gingerReducer","listener","payload","clampVolume","clampPlaybackRate","useGingerPlaybackHistory","maxLength","history","setHistory","prevTracksRef","track","entry","clearHistory","useGingerVolumeFade","setVolume","volume","isFading","setIsFading","rafRef","cancelledRef","cancelFade","targetVolume","onComplete","clamp","v","startTime","startVolume","tick","now","progress","current","useGingerChapterProgress","chapters","c","b","activeIdx","i","chapter","nextChapter","chapterEnd","chapterDuration","remaining"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lucaismyname/ginger",
3
- "version": "0.0.40",
4
- "description": "A headless & batteries-included React audio-player primitive",
3
+ "version": "0.0.42",
4
+ "description": "A headless React audio-player primitive",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "files": [
@@ -58,6 +58,11 @@
58
58
  "types": "./dist/remote/index.d.ts",
59
59
  "import": "./dist/remote/index.js",
60
60
  "require": "./dist/remote/index.cjs"
61
+ },
62
+ "./crossfade": {
63
+ "types": "./dist/crossfade/index.d.ts",
64
+ "import": "./dist/crossfade/index.js",
65
+ "require": "./dist/crossfade/index.cjs"
61
66
  }
62
67
  },
63
68
  "scripts": {
package/CHANGELOG.md DELETED
@@ -1,18 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to `@lucaismyname/ginger` are documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
- and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ### Changed
11
-
12
- - Clarified documentation links in `README.md` so npm and GitHub readers land on valid docs.
13
-
14
- ## [0.0.40] - 2026-04-12
15
-
16
- ### Added
17
-
18
- - Latest package release.