{"version":3,"sources":["../src/hooks/useAudioTracks.ts","../src/hooks/buildTrackFromConfig.ts","../src/hooks/useAudioEffects.ts","../src/hooks/useDynamicEffects.ts","../src/effects/effectDefinitions.ts","../src/effects/effectFactory.ts","../src/effects/loadWam.ts","../src/effects/wamEffectFactory.ts","../src/effects/offlineChain.ts","../src/hooks/useTrackDynamicEffects.ts","../src/hooks/useExportWav.ts","../src/utils/renderToneOffline.ts","../src/utils/wavEncoder.ts","../src/hooks/useDynamicTracks.ts","../src/hooks/useOutputMeter.ts","../src/components/ExportControls.tsx","../src/WaveformPlaylistContext.tsx","../src/components/WamEffectGui.tsx"],"sourcesContent":["import { useState, useEffect, useRef, useMemo } from 'react';\nimport {\n  ClipTrack,\n  type Fade,\n  type TrackEffectsFunction,\n  type WaveformDataObject,\n  type RenderMode,\n  type SpectrogramConfig,\n  type ColorMapValue,\n} from '@waveform-playlist/core';\nimport * as Tone from 'tone';\nimport { buildTrackFromConfig } from './buildTrackFromConfig';\n\n/**\n * Configuration for a single audio track to load\n *\n * Audio can be provided in three ways:\n * 1. `src` - URL to fetch and decode (standard loading)\n * 2. `audioBuffer` - Pre-loaded AudioBuffer (skip fetch/decode)\n * 3. `waveformData` only - Peaks-first rendering (audio loads later)\n *\n * For peaks-first rendering, just provide `waveformData` - the sample rate\n * and duration are derived from the waveform data automatically.\n */\nexport interface AudioTrackConfig {\n  /** URL to audio file - used if audioBuffer not provided */\n  src?: string;\n  /** Pre-loaded AudioBuffer - skips fetch/decode if provided */\n  audioBuffer?: AudioBuffer;\n  name?: string;\n  muted?: boolean;\n  soloed?: boolean;\n  volume?: number;\n  pan?: number;\n  color?: string;\n  effects?: TrackEffectsFunction;\n  // Multi-clip support\n  startTime?: number; // When the clip starts on the timeline (default: 0)\n  duration?: number; // Duration of the clip (default: full audio duration)\n  offset?: number; // Offset into the source audio file (default: 0)\n  // Fade support\n  fadeIn?: Fade; // Fade in configuration\n  fadeOut?: Fade; // Fade out configuration\n  // Pre-computed waveform data (BBC audiowaveform format)\n  // For peaks-first rendering, provide this without audioBuffer/src\n  // Sample rate and duration are derived from waveformData.sample_rate and waveformData.duration\n  waveformData?: WaveformDataObject;\n  /** Visualization render mode: 'waveform' | 'spectrogram' | 'both'. Default: 'waveform' */\n  renderMode?: RenderMode;\n  /** Spectrogram configuration (FFT size, window, frequency scale, etc.) */\n  spectrogramConfig?: SpectrogramConfig;\n  /** Spectrogram color map name or custom color array */\n  spectrogramColorMap?: ColorMapValue;\n}\n\n/**\n * Options for useAudioTracks hook\n */\nexport interface UseAudioTracksOptions {\n  /**\n   * When true, all tracks render immediately as placeholders with clip geometry\n   * from the config. Audio fills in progressively as files decode, and peaks\n   * render as each buffer becomes available. Use with `deferEngineRebuild={loading}`\n   * on the provider for a single engine build when all tracks are ready.\n   *\n   * Requires `duration` or `waveformData` in each config so clip dimensions are known upfront.\n   * Default: false\n   */\n  immediate?: boolean;\n  /** @deprecated Use `immediate` instead. */\n  progressive?: boolean;\n}\n\n/**\n * Hook to load audio from URLs and convert to ClipTrack format\n *\n * This hook fetches audio files, decodes them, and creates ClipTrack objects\n * with a single clip per track. Supports custom positioning for multi-clip arrangements.\n *\n * @param configs - Array of audio track configurations\n * @param options - Optional configuration for loading behavior\n * @returns Object with tracks array, loading state, and progress info\n *\n * @example\n * ```typescript\n * // Basic usage (clips positioned at start)\n * const { tracks, loading, error } = useAudioTracks([\n *   { src: 'audio/vocals.mp3', name: 'Vocals' },\n *   { src: 'audio/drums.mp3', name: 'Drums' },\n * ]);\n *\n * // Immediate rendering with deferred engine build (recommended for multi-track)\n * const { tracks, loading } = useAudioTracks(\n *   [\n *     { src: 'audio/vocals.mp3', name: 'Vocals', duration: 30 },\n *     { src: 'audio/drums.mp3', name: 'Drums', duration: 30 },\n *   ],\n *   { immediate: true }\n * );\n * // All tracks render instantly as placeholders, peaks fill in as files load\n * return (\n *   <WaveformPlaylistProvider tracks={tracks} deferEngineRebuild={loading}>\n *     ...\n *   </WaveformPlaylistProvider>\n * );\n *\n * // Pre-loaded AudioBuffer (skip fetch/decode)\n * const { tracks } = useAudioTracks([\n *   { audioBuffer: myPreloadedBuffer, name: 'Pre-loaded' },\n * ]);\n *\n * // Peaks-first rendering (instant visual, audio loads later)\n * const { tracks } = useAudioTracks([\n *   { waveformData: preloadedPeaks, name: 'Peaks Only' },  // Renders immediately\n * ]);\n * ```\n */\nexport function useAudioTracks(configs: AudioTrackConfig[], options: UseAudioTracksOptions = {}) {\n  const { immediate = false, progressive = false } = options;\n  // progressive is a deprecated alias for immediate\n  const isImmediate = immediate || progressive;\n  const [loading, setLoading] = useState(true);\n  const [error, setError] = useState<string | null>(null);\n  const [loadedCount, setLoadedCount] = useState(0);\n\n  const totalCount = configs.length;\n\n  // For immediate mode: store loaded AudioBuffers by config index\n  const [loadedBuffers, setLoadedBuffers] = useState<Map<number, AudioBuffer>>(new Map());\n\n  // Stable track/clip IDs across rebuilds (immediate mode)\n  const stableIdsRef = useRef<Map<number, { trackId: string; clipId: string }>>(new Map());\n\n  // AudioContext sample rate, populated once loadTracks runs.\n  // Used as fallback when neither buffer nor waveformData provides a sample rate.\n  const contextSampleRateRef = useRef<number>(48000);\n\n  // For immediate mode: derive tracks from configs + loaded buffers.\n  // Runs on mount (creates placeholders) and each time a buffer loads (attaches audioBuffer).\n  const derivedTracks = useMemo(() => {\n    if (!isImmediate) return null;\n\n    const result: ClipTrack[] = [];\n    for (let i = 0; i < configs.length; i++) {\n      // A config whose decode hasn't arrived yet is an EXPECTED transient\n      // state in immediate mode — it builds on a later pass once the buffer\n      // lands. Suppress the missing-duration warning for those so each\n      // rebuild pass doesn't warn about every still-pending track (the\n      // count grows quadratically with track count otherwise). Configs with\n      // nothing left to load (no src, no buffer coming) keep the warning —\n      // for them, missing duration is a real misconfiguration.\n      const pendingDecode =\n        !loadedBuffers.has(i) && !configs[i].audioBuffer && configs[i].src != null;\n      const track = buildTrackFromConfig(\n        configs[i],\n        i,\n        loadedBuffers.get(i),\n        stableIdsRef.current,\n        contextSampleRateRef.current,\n        { suppressMissingDurationWarning: pendingDecode }\n      );\n      if (track) result.push(track);\n    }\n    return result;\n  }, [isImmediate, configs, loadedBuffers]);\n\n  // Initialize tracks with derivedTracks so immediate-mode placeholders\n  // appear on the very first render (no flash of empty content).\n  const [tracks, setTracks] = useState<ClipTrack[]>(derivedTracks ?? []);\n\n  // Sync derived tracks into state synchronously during render (not useEffect).\n  // useEffect sync causes a 1-render lag — if deferEngineRebuild flips in a\n  // separate batch, the provider sees stale tracks and rebuilds the engine twice.\n  const prevDerivedRef = useRef(derivedTracks);\n  if (derivedTracks !== prevDerivedRef.current) {\n    prevDerivedRef.current = derivedTracks;\n    if (derivedTracks) setTracks(derivedTracks);\n  }\n\n  useEffect(() => {\n    if (configs.length === 0) {\n      setTracks([]);\n      setLoading(false);\n      setLoadedCount(0);\n      return;\n    }\n\n    let cancelled = false;\n    const abortController = new AbortController();\n\n    const loadTracks = async () => {\n      try {\n        setLoading(true);\n        setError(null);\n        setLoadedCount(0);\n\n        if (isImmediate) {\n          setLoadedBuffers(new Map());\n        }\n\n        const audioContext = Tone.getContext().rawContext as AudioContext;\n        contextSampleRateRef.current = audioContext.sampleRate;\n\n        // Process each config\n        const loadPromises = configs.map(async (config, index) => {\n          // Case 1: Already have audioBuffer - no loading needed\n          if (config.audioBuffer) {\n            if (isImmediate && !cancelled) {\n              setLoadedBuffers((prev) => {\n                const next = new Map(prev);\n                next.set(index, config.audioBuffer!);\n                return next;\n              });\n              setLoadedCount((prev) => prev + 1);\n              return;\n            }\n\n            return buildTrackFromConfig(\n              config,\n              index,\n              config.audioBuffer,\n              stableIdsRef.current,\n              audioContext.sampleRate\n            );\n          }\n\n          // Case 2: Have waveformData but no src - peaks-only (no audio to load)\n          if (!config.src && config.waveformData) {\n            if (isImmediate && !cancelled) {\n              // No buffer to load — track is already created from waveformData\n              setLoadedCount((prev) => prev + 1);\n              return;\n            }\n\n            return buildTrackFromConfig(\n              config,\n              index,\n              undefined,\n              stableIdsRef.current,\n              audioContext.sampleRate\n            );\n          }\n\n          // Case 3: Need to fetch and decode audio from src\n          if (!config.src) {\n            throw new Error(`Track ${index + 1}: Must provide src, audioBuffer, or waveformData`);\n          }\n\n          const response = await fetch(config.src, { signal: abortController.signal });\n          if (!response.ok) {\n            throw new Error(`Failed to fetch ${config.src}: ${response.statusText}`);\n          }\n\n          const arrayBuffer = await response.arrayBuffer();\n          const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n\n          // Validate audioBuffer\n          if (!audioBuffer || !audioBuffer.sampleRate || !audioBuffer.duration) {\n            throw new Error(`Invalid audio buffer for ${config.src}`);\n          }\n\n          if (isImmediate && !cancelled) {\n            // Store buffer — useMemo will derive the updated track\n            setLoadedBuffers((prev) => {\n              const next = new Map(prev);\n              next.set(index, audioBuffer);\n              return next;\n            });\n            setLoadedCount((prev) => prev + 1);\n            return;\n          }\n\n          return buildTrackFromConfig(\n            config,\n            index,\n            audioBuffer,\n            stableIdsRef.current,\n            audioContext.sampleRate\n          );\n        });\n\n        const loadedTracks = await Promise.all(loadPromises);\n\n        if (!cancelled) {\n          // For non-immediate mode: set all tracks at once\n          if (!isImmediate) {\n            const validTracks = loadedTracks.filter((t): t is ClipTrack => t != null);\n            setTracks(validTracks);\n            setLoadedCount(validTracks.length);\n          }\n          setLoading(false);\n        }\n      } catch (err) {\n        if (!cancelled) {\n          const errorMessage = err instanceof Error ? err.message : 'Unknown error loading audio';\n          setError(errorMessage);\n          setLoading(false);\n          console.error(`[waveform-playlist] Error loading audio tracks: ${errorMessage}`);\n        }\n      }\n    };\n\n    loadTracks();\n\n    // Cleanup: prevent state updates and abort in-flight fetches on unmount\n    return () => {\n      cancelled = true;\n      abortController.abort();\n    };\n  }, [configs, isImmediate]);\n\n  return { tracks, loading, error, loadedCount, totalCount };\n}\n","import { ClipTrack, createTrack, createClipFromSeconds } from '@waveform-playlist/core';\nimport type { AudioTrackConfig } from './useAudioTracks';\n\nexport interface BuildTrackOptions {\n  /**\n   * Suppress the \"Cannot create track\" warning when the duration is\n   * underivable. Immediate mode passes true for configs still awaiting their\n   * decode — a missing duration there is an expected transient state (the\n   * track builds on the next pass once the buffer arrives), and warning on\n   * every rebuild pass floods the console (#620 follow-up). Terminal call\n   * sites (non-immediate mode, or configs with nothing left to load) keep\n   * the warning.\n   */\n  suppressMissingDurationWarning?: boolean;\n}\n\n/** Build a ClipTrack from config + optional audioBuffer, preserving stable IDs. */\nexport function buildTrackFromConfig(\n  config: AudioTrackConfig,\n  index: number,\n  audioBuffer: AudioBuffer | undefined,\n  stableIds: Map<number, { trackId: string; clipId: string }>,\n  contextSampleRate: number = 48000,\n  options: BuildTrackOptions = {}\n): ClipTrack | null {\n  const buffer = audioBuffer ?? config.audioBuffer;\n\n  // Determine if we have enough info to create the track\n  // Prefer buffer/waveformData sample rate; fall back to the AudioContext's rate\n  const sampleRate = buffer?.sampleRate ?? config.waveformData?.sample_rate ?? contextSampleRate;\n  const sourceDuration =\n    buffer?.duration ??\n    config.waveformData?.duration ??\n    (config.duration != null ? config.duration + (config.offset ?? 0) : undefined);\n\n  if (sourceDuration === undefined) {\n    if (!options.suppressMissingDurationWarning) {\n      console.warn(\n        `[waveform-playlist] Track ${index + 1} (\"${config.name ?? 'unnamed'}\"): ` +\n          `Cannot create track — provide duration, audioBuffer, or waveformData with duration.`\n      );\n    }\n    return null;\n  }\n\n  const clip = createClipFromSeconds({\n    audioBuffer: buffer,\n    sampleRate,\n    sourceDuration,\n    startTime: config.startTime ?? 0,\n    duration: config.duration ?? sourceDuration,\n    offset: config.offset ?? 0,\n    name: config.name || `Track ${index + 1}`,\n    fadeIn: config.fadeIn,\n    fadeOut: config.fadeOut,\n    waveformData: config.waveformData,\n  });\n\n  // Validate clip values\n  if (isNaN(clip.startSample) || isNaN(clip.durationSamples) || isNaN(clip.offsetSamples)) {\n    console.error(\n      `[waveform-playlist] Invalid clip values for track ${index + 1} (\"${config.name ?? 'unnamed'}\"): ` +\n        `startSample=${clip.startSample}, durationSamples=${clip.durationSamples}, offsetSamples=${clip.offsetSamples}`\n    );\n    return null;\n  }\n\n  const track: ClipTrack = {\n    ...createTrack({\n      name: config.name || `Track ${index + 1}`,\n      clips: [clip],\n      muted: config.muted ?? false,\n      soloed: config.soloed ?? false,\n      volume: config.volume ?? 1.0,\n      pan: config.pan ?? 0,\n      color: config.color,\n    }),\n    effects: config.effects,\n    renderMode: config.renderMode,\n    spectrogramConfig: config.spectrogramConfig,\n    spectrogramColorMap: config.spectrogramColorMap,\n  };\n\n  // Preserve stable IDs across rebuilds so React doesn't unmount/remount tracks\n  const existingIds = stableIds.get(index);\n  if (existingIds) {\n    track.id = existingIds.trackId;\n    track.clips[0] = { ...track.clips[0], id: existingIds.clipId };\n  } else {\n    stableIds.set(index, { trackId: track.id, clipId: track.clips[0].id });\n  }\n\n  return track;\n}\n","import { useRef, useCallback } from 'react';\nimport type { EffectsFunction } from '@waveform-playlist/playout';\n// Import Tone.js classes directly for tree-shaking\nimport { Analyser } from 'tone';\n\n/**\n * Hook for master effects with frequency analyzer\n * Returns the analyser ref and the effects function to pass to WaveformPlaylistProvider\n *\n * For more advanced effects (reverb, delay, filters, etc.), use useDynamicEffects instead.\n */\nexport const useMasterAnalyser = (fftSize: number = 256) => {\n  const analyserRef = useRef<Analyser | null>(null);\n\n  const masterEffects: EffectsFunction = useCallback(\n    (masterGainNode, destination, _isOffline) => {\n      // Create analyser and connect it in parallel to monitor the output\n      const analyserNode = new Analyser('fft', fftSize);\n      masterGainNode.connect(analyserNode);\n\n      // Connect master to destination as normal\n      masterGainNode.connect(destination);\n\n      // Store analyser for visualization\n      analyserRef.current = analyserNode;\n\n      return function cleanup() {\n        // Cleanup when playlist is destroyed\n        analyserNode.dispose();\n        analyserRef.current = null;\n      };\n    },\n    [fftSize]\n  );\n\n  return { analyserRef, masterEffects };\n};\n","import { useState, useCallback, useRef, useEffect } from 'react';\nimport {\n  isNativeGlobalContext,\n  getGlobalAudioContext,\n  type EffectsFunction,\n} from '@waveform-playlist/playout';\nimport {\n  effectDefinitions,\n  getEffectDefinition,\n  type EffectDefinition,\n} from '../effects/effectDefinitions';\nimport { createEffectInstance, type EffectInstance } from '../effects/effectFactory';\nimport { loadWamModule } from '../effects/loadWam';\nimport { createWamEffectInstance, type WamEffectInstance } from '../effects/wamEffectFactory';\nimport { buildOfflineChain, connectOfflineChain } from '../effects/offlineChain';\nimport type { OfflineEffectsFunction } from './useExportWav';\nimport type { WamPluginInstance } from '@dawcore/wam'; // type-only — erased at runtime\nimport { Analyser, Volume, ToneAudioNode, connect } from 'tone';\n\nexport interface ActiveEffect {\n  instanceId: string;\n  effectId: string;\n  /** 'native' = built-in Tone effect; 'wam' = hosted WAM plugin. */\n  kind: 'native' | 'wam';\n  /** Module URL for wam entries. */\n  url?: string;\n  definition: EffectDefinition;\n  params: Record<string, number | string | boolean>;\n  bypassed: boolean;\n}\n\nexport interface UseDynamicEffectsReturn {\n  // State\n  activeEffects: ActiveEffect[];\n  availableEffects: EffectDefinition[];\n\n  // Actions\n  addEffect: (effectId: string) => void;\n  /**\n   * Hosts a WAM plugin from a module URL and appends it to the master chain.\n   * Requires native-context mode — call configureGlobalContext({ nativeAudioContext: true })\n   * from @waveform-playlist/playout before any audio initialization.\n   * WAM entries render in offline WAV export (re-instantiated on the offline context).\n   * Resolves with the new entry's instanceId.\n   */\n  addWamEffect: (url: string, initialState?: unknown) => Promise<string>;\n  /** Live plugin handle for a hosted WAM entry (for GUI mounting via WamEffectGui). */\n  getWamPlugin: (instanceId: string) => WamPluginInstance | undefined;\n  removeEffect: (instanceId: string) => void;\n  updateParameter: (\n    instanceId: string,\n    paramName: string,\n    value: number | string | boolean\n  ) => void;\n  toggleBypass: (instanceId: string) => void;\n  reorderEffects: (fromIndex: number, toIndex: number) => void;\n  clearAllEffects: () => void;\n\n  // For connecting to audio graph\n  masterEffects: EffectsFunction;\n\n  /**\n   * Creates a fresh effects function for offline rendering. Native effects are\n   * re-created on the offline context; WAM entries are re-instantiated from\n   * their URL-cached factories with the live instance's state transferred.\n   * The returned function may be async and may reject — a WAV export never\n   * silently renders without an effect the live chain has.\n   */\n  createOfflineEffectsFunction: () => OfflineEffectsFunction | undefined;\n\n  // Analyser for visualization\n  analyserRef: React.RefObject<Analyser | null>;\n}\n\n/**\n * Hook for managing a dynamic chain of audio effects with real-time parameter updates\n */\nexport function useDynamicEffects(fftSize: number = 256): UseDynamicEffectsReturn {\n  // Track active effects in state (for UI)\n  const [activeEffects, setActiveEffects] = useState<ActiveEffect[]>([]);\n\n  // Ref to store current activeEffects for reading in callbacks (avoids stale closures)\n  const activeEffectsRef = useRef<ActiveEffect[]>(activeEffects);\n  activeEffectsRef.current = activeEffects;\n\n  // Track effect instances (for audio processing)\n  const effectInstancesRef = useRef<Map<string, EffectInstance>>(new Map());\n\n  // Guards addWamEffect's async window — set false by the unmount cleanup.\n  const isMountedRef = useRef(true);\n\n  // Analyser for visualization\n  const analyserRef = useRef<Analyser | null>(null);\n\n  // Reference to the current audio graph nodes\n  const graphNodesRef = useRef<{\n    masterGainNode: Volume;\n    destination: ToneAudioNode;\n    analyserNode: Analyser;\n  } | null>(null);\n\n  // Rebuild the effect chain when effects change\n  // Note: effects is passed as parameter to avoid stale closure issues\n  const rebuildChain = useCallback((effects: ActiveEffect[]) => {\n    const nodes = graphNodesRef.current;\n    if (!nodes) return;\n\n    const { masterGainNode, destination, analyserNode } = nodes;\n\n    // Disconnect everything first\n    try {\n      masterGainNode.disconnect();\n    } catch (e) {\n      console.warn('[waveform-playlist] Error disconnecting master effects chain:', e);\n    }\n\n    // Get effect instances in order. Bypassed WAM entries are dropped from the\n    // chain entirely (disconnection bypass — WAM has no wet param).\n    const audible = effects.filter((ae) => !(ae.kind === 'wam' && ae.bypassed));\n    const instances = audible\n      .map((ae) => effectInstancesRef.current.get(ae.instanceId))\n      .filter((inst): inst is EffectInstance => inst !== undefined);\n\n    if (instances.length === 0) {\n      // No effects - connect directly to analyser -> destination\n      masterGainNode.connect(analyserNode);\n      analyserNode.connect(destination);\n    } else {\n      // Connect: masterGain -> effect1 -> effect2 -> ... -> analyser -> destination\n      let currentNode: ToneAudioNode | AudioNode = masterGainNode;\n\n      instances.forEach((inst) => {\n        try {\n          inst.disconnect();\n        } catch (e) {\n          console.warn(`[waveform-playlist] Error disconnecting effect \"${inst.id}\":`, e);\n        }\n        connect(currentNode, inst.effect);\n        currentNode = inst.effect;\n      });\n\n      // Connect last effect to analyser\n      connect(currentNode, analyserNode);\n      analyserNode.connect(destination);\n    }\n  }, []);\n\n  // Add a new effect\n  const addEffect = useCallback((effectId: string) => {\n    const definition = getEffectDefinition(effectId);\n    if (!definition) {\n      console.error(`Unknown effect: ${effectId}`);\n      return;\n    }\n\n    // Build default params\n    const params: Record<string, number | string | boolean> = {};\n    definition.parameters.forEach((p) => {\n      params[p.name] = p.default;\n    });\n\n    // Create the effect instance\n    const instance = createEffectInstance(definition, params);\n    effectInstancesRef.current.set(instance.instanceId, instance);\n\n    // Add to state\n    const newActiveEffect: ActiveEffect = {\n      instanceId: instance.instanceId,\n      effectId: definition.id,\n      kind: 'native',\n      definition,\n      params,\n      bypassed: false,\n    };\n\n    setActiveEffects((prev) => [...prev, newActiveEffect]);\n  }, []);\n\n  // Host a WAM plugin and append it to the chain. Requires native-context mode.\n  const addWamEffect = useCallback(async (url: string, initialState?: unknown): Promise<string> => {\n    if (!isNativeGlobalContext()) {\n      throw new Error(\n        '[waveform-playlist] WAM plugins require a native AudioContext. Call ' +\n          'configureGlobalContext({ nativeAudioContext: true }) from ' +\n          '@waveform-playlist/playout before any audio initialization.'\n      );\n    }\n    const wam = await loadWamModule();\n    const ctx = getGlobalAudioContext();\n    const { hostGroupId } = await wam.ensureWamHost(ctx);\n    const plugin = await wam.createWamInstance(\n      url,\n      ctx,\n      hostGroupId,\n      initialState !== undefined ? { initialState } : undefined\n    );\n    if (!isMountedRef.current) {\n      try {\n        plugin.destroy();\n      } catch (err) {\n        console.warn(\n          '[waveform-playlist] Error destroying WAM plugin after unmount: ' +\n            (err instanceof Error ? err.message : String(err))\n        );\n      }\n      throw new Error(\n        '[waveform-playlist] addWamEffect aborted: hook unmounted before the plugin finished loading.'\n      );\n    }\n    const instance = createWamEffectInstance(plugin);\n    effectInstancesRef.current.set(instance.instanceId, instance);\n\n    const definition: EffectDefinition = {\n      id: instance.id,\n      name: plugin.descriptor?.name ?? url,\n      category: 'wam',\n      description: 'WAM plugin',\n      parameters: [],\n    };\n    setActiveEffects((prev) => [\n      ...prev,\n      {\n        instanceId: instance.instanceId,\n        effectId: instance.id,\n        kind: 'wam',\n        url,\n        definition,\n        params: {},\n        bypassed: false,\n      },\n    ]);\n    return instance.instanceId;\n  }, []);\n\n  // Live plugin handle for GUI mounting (WamEffectGui).\n  const getWamPlugin = useCallback((instanceId: string): WamPluginInstance | undefined => {\n    const inst = effectInstancesRef.current.get(instanceId) as WamEffectInstance | undefined;\n    return inst?.kind === 'wam' ? inst.plugin : undefined;\n  }, []);\n\n  // Remove an effect\n  const removeEffect = useCallback((instanceId: string) => {\n    const instance = effectInstancesRef.current.get(instanceId);\n    if (instance) {\n      instance.dispose();\n      effectInstancesRef.current.delete(instanceId);\n    }\n\n    setActiveEffects((prev) => prev.filter((e) => e.instanceId !== instanceId));\n  }, []);\n\n  // Update a parameter in real-time\n  const updateParameter = useCallback(\n    (instanceId: string, paramName: string, value: number | string | boolean) => {\n      // Update the actual effect instance\n      const instance = effectInstancesRef.current.get(instanceId);\n      if (instance) {\n        instance.setParameter(paramName, value);\n      }\n\n      // Update state for UI\n      setActiveEffects((prev) =>\n        prev.map((e) =>\n          e.instanceId === instanceId ? { ...e, params: { ...e.params, [paramName]: value } } : e\n        )\n      );\n    },\n    []\n  );\n\n  // Toggle bypass for an effect (uses wet parameter - 0 = bypass, restore original for active)\n  const toggleBypass = useCallback((instanceId: string) => {\n    // Get current state from ref to determine new bypassed value (avoids stale closure)\n    const effect = activeEffectsRef.current.find((e) => e.instanceId === instanceId);\n    if (!effect) return;\n\n    const newBypassed = !effect.bypassed;\n\n    if (effect.kind === 'wam') {\n      // Disconnection bypass: the rebuild effect drops bypassed wam entries.\n      setActiveEffects((prev) =>\n        prev.map((e) => (e.instanceId === instanceId ? { ...e, bypassed: newBypassed } : e))\n      );\n      return;\n    }\n\n    // Update the actual effect instance\n    // When bypassing: set wet to 0\n    // When un-bypassing: restore the original wet value from params\n    const instance = effectInstancesRef.current.get(instanceId);\n    if (instance) {\n      const originalWet = (effect.params.wet as number) ?? 1;\n      instance.setParameter('wet', newBypassed ? 0 : originalWet);\n    }\n\n    // Update state for UI\n    setActiveEffects((prev) =>\n      prev.map((e) => (e.instanceId === instanceId ? { ...e, bypassed: newBypassed } : e))\n    );\n  }, []);\n\n  // Reorder effects in the chain\n  const reorderEffects = useCallback((fromIndex: number, toIndex: number) => {\n    setActiveEffects((prev) => {\n      const newEffects = [...prev];\n      const [removed] = newEffects.splice(fromIndex, 1);\n      newEffects.splice(toIndex, 0, removed);\n      return newEffects;\n    });\n  }, []);\n\n  // Clear all effects\n  const clearAllEffects = useCallback(() => {\n    // Dispose all instances\n    effectInstancesRef.current.forEach((inst) => inst.dispose());\n    effectInstancesRef.current.clear();\n\n    setActiveEffects([]);\n  }, []);\n\n  // Rebuild chain when effects change\n  useEffect(() => {\n    rebuildChain(activeEffects);\n  }, [activeEffects, rebuildChain]);\n\n  // The effects function that gets passed to WaveformPlaylistProvider\n  // This function is stable - it reads from refs at call time to avoid stale closures\n  const masterEffects: EffectsFunction = useCallback(\n    (masterGainNode, destination, _isOffline) => {\n      // Create analyser for visualization\n      const analyserNode = new Analyser('fft', fftSize);\n      analyserRef.current = analyserNode;\n\n      // Store references for rebuilding chain\n      graphNodesRef.current = {\n        masterGainNode,\n        destination,\n        analyserNode,\n      };\n\n      // Build initial chain - read from ref to get current state.\n      // Bypassed WAM entries are dropped (disconnection bypass — no wet param).\n      const effects = activeEffectsRef.current;\n      const audible = effects.filter((ae) => !(ae.kind === 'wam' && ae.bypassed));\n      const instances = audible\n        .map((ae) => effectInstancesRef.current.get(ae.instanceId))\n        .filter((inst): inst is EffectInstance => inst !== undefined);\n\n      if (instances.length === 0) {\n        // No effects - connect directly to analyser -> destination\n        masterGainNode.connect(analyserNode);\n        analyserNode.connect(destination);\n      } else {\n        // Connect: masterGain -> effect1 -> effect2 -> ... -> analyser -> destination\n        let currentNode: ToneAudioNode | AudioNode = masterGainNode;\n\n        instances.forEach((inst) => {\n          connect(currentNode, inst.effect);\n          currentNode = inst.effect;\n        });\n\n        // Connect last effect to analyser\n        connect(currentNode, analyserNode);\n        analyserNode.connect(destination);\n      }\n\n      return function cleanup() {\n        analyserNode.dispose();\n        analyserRef.current = null;\n        graphNodesRef.current = null;\n      };\n    },\n    [fftSize] // Only fftSize - reads effects from ref\n  );\n\n  // Cleanup on unmount\n  useEffect(() => {\n    isMountedRef.current = true;\n    const effectInstances = effectInstancesRef.current;\n    return () => {\n      isMountedRef.current = false;\n      effectInstances.forEach((inst) => inst.dispose());\n      effectInstances.clear();\n    };\n  }, []);\n\n  /**\n   * Creates a fresh effects function for offline rendering. Native effects\n   * are re-created on the offline context; WAM entries are re-instantiated\n   * from the live instance's state (cloneInstanceInto), asynchronously.\n   */\n  const createOfflineEffectsFunction = useCallback((): OfflineEffectsFunction | undefined => {\n    // Bypassed entries are excluded offline: natives keep the existing\n    // exclusion; WAM entries use disconnection bypass (parity with live).\n    const nonBypassed = activeEffects.filter((e) => !e.bypassed);\n    if (nonBypassed.length === 0) {\n      return undefined;\n    }\n    const hasWam = nonBypassed.some((e) => e.kind === 'wam');\n\n    return async (masterGainNode: Volume, destination: ToneAudioNode, _isOffline: boolean) => {\n      if (hasWam && !isNativeGlobalContext()) {\n        throw new Error(\n          '[waveform-playlist] WAV export with WAM effects requires a native AudioContext. ' +\n            'Call configureGlobalContext({ nativeAudioContext: true }) from ' +\n            '@waveform-playlist/playout before any audio initialization.'\n        );\n      }\n      // Tone nodes inside the offline build are created on the current\n      // (offline) global context — its rawContext must host the WAM clones.\n      const rawContext = masterGainNode.context.rawContext as unknown as BaseAudioContext;\n      const { instances, dispose } = await buildOfflineChain(\n        nonBypassed,\n        (instanceId) => {\n          const inst = effectInstancesRef.current.get(instanceId) as WamEffectInstance | undefined;\n          return inst?.kind === 'wam' ? inst.plugin : undefined;\n        },\n        rawContext\n      );\n      connectOfflineChain(masterGainNode, instances, destination);\n      return dispose;\n    };\n  }, [activeEffects]);\n\n  return {\n    activeEffects,\n    availableEffects: effectDefinitions,\n    addEffect,\n    addWamEffect,\n    getWamPlugin,\n    removeEffect,\n    updateParameter,\n    toggleBypass,\n    reorderEffects,\n    clearAllEffects,\n    masterEffects,\n    createOfflineEffectsFunction,\n    analyserRef,\n  };\n}\n","/**\n * Effect definitions for all available Tone.js effects\n * Each effect has parameters with min/max/default values for UI controls\n */\n\nexport type ParameterType = 'number' | 'select' | 'boolean';\n\nexport interface EffectParameter {\n  name: string;\n  label: string;\n  type: ParameterType;\n  min?: number;\n  max?: number;\n  step?: number;\n  default: number | string | boolean;\n  unit?: string;\n  options?: { value: string | number; label: string }[];\n}\n\nexport interface EffectDefinition {\n  id: string;\n  name: string;\n  category:\n    | 'delay'\n    | 'reverb'\n    | 'modulation'\n    | 'distortion'\n    | 'filter'\n    | 'dynamics'\n    | 'spatial'\n    | 'wam';\n  description: string;\n  parameters: EffectParameter[];\n}\n\nexport const effectDefinitions: EffectDefinition[] = [\n  // === REVERB EFFECTS ===\n  {\n    id: 'reverb',\n    name: 'Reverb',\n    category: 'reverb',\n    description: 'Simple convolution reverb with adjustable decay time',\n    parameters: [\n      {\n        name: 'decay',\n        label: 'Decay',\n        type: 'number',\n        min: 0.1,\n        max: 10,\n        step: 0.1,\n        default: 1.5,\n        unit: 's',\n      },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n    ],\n  },\n  {\n    id: 'freeverb',\n    name: 'Freeverb',\n    category: 'reverb',\n    description: 'Classic Schroeder/Moorer reverb with room size and dampening',\n    parameters: [\n      {\n        name: 'roomSize',\n        label: 'Room Size',\n        type: 'number',\n        min: 0,\n        max: 1,\n        step: 0.01,\n        default: 0.7,\n      },\n      {\n        name: 'dampening',\n        label: 'Dampening',\n        type: 'number',\n        min: 0,\n        max: 10000,\n        step: 100,\n        default: 3000,\n        unit: 'Hz',\n      },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n    ],\n  },\n  {\n    id: 'jcReverb',\n    name: 'JC Reverb',\n    category: 'reverb',\n    description: 'Attempt at Roland JC-120 chorus reverb emulation',\n    parameters: [\n      {\n        name: 'roomSize',\n        label: 'Room Size',\n        type: 'number',\n        min: 0,\n        max: 1,\n        step: 0.01,\n        default: 0.5,\n      },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n    ],\n  },\n\n  // === DELAY EFFECTS ===\n  {\n    id: 'feedbackDelay',\n    name: 'Feedback Delay',\n    category: 'delay',\n    description: 'Delay line with feedback for echo effects',\n    parameters: [\n      {\n        name: 'delayTime',\n        label: 'Delay Time',\n        type: 'number',\n        min: 0,\n        max: 1,\n        step: 0.01,\n        default: 0.25,\n        unit: 's',\n      },\n      {\n        name: 'feedback',\n        label: 'Feedback',\n        type: 'number',\n        min: 0,\n        max: 0.95,\n        step: 0.01,\n        default: 0.5,\n      },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n    ],\n  },\n  {\n    id: 'pingPongDelay',\n    name: 'Ping Pong Delay',\n    category: 'delay',\n    description: 'Stereo delay bouncing between left and right channels',\n    parameters: [\n      {\n        name: 'delayTime',\n        label: 'Delay Time',\n        type: 'number',\n        min: 0,\n        max: 1,\n        step: 0.01,\n        default: 0.25,\n        unit: 's',\n      },\n      {\n        name: 'feedback',\n        label: 'Feedback',\n        type: 'number',\n        min: 0,\n        max: 0.95,\n        step: 0.01,\n        default: 0.5,\n      },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n    ],\n  },\n\n  // === MODULATION EFFECTS ===\n  {\n    id: 'chorus',\n    name: 'Chorus',\n    category: 'modulation',\n    description: 'Creates thickness by layering detuned copies of the signal',\n    parameters: [\n      {\n        name: 'frequency',\n        label: 'Rate',\n        type: 'number',\n        min: 0.1,\n        max: 10,\n        step: 0.1,\n        default: 1.5,\n        unit: 'Hz',\n      },\n      {\n        name: 'delayTime',\n        label: 'Delay',\n        type: 'number',\n        min: 0,\n        max: 20,\n        step: 0.5,\n        default: 3.5,\n        unit: 'ms',\n      },\n      { name: 'depth', label: 'Depth', type: 'number', min: 0, max: 1, step: 0.01, default: 0.7 },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n    ],\n  },\n  {\n    id: 'phaser',\n    name: 'Phaser',\n    category: 'modulation',\n    description: 'Classic phaser effect using allpass filters',\n    parameters: [\n      {\n        name: 'frequency',\n        label: 'Rate',\n        type: 'number',\n        min: 0.1,\n        max: 10,\n        step: 0.1,\n        default: 0.5,\n        unit: 'Hz',\n      },\n      { name: 'octaves', label: 'Octaves', type: 'number', min: 1, max: 6, step: 1, default: 3 },\n      {\n        name: 'baseFrequency',\n        label: 'Base Freq',\n        type: 'number',\n        min: 100,\n        max: 2000,\n        step: 10,\n        default: 350,\n        unit: 'Hz',\n      },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n    ],\n  },\n  {\n    id: 'tremolo',\n    name: 'Tremolo',\n    category: 'modulation',\n    description: 'Rhythmic volume modulation',\n    parameters: [\n      {\n        name: 'frequency',\n        label: 'Rate',\n        type: 'number',\n        min: 0.1,\n        max: 20,\n        step: 0.1,\n        default: 4,\n        unit: 'Hz',\n      },\n      { name: 'depth', label: 'Depth', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n    ],\n  },\n  {\n    id: 'vibrato',\n    name: 'Vibrato',\n    category: 'modulation',\n    description: 'Pitch modulation effect',\n    parameters: [\n      {\n        name: 'frequency',\n        label: 'Rate',\n        type: 'number',\n        min: 0.1,\n        max: 20,\n        step: 0.1,\n        default: 5,\n        unit: 'Hz',\n      },\n      { name: 'depth', label: 'Depth', type: 'number', min: 0, max: 1, step: 0.01, default: 0.1 },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n    ],\n  },\n  {\n    id: 'autoPanner',\n    name: 'Auto Panner',\n    category: 'modulation',\n    description: 'Automatic left-right panning',\n    parameters: [\n      {\n        name: 'frequency',\n        label: 'Rate',\n        type: 'number',\n        min: 0.1,\n        max: 10,\n        step: 0.1,\n        default: 1,\n        unit: 'Hz',\n      },\n      { name: 'depth', label: 'Depth', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n    ],\n  },\n\n  // === FILTER EFFECTS ===\n  {\n    id: 'autoFilter',\n    name: 'Auto Filter',\n    category: 'filter',\n    description: 'Automated filter sweep with LFO',\n    parameters: [\n      {\n        name: 'frequency',\n        label: 'Rate',\n        type: 'number',\n        min: 0.1,\n        max: 10,\n        step: 0.1,\n        default: 1,\n        unit: 'Hz',\n      },\n      {\n        name: 'baseFrequency',\n        label: 'Base Freq',\n        type: 'number',\n        min: 20,\n        max: 2000,\n        step: 10,\n        default: 200,\n        unit: 'Hz',\n      },\n      {\n        name: 'octaves',\n        label: 'Octaves',\n        type: 'number',\n        min: 0.5,\n        max: 8,\n        step: 0.5,\n        default: 2.6,\n      },\n      { name: 'depth', label: 'Depth', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n    ],\n  },\n  {\n    id: 'autoWah',\n    name: 'Auto Wah',\n    category: 'filter',\n    description: 'Envelope follower filter effect',\n    parameters: [\n      {\n        name: 'baseFrequency',\n        label: 'Base Freq',\n        type: 'number',\n        min: 20,\n        max: 500,\n        step: 10,\n        default: 100,\n        unit: 'Hz',\n      },\n      { name: 'octaves', label: 'Octaves', type: 'number', min: 1, max: 8, step: 1, default: 6 },\n      {\n        name: 'sensitivity',\n        label: 'Sensitivity',\n        type: 'number',\n        min: -40,\n        max: 0,\n        step: 1,\n        default: 0,\n        unit: 'dB',\n      },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n    ],\n  },\n  {\n    id: 'eq3',\n    name: '3-Band EQ',\n    category: 'filter',\n    description: 'Three band equalizer with low, mid, and high controls',\n    parameters: [\n      {\n        name: 'low',\n        label: 'Low',\n        type: 'number',\n        min: -24,\n        max: 24,\n        step: 0.5,\n        default: 0,\n        unit: 'dB',\n      },\n      {\n        name: 'mid',\n        label: 'Mid',\n        type: 'number',\n        min: -24,\n        max: 24,\n        step: 0.5,\n        default: 0,\n        unit: 'dB',\n      },\n      {\n        name: 'high',\n        label: 'High',\n        type: 'number',\n        min: -24,\n        max: 24,\n        step: 0.5,\n        default: 0,\n        unit: 'dB',\n      },\n      {\n        name: 'lowFrequency',\n        label: 'Low Freq',\n        type: 'number',\n        min: 20,\n        max: 500,\n        step: 10,\n        default: 400,\n        unit: 'Hz',\n      },\n      {\n        name: 'highFrequency',\n        label: 'High Freq',\n        type: 'number',\n        min: 1000,\n        max: 10000,\n        step: 100,\n        default: 2500,\n        unit: 'Hz',\n      },\n    ],\n  },\n\n  // === DISTORTION EFFECTS ===\n  {\n    id: 'distortion',\n    name: 'Distortion',\n    category: 'distortion',\n    description: 'Wave shaping distortion effect',\n    parameters: [\n      {\n        name: 'distortion',\n        label: 'Drive',\n        type: 'number',\n        min: 0,\n        max: 1,\n        step: 0.01,\n        default: 0.4,\n      },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n    ],\n  },\n  {\n    id: 'bitCrusher',\n    name: 'Bit Crusher',\n    category: 'distortion',\n    description: 'Reduces bit depth for lo-fi digital texture',\n    parameters: [\n      { name: 'bits', label: 'Bits', type: 'number', min: 1, max: 16, step: 1, default: 4 },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n    ],\n  },\n  {\n    id: 'chebyshev',\n    name: 'Chebyshev',\n    category: 'distortion',\n    description: 'Waveshaping distortion using Chebyshev polynomials',\n    parameters: [\n      { name: 'order', label: 'Order', type: 'number', min: 1, max: 100, step: 1, default: 50 },\n      { name: 'wet', label: 'Mix', type: 'number', min: 0, max: 1, step: 0.01, default: 1 },\n    ],\n  },\n\n  // === DYNAMICS EFFECTS ===\n  {\n    id: 'compressor',\n    name: 'Compressor',\n    category: 'dynamics',\n    description: 'Dynamic range compressor',\n    parameters: [\n      {\n        name: 'threshold',\n        label: 'Threshold',\n        type: 'number',\n        min: -60,\n        max: 0,\n        step: 1,\n        default: -24,\n        unit: 'dB',\n      },\n      { name: 'ratio', label: 'Ratio', type: 'number', min: 1, max: 20, step: 0.5, default: 4 },\n      {\n        name: 'attack',\n        label: 'Attack',\n        type: 'number',\n        min: 0,\n        max: 1,\n        step: 0.001,\n        default: 0.003,\n        unit: 's',\n      },\n      {\n        name: 'release',\n        label: 'Release',\n        type: 'number',\n        min: 0,\n        max: 1,\n        step: 0.01,\n        default: 0.25,\n        unit: 's',\n      },\n      {\n        name: 'knee',\n        label: 'Knee',\n        type: 'number',\n        min: 0,\n        max: 40,\n        step: 1,\n        default: 30,\n        unit: 'dB',\n      },\n    ],\n  },\n  {\n    id: 'limiter',\n    name: 'Limiter',\n    category: 'dynamics',\n    description: 'Hard limiter to prevent clipping',\n    parameters: [\n      {\n        name: 'threshold',\n        label: 'Threshold',\n        type: 'number',\n        min: -12,\n        max: 0,\n        step: 0.5,\n        default: -6,\n        unit: 'dB',\n      },\n    ],\n  },\n  {\n    id: 'gate',\n    name: 'Gate',\n    category: 'dynamics',\n    description: 'Noise gate to silence signal below threshold',\n    parameters: [\n      {\n        name: 'threshold',\n        label: 'Threshold',\n        type: 'number',\n        min: -100,\n        max: 0,\n        step: 1,\n        default: -40,\n        unit: 'dB',\n      },\n      {\n        name: 'attack',\n        label: 'Attack',\n        type: 'number',\n        min: 0,\n        max: 0.3,\n        step: 0.001,\n        default: 0.001,\n        unit: 's',\n      },\n      {\n        name: 'release',\n        label: 'Release',\n        type: 'number',\n        min: 0,\n        max: 0.5,\n        step: 0.01,\n        default: 0.1,\n        unit: 's',\n      },\n    ],\n  },\n\n  // === SPATIAL EFFECTS ===\n  {\n    id: 'stereoWidener',\n    name: 'Stereo Widener',\n    category: 'spatial',\n    description: 'Expands or narrows the stereo image',\n    parameters: [\n      { name: 'width', label: 'Width', type: 'number', min: 0, max: 1, step: 0.01, default: 0.5 },\n    ],\n  },\n];\n\n// Helper to get effect definition by ID\nexport const getEffectDefinition = (id: string): EffectDefinition | undefined => {\n  return effectDefinitions.find((def) => def.id === id);\n};\n\n// Helper to get effects by category\nexport const getEffectsByCategory = (\n  category: EffectDefinition['category']\n): EffectDefinition[] => {\n  return effectDefinitions.filter((def) => def.category === category);\n};\n\n// All categories with their display names\nexport const effectCategories: { id: EffectDefinition['category']; name: string }[] = [\n  { id: 'reverb', name: 'Reverb' },\n  { id: 'delay', name: 'Delay' },\n  { id: 'modulation', name: 'Modulation' },\n  { id: 'filter', name: 'Filter' },\n  { id: 'distortion', name: 'Distortion' },\n  { id: 'dynamics', name: 'Dynamics' },\n  { id: 'spatial', name: 'Spatial' },\n];\n","/**\n * Factory for creating Tone.js effect instances from effect definitions\n */\nimport {\n  Reverb,\n  Freeverb,\n  JCReverb,\n  FeedbackDelay,\n  PingPongDelay,\n  Chorus,\n  Phaser,\n  Tremolo,\n  Vibrato,\n  AutoPanner,\n  AutoFilter,\n  AutoWah,\n  EQ3,\n  Distortion,\n  BitCrusher,\n  Chebyshev,\n  Compressor,\n  Limiter,\n  Gate,\n  StereoWidener,\n  ToneAudioNode,\n  connect,\n} from 'tone';\nimport type { InputNode } from 'tone';\nimport type { EffectDefinition } from './effectDefinitions';\n\n// Type for effect instance with common methods\nexport interface EffectInstance {\n  effect: ToneAudioNode | AudioNode; // Tone.js effect, or a native node (WAM plugins)\n  id: string;\n  instanceId: string;\n  dispose: () => void;\n  setParameter: (name: string, value: number | string | boolean) => void;\n  getParameter: (name: string) => number | string | boolean | undefined;\n  connect: (destination: InputNode) => void;\n  disconnect: () => void;\n}\n\n// Each Tone.js effect constructor accepts different option types (ReverbOptions,\n// ChorusOptions, etc.) but all produce ToneAudioNode subclasses. We use a\n// permissive constructor signature to unify them in a single lookup map.\ntype EffectConstructor = new (options?: Record<string, number | string | boolean>) => ToneAudioNode;\n\n/** Centralizes the single unavoidable cast from a specific Tone.js effect constructor to EffectConstructor. */\nfunction asEffectConstructor(ctor: new (...args: never[]) => ToneAudioNode): EffectConstructor {\n  return ctor as unknown as EffectConstructor;\n}\n\n// Map of effect IDs to their Tone.js constructors\nconst effectConstructors: Record<string, EffectConstructor> = {\n  reverb: asEffectConstructor(Reverb),\n  freeverb: asEffectConstructor(Freeverb),\n  jcReverb: asEffectConstructor(JCReverb),\n  feedbackDelay: asEffectConstructor(FeedbackDelay),\n  pingPongDelay: asEffectConstructor(PingPongDelay),\n  chorus: asEffectConstructor(Chorus),\n  phaser: asEffectConstructor(Phaser),\n  tremolo: asEffectConstructor(Tremolo),\n  vibrato: asEffectConstructor(Vibrato),\n  autoPanner: asEffectConstructor(AutoPanner),\n  autoFilter: asEffectConstructor(AutoFilter),\n  autoWah: asEffectConstructor(AutoWah),\n  eq3: asEffectConstructor(EQ3),\n  distortion: asEffectConstructor(Distortion),\n  bitCrusher: asEffectConstructor(BitCrusher),\n  chebyshev: asEffectConstructor(Chebyshev),\n  compressor: asEffectConstructor(Compressor),\n  limiter: asEffectConstructor(Limiter),\n  gate: asEffectConstructor(Gate),\n  stereoWidener: asEffectConstructor(StereoWidener),\n};\n\n// Generate unique instance ID\nlet instanceCounter = 0;\nconst generateInstanceId = (): string => {\n  return `effect_${Date.now()}_${++instanceCounter}`;\n};\n\n/**\n * Create an effect instance from a definition with initial parameter values\n */\nexport function createEffectInstance(\n  definition: EffectDefinition,\n  initialParams?: Record<string, number | string | boolean>\n): EffectInstance {\n  const Constructor = effectConstructors[definition.id];\n  if (!Constructor) {\n    throw new Error(`Unknown effect type: ${definition.id}`);\n  }\n\n  // Build initial options from definition defaults and any overrides\n  const options: Record<string, number | string | boolean> = {};\n  definition.parameters.forEach((param) => {\n    const value = initialParams?.[param.name] ?? param.default;\n    options[param.name] = value;\n  });\n\n  // Create the effect instance\n  const effect = new Constructor(options);\n  const instanceId = generateInstanceId();\n\n  // Dynamic property access on Tone.js effects for parameter get/set.\n  // Each effect type (Reverb, Chorus, EQ3, etc.) exposes different parameters as\n  // properties or Signals. We cast to a record type for safe dynamic access.\n  const effectRecord = effect as unknown as Record<string, { value?: unknown } | unknown>;\n\n  return {\n    effect,\n    id: definition.id,\n    instanceId,\n\n    dispose() {\n      try {\n        effect.disconnect();\n        effect.dispose();\n      } catch (e) {\n        console.warn(\n          `[waveform-playlist] Error disposing effect \"${definition.id}\" (${instanceId}): ${\n            e instanceof Error ? e.message : String(e)\n          }`\n        );\n      }\n    },\n\n    setParameter(name: string, value: number | string | boolean) {\n      // Handle special cases for different effect types\n      const prop = effectRecord[name];\n      if (name === 'wet') {\n        const wetProp = effectRecord['wet'] as { value?: number } | undefined;\n        if (wetProp && typeof wetProp === 'object' && 'value' in wetProp) {\n          wetProp.value = value as number;\n          return;\n        }\n      }\n      if (prop !== undefined) {\n        // Check if it's a Tone.js Signal (has .value property)\n        if (prop && typeof prop === 'object' && 'value' in prop) {\n          (prop as { value: unknown }).value = value;\n        } else {\n          effectRecord[name] = value;\n        }\n      }\n    },\n\n    getParameter(name: string): number | string | boolean | undefined {\n      if (name === 'wet') {\n        const wetProp = effectRecord['wet'] as { value?: number } | undefined;\n        if (wetProp && typeof wetProp === 'object' && 'value' in wetProp) {\n          return wetProp.value;\n        }\n      }\n      const prop = effectRecord[name];\n      if (prop !== undefined) {\n        if (prop && typeof prop === 'object' && 'value' in prop) {\n          return (prop as { value: unknown }).value as number | string | boolean;\n        }\n        return prop as number | string | boolean;\n      }\n      return undefined;\n    },\n\n    connect(destination: InputNode) {\n      effect.connect(destination);\n    },\n\n    disconnect() {\n      try {\n        effect.disconnect();\n      } catch (e) {\n        console.warn(\n          `[waveform-playlist] Error disconnecting effect \"${definition.id}\" (${instanceId}): ${\n            e instanceof Error ? e.message : String(e)\n          }`\n        );\n      }\n    },\n  };\n}\n\n/**\n * Create a chain of effects connected in series\n */\nexport function createEffectChain(effects: EffectInstance[]): {\n  input: ToneAudioNode | AudioNode;\n  output: ToneAudioNode | AudioNode;\n  dispose: () => void;\n} {\n  if (effects.length === 0) {\n    throw new Error('Cannot create effect chain with no effects');\n  }\n\n  // Connect effects in series\n  for (let i = 0; i < effects.length - 1; i++) {\n    connect(effects[i].effect, effects[i + 1].effect);\n  }\n\n  return {\n    input: effects[0].effect,\n    output: effects[effects.length - 1].effect,\n    dispose() {\n      effects.forEach((e) => e.dispose());\n    },\n  };\n}\n","/**\n * Dynamic loader for the optional '@dawcore/wam' peer (the @dawcore/midi\n * loadMidiImpl pattern). Keeps WAM hosting out of the bundle for consumers\n * that never use it; `import type` from '@dawcore/wam' elsewhere is fine\n * (erased at runtime).\n */\nexport type WamModule = typeof import('@dawcore/wam');\n\nexport async function loadWamModule(): Promise<WamModule> {\n  try {\n    return await import('@dawcore/wam');\n  } catch (err) {\n    const detail = err instanceof Error ? err.message : String(err);\n    throw new Error(\n      \"[waveform-playlist] WAM plugin support requires the optional '@dawcore/wam' package.\\n\" +\n        'Install it with: npm install @dawcore/wam\\n' +\n        'Original error: ' +\n        detail\n    );\n  }\n}\n","/**\n * WAM-flavored EffectInstance. The plugin's audioNode (a native\n * AudioWorkletNode) joins the same ordered chain as Tone effects; linking\n * goes through Tone's connect()/disconnect() helpers, which bridge\n * native↔Tone in both directions. Requires native-context mode (the caller\n * guards; see useDynamicEffects.addWamEffect).\n */\nimport { connect, disconnect } from 'tone';\nimport type { InputNode } from 'tone';\nimport type { WamPluginInstance } from '@dawcore/wam'; // type-only — erased at runtime\nimport type { EffectInstance } from './effectFactory';\n\nexport interface WamEffectInstance extends EffectInstance {\n  kind: 'wam';\n  plugin: WamPluginInstance;\n  url?: string;\n}\n\ninterface WamParamTarget {\n  setParameterValues?: (\n    values: Record<string, { id: string; value: number; normalized: boolean }>\n  ) => Promise<void>;\n}\n\nlet wamInstanceCounter = 0;\n\nexport function createWamEffectInstance(plugin: WamPluginInstance): WamEffectInstance {\n  const node = plugin.audioNode as unknown as AudioNode;\n  const instanceId = 'wam_' + ++wamInstanceCounter;\n  return {\n    kind: 'wam',\n    plugin,\n    url: plugin.url,\n    effect: node,\n    id: 'wam:' + (plugin.descriptor?.name ?? plugin.url ?? 'plugin'),\n    instanceId,\n    dispose: () => {\n      try {\n        plugin.destroy();\n      } catch (err) {\n        console.warn(\n          '[waveform-playlist] Error destroying WAM plugin: ' +\n            (err instanceof Error ? err.message : String(err))\n        );\n      }\n    },\n    setParameter: (name, value) => {\n      if (typeof value !== 'number') return;\n      const target = plugin.audioNode as unknown as WamParamTarget;\n      void target.setParameterValues?.({ [name]: { id: name, value, normalized: false } });\n    },\n    getParameter: () => undefined, // WAM params are async; read via the plugin GUI\n    connect: (destination: InputNode) => connect(node, destination),\n    disconnect: () => disconnect(node),\n  };\n}\n","/**\n * Shared offline effects chain builder for WAV export (master + per-track).\n * Natives are re-created fresh on the current (offline) context; WAM entries\n * are re-instantiated on the offline context from their URL-cached factories\n * with the live instance's state transferred (cloneInstanceInto — worklets\n * are context-bound). Any failure disposes the partially-built chain and\n * rethrows: a WAV export must never silently render without an effect the\n * live chain has (#536).\n */\nimport { connect } from 'tone';\nimport type { ToneAudioNode } from 'tone';\nimport type { WamPluginInstance } from '@dawcore/wam'; // type-only — erased at runtime\nimport type { EffectDefinition } from './effectDefinitions';\nimport { createEffectInstance, type EffectInstance } from './effectFactory';\nimport { createWamEffectInstance } from './wamEffectFactory';\nimport { loadWamModule } from './loadWam';\n\n/** Structural subset of ActiveEffect / TrackActiveEffect used offline. */\nexport interface OfflineChainEntry {\n  instanceId: string;\n  kind: 'native' | 'wam';\n  definition: EffectDefinition;\n  params: Record<string, number | string | boolean>;\n}\n\nexport interface OfflineChain {\n  /** Offline effect instances, in chain order. */\n  instances: EffectInstance[];\n  /** Disposes every offline instance (destroys WAM clones). Never throws. */\n  dispose: () => void;\n}\n\nexport async function buildOfflineChain(\n  entries: OfflineChainEntry[],\n  getLivePlugin: (instanceId: string) => WamPluginInstance | undefined,\n  rawContext: BaseAudioContext\n): Promise<OfflineChain> {\n  const instances: EffectInstance[] = [];\n  const dispose = (): void => {\n    // EffectInstance.dispose implementations catch internally — safe to run all.\n    instances.forEach((inst) => inst.dispose());\n  };\n  try {\n    for (const entry of entries) {\n      if (entry.kind === 'native') {\n        instances.push(createEffectInstance(entry.definition, entry.params));\n        continue;\n      }\n      const livePlugin = getLivePlugin(entry.instanceId);\n      if (!livePlugin) {\n        throw new Error(\n          '[waveform-playlist] WAV export: no live WAM plugin found for \"' +\n            entry.definition.name +\n            '\" (' +\n            entry.instanceId +\n            ') — cannot re-instantiate it offline.'\n        );\n      }\n      const wam = await loadWamModule();\n      // ensureWamHost is idempotent per context — repeated calls share one init.\n      const { hostGroupId } = await wam.ensureWamHost(rawContext);\n      const clone = await wam.cloneInstanceInto(livePlugin, rawContext, hostGroupId);\n      instances.push(createWamEffectInstance(clone));\n    }\n  } catch (err) {\n    dispose();\n    throw err;\n  }\n  return { instances, dispose };\n}\n\n/** Wire from → instances (in order) → to via Tone's native↔Tone bridging connect(). */\nexport function connectOfflineChain(\n  from: ToneAudioNode | AudioNode,\n  instances: EffectInstance[],\n  to: ToneAudioNode | AudioNode\n): void {\n  let current: ToneAudioNode | AudioNode = from;\n  for (const inst of instances) {\n    connect(current, inst.effect);\n    current = inst.effect;\n  }\n  connect(current, to);\n}\n","import { useState, useCallback, useRef, useEffect } from 'react';\nimport type { TrackEffectsFunction } from '../index';\nimport { isNativeGlobalContext, getGlobalAudioContext } from '@waveform-playlist/playout';\nimport {\n  effectDefinitions,\n  getEffectDefinition,\n  type EffectDefinition,\n} from '../effects/effectDefinitions';\nimport { createEffectInstance, type EffectInstance } from '../effects/effectFactory';\nimport { loadWamModule } from '../effects/loadWam';\nimport { createWamEffectInstance, type WamEffectInstance } from '../effects/wamEffectFactory';\nimport { buildOfflineChain, connectOfflineChain } from '../effects/offlineChain';\nimport type { OfflineTrackEffectsFunction } from './useExportWav';\nimport type { WamPluginInstance } from '@dawcore/wam'; // type-only — erased at runtime\nimport { Gain, ToneAudioNode, connect } from 'tone';\n\nexport interface TrackActiveEffect {\n  instanceId: string;\n  effectId: string;\n  /** 'native' = built-in Tone effect; 'wam' = hosted WAM plugin. */\n  kind: 'native' | 'wam';\n  /** Module URL for wam entries. */\n  url?: string;\n  definition: EffectDefinition;\n  params: Record<string, number | string | boolean>;\n  bypassed: boolean;\n}\n\nexport interface TrackEffectsState {\n  trackId: string;\n  activeEffects: TrackActiveEffect[];\n}\n\nexport interface UseTrackDynamicEffectsReturn {\n  // State per track\n  trackEffectsState: Map<string, TrackActiveEffect[]>;\n\n  // Actions\n  addEffectToTrack: (trackId: string, effectId: string) => void;\n  /**\n   * Hosts a WAM plugin from a module URL and appends it to a track's effect chain.\n   * Requires native-context mode — call configureGlobalContext({ nativeAudioContext: true })\n   * from @waveform-playlist/playout before any audio initialization.\n   * WAM entries render in offline WAV export (re-instantiated on the offline context).\n   * Resolves with the new entry's instanceId.\n   */\n  addWamEffectToTrack: (trackId: string, url: string, initialState?: unknown) => Promise<string>;\n  /** Live plugin handle for a hosted WAM entry on a track (for GUI mounting via WamEffectGui). */\n  getTrackWamPlugin: (trackId: string, instanceId: string) => WamPluginInstance | undefined;\n  removeEffectFromTrack: (trackId: string, instanceId: string) => void;\n  updateTrackEffectParameter: (\n    trackId: string,\n    instanceId: string,\n    paramName: string,\n    value: number | string | boolean\n  ) => void;\n  toggleBypass: (trackId: string, instanceId: string) => void;\n  clearTrackEffects: (trackId: string) => void;\n  getTrackEffectsFunction: (trackId: string) => TrackEffectsFunction | undefined;\n\n  /**\n   * Creates a fresh effects function for a track for offline rendering.\n   * Native effects are re-created on the offline context; WAM entries are\n   * re-instantiated with the live instance's state transferred. May reject —\n   * a WAV export never silently renders without an effect the live chain has.\n   */\n  createOfflineTrackEffectsFunction: (trackId: string) => OfflineTrackEffectsFunction | undefined;\n\n  // Available effects\n  availableEffects: EffectDefinition[];\n}\n\n/**\n * Hook for managing dynamic effects per track with real-time parameter updates\n */\nexport function useTrackDynamicEffects(): UseTrackDynamicEffectsReturn {\n  // Track effects state per track (for UI)\n  const [trackEffectsState, setTrackEffectsState] = useState<Map<string, TrackActiveEffect[]>>(\n    new Map()\n  );\n\n  // Track effect instances per track (for audio processing)\n  const trackEffectInstancesRef = useRef<Map<string, Map<string, EffectInstance>>>(new Map());\n\n  // Guards addWamEffectToTrack's async window — set false by the unmount cleanup.\n  const isMountedRef = useRef(true);\n\n  // Track graph nodes per track for rebuilding chains\n  const trackGraphNodesRef = useRef<\n    Map<\n      string,\n      {\n        graphEnd: Gain;\n        masterGainNode: ToneAudioNode;\n      }\n    >\n  >(new Map());\n\n  // Rebuild the effect chain for a specific track\n  // Note: trackEffects is passed as parameter to avoid stale closure issues\n  const rebuildTrackChain = useCallback((trackId: string, trackEffects: TrackActiveEffect[]) => {\n    const nodes = trackGraphNodesRef.current.get(trackId);\n    if (!nodes) return;\n\n    const { graphEnd, masterGainNode } = nodes;\n    const instancesMap = trackEffectInstancesRef.current.get(trackId);\n\n    // Disconnect everything first\n    try {\n      graphEnd.disconnect();\n    } catch (e) {\n      console.warn(`[waveform-playlist] Error disconnecting track \"${trackId}\" effect chain:`, e);\n    }\n\n    // Get effect instances in order. Bypassed WAM entries are dropped from the\n    // chain entirely (disconnection bypass — WAM has no wet param).\n    const audible = trackEffects.filter((ae) => !(ae.kind === 'wam' && ae.bypassed));\n    const instances = audible\n      .map((ae) => instancesMap?.get(ae.instanceId))\n      .filter((inst): inst is EffectInstance => inst !== undefined);\n\n    if (instances.length === 0) {\n      // No effects - connect directly\n      graphEnd.connect(masterGainNode);\n    } else {\n      // Connect: graphEnd -> effect1 -> effect2 -> ... -> masterGainNode\n      let currentNode: ToneAudioNode | AudioNode = graphEnd;\n\n      instances.forEach((inst) => {\n        try {\n          inst.disconnect();\n        } catch (e) {\n          console.warn(\n            `[waveform-playlist] Error disconnecting effect \"${inst.id}\" on track \"${trackId}\":`,\n            e\n          );\n        }\n        connect(currentNode, inst.effect);\n        currentNode = inst.effect;\n      });\n\n      // Connect last effect to master\n      connect(currentNode, masterGainNode);\n    }\n  }, []);\n\n  // Add a new effect to a track\n  const addEffectToTrack = useCallback((trackId: string, effectId: string) => {\n    const definition = getEffectDefinition(effectId);\n    if (!definition) {\n      console.error(`Unknown effect: ${effectId}`);\n      return;\n    }\n\n    // Build default params\n    const params: Record<string, number | string | boolean> = {};\n    definition.parameters.forEach((p) => {\n      params[p.name] = p.default;\n    });\n\n    // Create the effect instance\n    const instance = createEffectInstance(definition, params);\n\n    // Initialize maps if needed\n    if (!trackEffectInstancesRef.current.has(trackId)) {\n      trackEffectInstancesRef.current.set(trackId, new Map());\n    }\n    trackEffectInstancesRef.current.get(trackId)!.set(instance.instanceId, instance);\n\n    // Add to state\n    const newActiveEffect: TrackActiveEffect = {\n      instanceId: instance.instanceId,\n      effectId: definition.id,\n      kind: 'native',\n      definition,\n      params,\n      bypassed: false,\n    };\n\n    setTrackEffectsState((prev) => {\n      const newState = new Map(prev);\n      const existing = newState.get(trackId) || [];\n      newState.set(trackId, [...existing, newActiveEffect]);\n      return newState;\n    });\n  }, []);\n\n  // Host a WAM plugin and append it to a track's chain. Requires native-context mode.\n  const addWamEffectToTrack = useCallback(\n    async (trackId: string, url: string, initialState?: unknown): Promise<string> => {\n      if (!isNativeGlobalContext()) {\n        throw new Error(\n          '[waveform-playlist] WAM plugins require a native AudioContext. Call ' +\n            'configureGlobalContext({ nativeAudioContext: true }) from ' +\n            '@waveform-playlist/playout before any audio initialization.'\n        );\n      }\n      const wam = await loadWamModule();\n      const ctx = getGlobalAudioContext();\n      const { hostGroupId } = await wam.ensureWamHost(ctx);\n      const plugin = await wam.createWamInstance(\n        url,\n        ctx,\n        hostGroupId,\n        initialState !== undefined ? { initialState } : undefined\n      );\n      if (!isMountedRef.current) {\n        try {\n          plugin.destroy();\n        } catch (err) {\n          console.warn(\n            '[waveform-playlist] Error destroying WAM plugin after unmount: ' +\n              (err instanceof Error ? err.message : String(err))\n          );\n        }\n        throw new Error(\n          '[waveform-playlist] addWamEffectToTrack aborted: hook unmounted before the plugin finished loading.'\n        );\n      }\n      const instance = createWamEffectInstance(plugin);\n\n      // Initialize maps if needed\n      if (!trackEffectInstancesRef.current.has(trackId)) {\n        trackEffectInstancesRef.current.set(trackId, new Map());\n      }\n      trackEffectInstancesRef.current.get(trackId)!.set(instance.instanceId, instance);\n\n      const definition: EffectDefinition = {\n        id: instance.id,\n        name: plugin.descriptor?.name ?? url,\n        category: 'wam',\n        description: 'WAM plugin',\n        parameters: [],\n      };\n\n      setTrackEffectsState((prev) => {\n        const newState = new Map(prev);\n        const existing = newState.get(trackId) || [];\n        newState.set(trackId, [\n          ...existing,\n          {\n            instanceId: instance.instanceId,\n            effectId: instance.id,\n            kind: 'wam',\n            url,\n            definition,\n            params: {},\n            bypassed: false,\n          },\n        ]);\n        return newState;\n      });\n\n      return instance.instanceId;\n    },\n    []\n  );\n\n  // Live plugin handle for GUI mounting (WamEffectGui).\n  const getTrackWamPlugin = useCallback(\n    (trackId: string, instanceId: string): WamPluginInstance | undefined => {\n      const instancesMap = trackEffectInstancesRef.current.get(trackId);\n      const inst = instancesMap?.get(instanceId) as WamEffectInstance | undefined;\n      return inst?.kind === 'wam' ? inst.plugin : undefined;\n    },\n    []\n  );\n\n  // Remove an effect from a track\n  const removeEffectFromTrack = useCallback((trackId: string, instanceId: string) => {\n    const instancesMap = trackEffectInstancesRef.current.get(trackId);\n    const instance = instancesMap?.get(instanceId);\n    if (instance) {\n      instance.dispose();\n      instancesMap?.delete(instanceId);\n    }\n\n    setTrackEffectsState((prev) => {\n      const newState = new Map(prev);\n      const existing = newState.get(trackId) || [];\n      newState.set(\n        trackId,\n        existing.filter((e) => e.instanceId !== instanceId)\n      );\n      return newState;\n    });\n  }, []);\n\n  // Update a parameter in real-time\n  const updateTrackEffectParameter = useCallback(\n    (trackId: string, instanceId: string, paramName: string, value: number | string | boolean) => {\n      // Update the actual effect instance\n      const instancesMap = trackEffectInstancesRef.current.get(trackId);\n      const instance = instancesMap?.get(instanceId);\n      if (instance) {\n        instance.setParameter(paramName, value);\n      }\n\n      // Update state for UI\n      setTrackEffectsState((prev) => {\n        const newState = new Map(prev);\n        const existing = newState.get(trackId) || [];\n        newState.set(\n          trackId,\n          existing.map((e) =>\n            e.instanceId === instanceId ? { ...e, params: { ...e.params, [paramName]: value } } : e\n          )\n        );\n        return newState;\n      });\n    },\n    []\n  );\n\n  // Toggle bypass for an effect (uses wet parameter - 0 = bypass, restore original for active)\n  const toggleBypass = useCallback((trackId: string, instanceId: string) => {\n    // Get current state from ref to determine new bypassed value (avoids stale closure)\n    const trackEffects = trackEffectsStateRef.current.get(trackId) || [];\n    const effect = trackEffects.find((e) => e.instanceId === instanceId);\n    if (!effect) return;\n\n    const newBypassed = !effect.bypassed;\n\n    if (effect.kind === 'wam') {\n      // Disconnection bypass: the rebuild effect drops bypassed wam entries.\n      setTrackEffectsState((prev) => {\n        const newState = new Map(prev);\n        const existing = newState.get(trackId) || [];\n        newState.set(\n          trackId,\n          existing.map((e) => (e.instanceId === instanceId ? { ...e, bypassed: newBypassed } : e))\n        );\n        return newState;\n      });\n      return;\n    }\n\n    // Update the actual effect instance\n    // When bypassing: set wet to 0\n    // When un-bypassing: restore the original wet value from params\n    const instancesMap = trackEffectInstancesRef.current.get(trackId);\n    const instance = instancesMap?.get(instanceId);\n    if (instance) {\n      const originalWet = (effect.params.wet as number) ?? 1;\n      instance.setParameter('wet', newBypassed ? 0 : originalWet);\n    }\n\n    // Update state for UI\n    setTrackEffectsState((prev) => {\n      const newState = new Map(prev);\n      const existing = newState.get(trackId) || [];\n      newState.set(\n        trackId,\n        existing.map((e) => (e.instanceId === instanceId ? { ...e, bypassed: newBypassed } : e))\n      );\n      return newState;\n    });\n  }, []);\n\n  // Clear all effects from a track\n  const clearTrackEffects = useCallback((trackId: string) => {\n    // Dispose all instances for this track\n    const instancesMap = trackEffectInstancesRef.current.get(trackId);\n    if (instancesMap) {\n      instancesMap.forEach((inst) => inst.dispose());\n      instancesMap.clear();\n    }\n\n    setTrackEffectsState((prev) => {\n      const newState = new Map(prev);\n      newState.set(trackId, []);\n      return newState;\n    });\n  }, []);\n\n  // Ref to store the current trackEffectsState for reading in effects function\n  // This avoids stale closure issues when the effects function is called later\n  const trackEffectsStateRef = useRef<Map<string, TrackActiveEffect[]>>(trackEffectsState);\n  trackEffectsStateRef.current = trackEffectsState;\n\n  // Get the effects function for a track to pass to useAudioTracks\n  // This function is stable (no dependencies) - it reads from refs at call time\n  const getTrackEffectsFunction = useCallback(\n    (trackId: string): TrackEffectsFunction | undefined => {\n      // Return a function that connects effects when the track is loaded\n      return (graphEnd, masterGainNode, _isOffline) => {\n        // Store references for rebuilding chain\n        trackGraphNodesRef.current.set(trackId, {\n          graphEnd,\n          masterGainNode,\n        });\n\n        // Read current state from ref (not stale closure)\n        const trackEffects = trackEffectsStateRef.current.get(trackId) || [];\n        const instancesMap = trackEffectInstancesRef.current.get(trackId);\n\n        // Get effect instances in order. Bypassed WAM entries are dropped from\n        // the chain entirely (disconnection bypass — WAM has no wet param).\n        const audible = trackEffects.filter((ae) => !(ae.kind === 'wam' && ae.bypassed));\n        const instances = audible\n          .map((ae) => instancesMap?.get(ae.instanceId))\n          .filter((inst): inst is EffectInstance => inst !== undefined);\n\n        if (instances.length === 0) {\n          // No effects - connect directly\n          graphEnd.connect(masterGainNode);\n        } else {\n          // Connect: graphEnd -> effect1 -> effect2 -> ... -> masterGainNode\n          let currentNode: ToneAudioNode | AudioNode = graphEnd;\n\n          instances.forEach((inst) => {\n            connect(currentNode, inst.effect);\n            currentNode = inst.effect;\n          });\n\n          // Connect last effect to master\n          connect(currentNode, masterGainNode);\n        }\n\n        return function cleanup() {\n          trackGraphNodesRef.current.delete(trackId);\n        };\n      };\n    },\n    [] // No dependencies - stable function that reads from refs\n  );\n\n  // Rebuild chains when effects change\n  useEffect(() => {\n    trackEffectsState.forEach((effects, trackId) => {\n      rebuildTrackChain(trackId, effects);\n    });\n  }, [trackEffectsState, rebuildTrackChain]);\n\n  // Cleanup on unmount\n  useEffect(() => {\n    isMountedRef.current = true;\n    const trackEffectInstances = trackEffectInstancesRef.current;\n    return () => {\n      isMountedRef.current = false;\n      trackEffectInstances.forEach((instancesMap) => {\n        instancesMap.forEach((inst) => inst.dispose());\n        instancesMap.clear();\n      });\n      trackEffectInstances.clear();\n    };\n  }, []);\n\n  /**\n   * Creates a fresh effects function for a track for offline rendering. Native\n   * effects are re-created on the offline context; WAM entries are\n   * re-instantiated from the live instance's state (cloneInstanceInto).\n   */\n  const createOfflineTrackEffectsFunction = useCallback(\n    (trackId: string): OfflineTrackEffectsFunction | undefined => {\n      const trackEffects = trackEffectsState.get(trackId) || [];\n      // Bypassed entries are excluded offline: natives keep the existing\n      // exclusion; WAM entries use disconnection bypass (parity with live).\n      const nonBypassed = trackEffects.filter((e) => !e.bypassed);\n      if (nonBypassed.length === 0) {\n        return undefined;\n      }\n      const hasWam = nonBypassed.some((e) => e.kind === 'wam');\n\n      return async (graphEnd: Gain, masterGainNode: ToneAudioNode, _isOffline: boolean) => {\n        if (hasWam && !isNativeGlobalContext()) {\n          throw new Error(\n            '[waveform-playlist] WAV export with WAM effects requires a native AudioContext. ' +\n              'Call configureGlobalContext({ nativeAudioContext: true }) from ' +\n              '@waveform-playlist/playout before any audio initialization.'\n          );\n        }\n        // Tone nodes inside the offline build are created on the current\n        // (offline) global context — its rawContext must host the WAM clones.\n        const rawContext = graphEnd.context.rawContext as unknown as BaseAudioContext;\n        const { instances, dispose } = await buildOfflineChain(\n          nonBypassed,\n          (instanceId) => {\n            const inst = trackEffectInstancesRef.current.get(trackId)?.get(instanceId) as\n              | WamEffectInstance\n              | undefined;\n            return inst?.kind === 'wam' ? inst.plugin : undefined;\n          },\n          rawContext\n        );\n        connectOfflineChain(graphEnd, instances, masterGainNode);\n        return dispose;\n      };\n    },\n    [trackEffectsState]\n  );\n\n  return {\n    trackEffectsState,\n    addEffectToTrack,\n    addWamEffectToTrack,\n    getTrackWamPlugin,\n    removeEffectFromTrack,\n    updateTrackEffectParameter,\n    toggleBypass,\n    clearTrackEffects,\n    getTrackEffectsFunction,\n    createOfflineTrackEffectsFunction,\n    availableEffects: effectDefinitions,\n  };\n}\n","import { useState, useCallback } from 'react';\nimport {\n  gainToDb,\n  trackChannelCount,\n  applyFadeIn,\n  applyFadeOut,\n  type ClipTrack,\n  type FadeType,\n} from '@waveform-playlist/core';\nimport { getUnderlyingAudioParam, getGlobalAudioContext } from '@waveform-playlist/playout';\nimport type { Volume, Gain, ToneAudioNode } from 'tone';\nimport { renderToneOffline } from '../utils/renderToneOffline';\nimport { encodeWav, downloadBlob, type WavEncoderOptions } from '../utils/wavEncoder';\n\n/** Function type for per-track effects (same as in @waveform-playlist/core) */\nexport type TrackEffectsFunction = (\n  graphEnd: unknown,\n  destination: unknown,\n  isOffline: boolean\n) => void | (() => void);\n\n/** Cleanup returned by an offline effects function (disposes offline instances / WAM clones). */\nexport type OfflineEffectsCleanup = void | (() => void);\n\n/**\n * Master-chain effects function for offline rendering. May return a Promise —\n * WAM entries are re-instantiated asynchronously on the offline context.\n * Every live EffectsFunction is assignable to this type.\n */\nexport type OfflineEffectsFunction = (\n  masterVolume: Volume,\n  destination: ToneAudioNode,\n  isOffline: boolean\n) => OfflineEffectsCleanup | Promise<OfflineEffectsCleanup>;\n\n/** Per-track variant of OfflineEffectsFunction. */\nexport type OfflineTrackEffectsFunction = (\n  graphEnd: Gain,\n  masterGainNode: ToneAudioNode,\n  isOffline: boolean\n) => OfflineEffectsCleanup | Promise<OfflineEffectsCleanup>;\n\nexport interface ExportOptions extends WavEncoderOptions {\n  /** Filename for download (without extension) */\n  filename?: string;\n  /** Export mode: 'master' for full mixdown, 'individual' for single track */\n  mode?: 'master' | 'individual';\n  /** Track index for individual export (only used when mode is 'individual') */\n  trackIndex?: number;\n  /** Whether to trigger automatic download */\n  autoDownload?: boolean;\n  /** Whether to apply effects (fades, etc.) - defaults to true */\n  applyEffects?: boolean;\n  /**\n   * Optional effects function for master effects. When provided, export renders\n   * through the effects chain (WAM entries included — re-instantiated on the\n   * offline context). The function receives isOffline=true and may be async.\n   */\n  effectsFunction?: OfflineEffectsFunction;\n  /**\n   * Optional function to create offline track effects.\n   * Takes a trackId and returns an offline effects function for that track.\n   * This is used instead of track.effects to avoid AudioContext mismatch issues.\n   */\n  createOfflineTrackEffects?: (trackId: string) => OfflineTrackEffectsFunction | undefined;\n  /** Progress callback (0-1) */\n  onProgress?: (progress: number) => void;\n}\n\nexport interface ExportResult {\n  /** The rendered audio buffer */\n  audioBuffer: AudioBuffer;\n  /** The WAV file as a Blob */\n  blob: Blob;\n  /** Duration in seconds */\n  duration: number;\n}\n\nexport interface UseExportWavReturn {\n  /** Export the playlist to WAV */\n  exportWav: (\n    tracks: ClipTrack[],\n    trackStates: TrackState[],\n    options?: ExportOptions\n  ) => Promise<ExportResult>;\n  /** Whether export is in progress */\n  isExporting: boolean;\n  /** Export progress (0-1) */\n  progress: number;\n  /** Error message if export failed */\n  error: string | null;\n}\n\ninterface TrackState {\n  muted: boolean;\n  soloed: boolean;\n  volume: number;\n  pan: number;\n}\n\n/**\n * Hook for exporting the waveform playlist to WAV format.\n * Uses a Tone offline render (native OfflineAudioContext in native-context mode),\n * mirroring the live playback graph.\n */\nexport function useExportWav(): UseExportWavReturn {\n  const [isExporting, setIsExporting] = useState(false);\n  const [progress, setProgress] = useState(0);\n  const [error, setError] = useState<string | null>(null);\n\n  const exportWav = useCallback(\n    async (\n      tracks: ClipTrack[],\n      trackStates: TrackState[],\n      options: ExportOptions = {}\n    ): Promise<ExportResult> => {\n      const {\n        filename = 'export',\n        mode = 'master',\n        trackIndex,\n        autoDownload = true,\n        applyEffects = true,\n        effectsFunction,\n        createOfflineTrackEffects,\n        bitDepth = 16,\n        onProgress,\n      } = options;\n\n      setIsExporting(true);\n      setProgress(0);\n      setError(null);\n\n      try {\n        // Validate inputs\n        if (tracks.length === 0) {\n          throw new Error('No tracks to export');\n        }\n\n        if (\n          mode === 'individual' &&\n          (trackIndex === undefined || trackIndex < 0 || trackIndex >= tracks.length)\n        ) {\n          throw new Error('Invalid track index for individual export');\n        }\n\n        // Use AudioContext sample rate — the single source of truth for all audio\n        const sampleRate = getGlobalAudioContext().sampleRate;\n\n        // Calculate total duration from all clips (in samples)\n        let totalDurationSamples = 0;\n        for (const track of tracks) {\n          for (const clip of track.clips) {\n            const clipEndSample = clip.startSample + clip.durationSamples;\n            totalDurationSamples = Math.max(totalDurationSamples, clipEndSample);\n          }\n        }\n\n        // Add a small buffer at the end (0.1 seconds) to avoid cutting off\n        totalDurationSamples += Math.round(sampleRate * 0.1);\n\n        const duration = totalDurationSamples / sampleRate;\n\n        // Determine which tracks to render\n        const tracksToRender =\n          mode === 'individual'\n            ? [{ track: tracks[trackIndex!], state: trackStates[trackIndex!], index: trackIndex! }]\n            : tracks.map((track, index) => ({ track, state: trackStates[index], index }));\n\n        // Check for solo - if any track is soloed, only play soloed tracks.\n        // Skip solo logic for individual export — the user explicitly chose the track.\n        const hasSolo = mode === 'master' && trackStates.some((state) => state.soloed);\n\n        const reportProgress = (p: number) => {\n          setProgress(p);\n          onProgress?.(p);\n        };\n\n        const renderedBuffer = await renderOffline(\n          tracksToRender,\n          hasSolo,\n          duration,\n          sampleRate,\n          applyEffects,\n          effectsFunction,\n          createOfflineTrackEffects,\n          reportProgress\n        );\n\n        reportProgress(0.9);\n\n        // Encode to WAV\n        const blob = encodeWav(renderedBuffer, { bitDepth });\n\n        reportProgress(1);\n\n        // Auto download if requested\n        if (autoDownload) {\n          const exportFilename =\n            mode === 'individual' ? `${filename}_${tracks[trackIndex!].name}` : filename;\n          downloadBlob(blob, `${exportFilename}.wav`);\n        }\n\n        return {\n          audioBuffer: renderedBuffer,\n          blob,\n          duration,\n        };\n      } catch (err) {\n        const message = err instanceof Error ? err.message : 'Export failed';\n        setError(message);\n        throw err;\n      } finally {\n        setIsExporting(false);\n      }\n    },\n    []\n  );\n\n  return {\n    exportWav,\n    isExporting,\n    progress,\n    error,\n  };\n}\n\n/**\n * Render the playlist offline. Uses renderToneOffline — a hand-rolled\n * Tone.Offline variant that renders on a NATIVE OfflineAudioContext in\n * native-context mode so mixed Tone + WAM chains can be hosted (#536).\n * Mirrors the live playback graph: Player → fadeGain → trackVolume →\n * trackPan → trackMute → masterVolume → destination. Effects chains (master\n * and per-track) are conditionally inserted when provided; their cleanups\n * (which destroy offline WAM clones) always run after the render.\n */\nasync function renderOffline(\n  tracksToRender: { track: ClipTrack; state: TrackState; index: number }[],\n  hasSolo: boolean,\n  duration: number,\n  sampleRate: number,\n  applyEffects: boolean,\n  effectsFunction: OfflineEffectsFunction | undefined,\n  createOfflineTrackEffects:\n    | ((trackId: string) => OfflineTrackEffectsFunction | undefined)\n    | undefined,\n  onProgress: (progress: number) => void\n): Promise<AudioBuffer> {\n  const { Volume, Gain, Panner, Player, ToneAudioBuffer } = await import('tone');\n\n  onProgress(0.1);\n\n  // Derive output channel count from audible tracks only\n  const audibleTracks = tracksToRender.filter(({ state }) => {\n    if (state.muted && !state.soloed) return false;\n    if (hasSolo && !state.soloed) return false;\n    return true;\n  });\n  const outputChannels = audibleTracks.reduce(\n    (max, { track }) => Math.max(max, trackChannelCount(track)),\n    1\n  );\n\n  const cleanups: Array<() => void> = [];\n  try {\n    const audioBuffer = await renderToneOffline(\n      async (context) => {\n        // Master volume at unity gain\n        const masterVolume = new Volume(0);\n\n        // Conditionally insert master effects chain (may be async — WAM cloning)\n        if (effectsFunction && applyEffects) {\n          const cleanup = await effectsFunction(masterVolume, context.destination, true);\n          if (cleanup) cleanups.push(cleanup);\n        } else {\n          masterVolume.connect(context.destination);\n        }\n\n        for (const { track, state } of audibleTracks) {\n          // Track-level nodes mirror ToneTrack: volume → pan → mute\n          const trackVolume = new Volume(gainToDb(state.volume));\n          // Match channelCount to source material — Tone.js Panner defaults to 1\n          // which forces stereo→mono downmix. Use 2 only for stereo sources.\n          const trackPan = new Panner({ pan: state.pan, channelCount: trackChannelCount(track) });\n          const trackMute = new Gain(state.muted ? 0 : 1);\n\n          // Conditionally insert per-track effects chain (may be async — WAM cloning)\n          const trackEffects = createOfflineTrackEffects?.(track.id);\n          if (trackEffects && applyEffects) {\n            const cleanup = await trackEffects(trackMute, masterVolume, true);\n            if (cleanup) cleanups.push(cleanup);\n          } else {\n            trackMute.connect(masterVolume);\n          }\n\n          // Connect track chain: trackVolume → trackPan → trackMute\n          trackPan.connect(trackMute);\n          trackVolume.connect(trackPan);\n\n          // Schedule each clip\n          for (const clip of track.clips) {\n            const {\n              audioBuffer: clipBuffer,\n              startSample,\n              durationSamples,\n              offsetSamples,\n              gain: clipGain,\n              fadeIn,\n              fadeOut,\n            } = clip;\n\n            // Skip clips without audioBuffer (peaks-only clips can't be exported)\n            if (!clipBuffer) {\n              console.warn(\n                '[waveform-playlist] Skipping clip \"' +\n                  (clip.name || clip.id) +\n                  '\" - no audioBuffer for export'\n              );\n              continue;\n            }\n\n            // Convert samples to seconds\n            const startTime = startSample / sampleRate;\n            const clipDuration = durationSamples / sampleRate;\n            const offset = offsetSamples / sampleRate;\n\n            // Create player and clip-level fade gain\n            const toneBuffer = new ToneAudioBuffer(clipBuffer);\n            const player = new Player(toneBuffer);\n            const fadeGain = new Gain(clipGain);\n\n            // Connect: player → fadeGain → trackVolume\n            player.connect(fadeGain);\n            fadeGain.connect(trackVolume);\n\n            // Apply fade automation via native AudioParam\n            if (applyEffects) {\n              const audioParam = getUnderlyingAudioParam(fadeGain.gain);\n              if (audioParam) {\n                applyClipFades(audioParam, clipGain, startTime, clipDuration, fadeIn, fadeOut);\n              } else if (fadeIn || fadeOut) {\n                console.warn(\n                  '[waveform-playlist] Cannot apply fades for clip \"' +\n                    (clip.name || clip.id) +\n                    '\" - AudioParam not accessible'\n                );\n              }\n            }\n\n            player.start(startTime, offset, clipDuration);\n          }\n        }\n\n        context.transport.start(0);\n      },\n      duration,\n      outputChannels,\n      sampleRate\n    );\n\n    return audioBuffer;\n  } catch (err) {\n    if (err instanceof Error) {\n      throw err;\n    }\n    throw new Error('Offline rendering failed: ' + String(err));\n  } finally {\n    // Always dispose offline effect instances / destroy WAM clones —\n    // success or failure. Warn-and-continue per cleanup.\n    for (const cleanup of cleanups) {\n      try {\n        cleanup();\n      } catch (err) {\n        console.warn(\n          '[waveform-playlist] Export cleanup error: ' +\n            (err instanceof Error ? err.message : String(err))\n        );\n      }\n    }\n  }\n}\n\n/**\n * Apply fade in/out automation to a clip's gain AudioParam.\n * Delegates to core's applyFadeIn/applyFadeOut for consistent curves\n * between live playback and offline export.\n */\nfunction applyClipFades(\n  gainParam: AudioParam,\n  clipGain: number,\n  startTime: number,\n  clipDuration: number,\n  fadeIn: { duration: number; type?: FadeType } | undefined,\n  fadeOut: { duration: number; type?: FadeType } | undefined\n): void {\n  // Set initial gain (0 if fade in, clipGain otherwise)\n  if (fadeIn) {\n    gainParam.setValueAtTime(0, startTime);\n  } else {\n    gainParam.setValueAtTime(clipGain, startTime);\n  }\n\n  if (fadeIn) {\n    applyFadeIn(gainParam, startTime, fadeIn.duration, fadeIn.type || 'linear', 0, clipGain);\n  }\n\n  if (fadeOut) {\n    const fadeOutStart = startTime + clipDuration - fadeOut.duration;\n    // Ensure we're at clipGain before fade out starts\n    if (!fadeIn || fadeIn.duration < clipDuration - fadeOut.duration) {\n      gainParam.setValueAtTime(clipGain, fadeOutStart);\n    }\n    applyFadeOut(gainParam, fadeOutStart, fadeOut.duration, fadeOut.type || 'linear', clipGain, 0);\n  }\n}\n\n/**\n * Export types\n */\nexport type { WavEncoderOptions };\n","/**\n * Hand-rolled variant of Tone.Offline() that can render on a NATIVE\n * OfflineAudioContext when the app runs in native-context mode — required to\n * host WAM worklets in the offline graph (WAM nodes subclass the native\n * AudioWorkletNode; Tone.Offline hardcodes a standardized-audio-context\n * offline context). In standardized mode the construction is identical to\n * Tone.Offline's, so non-WAM exports behave exactly as before. Unlike\n * upstream, the previous global context is restored in a finally block, so a\n * failed graph build can't leave the offline context installed as the app's\n * global context.\n */\nimport { getContext, setContext, OfflineContext } from 'tone';\nimport { isNativeGlobalContext } from '@waveform-playlist/playout';\n\n/** Builds the offline graph. Runs while `context` is the global Tone context. */\nexport type OfflineBuildCallback = (context: OfflineContext) => Promise<void> | void;\n\n// Concurrent renders would interleave the getContext/setContext swaps below —\n// the second caller would capture the first's offline context as \"previous\"\n// and restore a spent OfflineAudioContext as the app's global context.\n// Serialize every render through a module-level queue.\nlet renderQueue: Promise<unknown> = Promise.resolve();\n\nexport async function renderToneOffline(\n  build: OfflineBuildCallback,\n  duration: number,\n  channels: number,\n  sampleRate: number\n): Promise<AudioBuffer> {\n  const run = renderQueue.then(() =>\n    renderToneOfflineSerial(build, duration, channels, sampleRate)\n  );\n  renderQueue = run.catch(() => undefined);\n  return run;\n}\n\nasync function renderToneOfflineSerial(\n  build: OfflineBuildCallback,\n  duration: number,\n  channels: number,\n  sampleRate: number\n): Promise<AudioBuffer> {\n  const offlineContext = isNativeGlobalContext()\n    ? new OfflineContext(\n        new OfflineAudioContext({\n          numberOfChannels: channels,\n          length: Math.round(duration * sampleRate),\n          sampleRate,\n        })\n      )\n    : new OfflineContext(channels, duration, sampleRate);\n\n  const previousContext = getContext();\n  setContext(offlineContext);\n  try {\n    await build(offlineContext);\n  } finally {\n    setContext(previousContext);\n  }\n\n  const toneBuffer = await offlineContext.render();\n  const audioBuffer = toneBuffer.get();\n  if (!audioBuffer) {\n    throw new Error('Offline rendering produced no audio buffer');\n  }\n  return audioBuffer;\n}\n","/**\n * WAV file encoder\n * Converts AudioBuffer to WAV format Blob\n */\n\nexport interface WavEncoderOptions {\n  /** Bit depth: 16 or 32. Default: 16 */\n  bitDepth?: 16 | 32;\n}\n\n/**\n * Encode an AudioBuffer to WAV format\n * @param audioBuffer - The AudioBuffer to encode\n * @param options - Encoding options\n * @returns WAV file as Blob\n */\nexport function encodeWav(audioBuffer: AudioBuffer, options: WavEncoderOptions = {}): Blob {\n  const { bitDepth = 16 } = options;\n\n  const numChannels = audioBuffer.numberOfChannels;\n  const sampleRate = audioBuffer.sampleRate;\n  const numSamples = audioBuffer.length;\n  const bytesPerSample = bitDepth / 8;\n  const blockAlign = numChannels * bytesPerSample;\n  const byteRate = sampleRate * blockAlign;\n  const dataSize = numSamples * blockAlign;\n\n  // WAV header is 44 bytes\n  const headerSize = 44;\n  const totalSize = headerSize + dataSize;\n\n  const buffer = new ArrayBuffer(totalSize);\n  const view = new DataView(buffer);\n\n  // Write WAV header\n  // RIFF chunk descriptor\n  writeString(view, 0, 'RIFF');\n  view.setUint32(4, totalSize - 8, true); // File size minus RIFF header\n  writeString(view, 8, 'WAVE');\n\n  // fmt sub-chunk\n  writeString(view, 12, 'fmt ');\n  view.setUint32(16, 16, true); // Subchunk1Size (16 for PCM)\n  view.setUint16(20, bitDepth === 32 ? 3 : 1, true); // AudioFormat (1=PCM, 3=IEEE float)\n  view.setUint16(22, numChannels, true);\n  view.setUint32(24, sampleRate, true);\n  view.setUint32(28, byteRate, true);\n  view.setUint16(32, blockAlign, true);\n  view.setUint16(34, bitDepth, true);\n\n  // data sub-chunk\n  writeString(view, 36, 'data');\n  view.setUint32(40, dataSize, true);\n\n  // Write interleaved audio data\n  const channelData: Float32Array[] = [];\n  for (let ch = 0; ch < numChannels; ch++) {\n    channelData.push(audioBuffer.getChannelData(ch));\n  }\n\n  let offset = headerSize;\n\n  if (bitDepth === 16) {\n    // 16-bit PCM\n    for (let i = 0; i < numSamples; i++) {\n      for (let ch = 0; ch < numChannels; ch++) {\n        const sample = channelData[ch][i];\n        // Clamp to [-1, 1] and convert to 16-bit signed integer\n        const clampedSample = Math.max(-1, Math.min(1, sample));\n        const intSample = clampedSample < 0 ? clampedSample * 0x8000 : clampedSample * 0x7fff;\n        view.setInt16(offset, intSample, true);\n        offset += 2;\n      }\n    }\n  } else {\n    // 32-bit IEEE float\n    for (let i = 0; i < numSamples; i++) {\n      for (let ch = 0; ch < numChannels; ch++) {\n        view.setFloat32(offset, channelData[ch][i], true);\n        offset += 4;\n      }\n    }\n  }\n\n  return new Blob([buffer], { type: 'audio/wav' });\n}\n\n/**\n * Write a string to a DataView at the specified offset\n */\nfunction writeString(view: DataView, offset: number, str: string): void {\n  for (let i = 0; i < str.length; i++) {\n    view.setUint8(offset + i, str.charCodeAt(i));\n  }\n}\n\n/**\n * Trigger a download of a Blob with the specified filename\n */\nexport function downloadBlob(blob: Blob, filename: string): void {\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement('a');\n  a.href = url;\n  a.download = filename;\n  a.style.display = 'none';\n  document.body.appendChild(a);\n  a.click();\n  document.body.removeChild(a);\n  URL.revokeObjectURL(url);\n}\n","/**\n * useDynamicTracks — imperative hook for runtime track additions.\n *\n * Complements `useAudioTracks` (declarative, configs-driven) with an\n * imperative `addTracks()` API for dynamic loading (drag-and-drop, file pickers).\n *\n * Placeholder tracks appear instantly while audio decodes in parallel.\n */\n\nimport { useState, useCallback, useRef, useEffect } from 'react';\nimport { ClipTrack, createTrack, createClipFromSeconds } from '@waveform-playlist/core';\nimport { getGlobalAudioContext } from '@waveform-playlist/playout';\n\n/** A source that can be decoded into a track. */\nexport type TrackSource = File | Blob | string | { src: string; name?: string };\n\n/** Info about a track that failed to load. */\nexport interface TrackLoadError {\n  /** Display name of the source that failed. */\n  name: string;\n  /** The underlying error. */\n  error: Error;\n}\n\nexport interface UseDynamicTracksReturn {\n  /**\n   * Current tracks array (placeholders + loaded). Pass to WaveformPlaylistProvider.\n   * Placeholder tracks have `clips: []` and names ending with \" (loading...)\".\n   */\n  tracks: ClipTrack[];\n  /** Add one or more sources — creates placeholder tracks immediately. */\n  addTracks: (sources: TrackSource[]) => void;\n  /** Remove a track by its id. Aborts in-flight fetch/decode if still loading. */\n  removeTrack: (trackId: string) => void;\n  /** Number of sources currently decoding. */\n  loadingCount: number;\n  /** True when any source is still decoding. */\n  isLoading: boolean;\n  /** Tracks that failed to load (removed from `tracks` automatically). */\n  errors: TrackLoadError[];\n}\n\n/** Extract a display name from a TrackSource. */\nfunction getSourceName(source: TrackSource): string {\n  if (source instanceof File) {\n    return source.name.replace(/\\.[^/.]+$/, '');\n  }\n  if (source instanceof Blob) {\n    return 'Untitled';\n  }\n  if (typeof source === 'string') {\n    return (\n      source\n        .split('/')\n        .pop()\n        ?.replace(/\\.[^/.]+$/, '') ?? 'Untitled'\n    );\n  }\n  return (\n    source.name ??\n    source.src\n      .split('/')\n      .pop()\n      ?.replace(/\\.[^/.]+$/, '') ??\n    'Untitled'\n  );\n}\n\n/** Decode a TrackSource into an AudioBuffer + clean name. */\nasync function decodeSource(\n  source: TrackSource,\n  audioContext: AudioContext,\n  signal?: AbortSignal\n): Promise<{ audioBuffer: AudioBuffer; name: string }> {\n  const name = getSourceName(source);\n\n  // File and Blob: read arrayBuffer directly (not abortable, but we check signal after)\n  if (source instanceof Blob) {\n    const arrayBuffer = await source.arrayBuffer();\n    signal?.throwIfAborted();\n    const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n    return { audioBuffer, name };\n  }\n\n  const url = typeof source === 'string' ? source : source.src;\n  const response = await fetch(url, { signal });\n  if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.statusText}`);\n  const arrayBuffer = await response.arrayBuffer();\n  signal?.throwIfAborted();\n  const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n  return { audioBuffer, name };\n}\n\nexport function useDynamicTracks(): UseDynamicTracksReturn {\n  const [tracks, setTracks] = useState<ClipTrack[]>([]);\n  const [loadingCount, setLoadingCount] = useState(0);\n  const [errors, setErrors] = useState<TrackLoadError[]>([]);\n  const cancelledRef = useRef(false);\n  /** Track IDs currently decoding — for accurate loadingCount on removal. */\n  const loadingIdsRef = useRef(new Set<string>());\n  /** Per-track AbortControllers for in-flight fetches — keyed by track ID. */\n  const abortControllersRef = useRef(new Map<string, AbortController>());\n\n  useEffect(() => {\n    const controllers = abortControllersRef.current;\n    return () => {\n      cancelledRef.current = true;\n      for (const controller of controllers.values()) {\n        controller.abort();\n      }\n      controllers.clear();\n    };\n  }, []);\n\n  const addTracks = useCallback((sources: TrackSource[]) => {\n    if (sources.length === 0) return;\n\n    const audioContext = getGlobalAudioContext();\n\n    // 1. Create placeholder tracks immediately\n    const placeholders = sources.map((source) => ({\n      track: createTrack({ name: `${getSourceName(source)} (loading...)`, clips: [] }),\n      source,\n    }));\n\n    setTracks((prev) => [...prev, ...placeholders.map((p) => p.track)]);\n    setLoadingCount((prev) => prev + sources.length);\n\n    // 2. Decode each source in parallel (fire-and-forget per source)\n    for (const { track, source } of placeholders) {\n      loadingIdsRef.current.add(track.id);\n      const controller = new AbortController();\n      abortControllersRef.current.set(track.id, controller);\n\n      (async () => {\n        try {\n          const { audioBuffer, name } = await decodeSource(source, audioContext, controller.signal);\n          const clip = createClipFromSeconds({\n            audioBuffer,\n            startTime: 0,\n            duration: audioBuffer.duration,\n            offset: 0,\n            name,\n          });\n\n          // Guard: skip state update if hook unmounted or track was removed while decoding\n          if (!cancelledRef.current && loadingIdsRef.current.has(track.id)) {\n            setTracks((prev) =>\n              prev.map((t) => (t.id === track.id ? { ...t, name, clips: [clip] } : t))\n            );\n          }\n        } catch (error) {\n          if (error instanceof DOMException && error.name === 'AbortError') return;\n          console.warn('[waveform-playlist] Error loading audio:', error);\n          // Guard: skip state update if hook unmounted or track was removed while decoding\n          if (!cancelledRef.current && loadingIdsRef.current.has(track.id)) {\n            setTracks((prev) => prev.filter((t) => t.id !== track.id));\n            setErrors((prev) => [\n              ...prev,\n              {\n                name: getSourceName(source),\n                error: error instanceof Error ? error : new Error(String(error)),\n              },\n            ]);\n          }\n        } finally {\n          abortControllersRef.current.delete(track.id);\n          if (!cancelledRef.current && loadingIdsRef.current.delete(track.id)) {\n            setLoadingCount((prev) => prev - 1);\n          }\n        }\n      })();\n    }\n  }, []);\n\n  const removeTrack = useCallback((trackId: string) => {\n    setTracks((prev) => prev.filter((t) => t.id !== trackId));\n    // Abort in-flight fetch/decode and update loading state\n    const controller = abortControllersRef.current.get(trackId);\n    if (controller) {\n      controller.abort();\n      abortControllersRef.current.delete(trackId);\n    }\n    if (loadingIdsRef.current.delete(trackId)) {\n      setLoadingCount((prev) => prev - 1);\n    }\n  }, []);\n\n  return {\n    tracks,\n    addTracks,\n    removeTrack,\n    loadingCount,\n    isLoading: loadingCount > 0,\n    errors,\n  };\n}\n","/**\n * Hook for monitoring master output levels\n *\n * Connects an AudioWorklet meter processor to the Destination node for\n * real-time output level monitoring. Computes sample-accurate peak and\n * RMS via the meter worklet — no transient is missed.\n *\n * IMPORTANT: Uses getGlobalContext() from playout to ensure the meter\n * is created on the same AudioContext as the audio engine. Tone.js's\n * getContext()/getDestination() return the DEFAULT context, which is\n * replaced when getGlobalContext() calls setContext() on first audio init.\n */\n\nimport { useEffect, useState, useRef, useCallback } from 'react';\nimport { getGlobalContext } from '@waveform-playlist/playout';\nimport { gainToNormalized } from '@waveform-playlist/core';\nimport { meterProcessorUrl, type MeterMessage } from '@waveform-playlist/worklets';\n\n/** Peak decay constant — exponential decay for smooth peak hold (~800ms to 1/e at 60fps) */\nconst PEAK_DECAY = 0.98;\n\nexport interface UseOutputMeterOptions {\n  /**\n   * Number of channels to meter.\n   * Default: 2 (stereo output)\n   */\n  channelCount?: number;\n\n  /**\n   * How often to update the levels (in Hz).\n   * Default: 60 (60fps)\n   */\n  updateRate?: number;\n\n  /**\n   * Whether audio is currently playing. When this transitions to false,\n   * all levels (current, peak, RMS) and smoothed state are reset to zero.\n   * Without this, the browser's tail-time optimization stops calling the\n   * worklet's process() when no audio flows, leaving the last non-zero\n   * levels frozen in state.\n   * Default: false\n   */\n  isPlaying?: boolean;\n}\n\nexport interface UseOutputMeterReturn {\n  /** Per-channel peak output levels (0-1) */\n  levels: number[];\n  /** Per-channel held peak levels (0-1) */\n  peakLevels: number[];\n  /** Per-channel RMS output levels (0-1) */\n  rmsLevels: number[];\n  /** Reset all held peak levels to 0 */\n  resetPeak: () => void;\n  /** Error from meter setup (worklet load failure, context issues, etc.) */\n  error: Error | null;\n}\n\nexport function useOutputMeter(options: UseOutputMeterOptions = {}): UseOutputMeterReturn {\n  const { channelCount = 2, updateRate = 60, isPlaying = false } = options;\n\n  const [levels, setLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n  const [peakLevels, setPeakLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n  const [rmsLevels, setRmsLevels] = useState<number[]>(() => new Array(channelCount).fill(0));\n\n  const workletNodeRef = useRef<AudioWorkletNode | null>(null);\n  const smoothedPeakRef = useRef<number[]>(new Array(channelCount).fill(0));\n  const [meterError, setMeterError] = useState<Error | null>(null);\n\n  const resetPeak = useCallback(\n    () => setPeakLevels(new Array(channelCount).fill(0)),\n    [channelCount]\n  );\n\n  // Reset all levels when playback stops\n  useEffect(() => {\n    if (!isPlaying) {\n      const zeros = new Array(channelCount).fill(0);\n      smoothedPeakRef.current = new Array(channelCount).fill(0);\n      setLevels(zeros);\n      setRmsLevels(zeros);\n      setPeakLevels(zeros);\n    }\n  }, [isPlaying, channelCount]);\n\n  useEffect(() => {\n    let isMounted = true;\n\n    const setup = async () => {\n      // Use getGlobalContext() to ensure we're on the SAME context as the audio engine.\n      const context = getGlobalContext();\n\n      // Load worklet directly on rawContext — Tone.js's addAudioWorkletModule\n      // only loads ONE module per context (caches _workletPromise), silently\n      // skipping subsequent calls with different URLs.\n      const rawCtx = context.rawContext as AudioContext;\n      await rawCtx.audioWorklet.addModule(meterProcessorUrl);\n      if (!isMounted) return;\n\n      // Use Tone.js's createAudioWorkletNode — avoids rawContext identity issues\n      // in webpack-aliased environments (Docusaurus)\n      const workletNode = context.createAudioWorkletNode('meter-processor', {\n        channelCount,\n        channelCountMode: 'explicit' as globalThis.ChannelCountMode,\n        processorOptions: {\n          numberOfChannels: channelCount,\n          updateRate,\n        },\n      });\n      workletNodeRef.current = workletNode;\n\n      workletNode.onprocessorerror = (event) => {\n        console.warn('[waveform-playlist] Output meter worklet processor error:', String(event));\n      };\n\n      // Insert as pass-through in destination chain:\n      // Volume → WorkletNode → Gain → rawContext.destination\n      const destination = context.destination;\n      destination.chain(workletNode);\n\n      smoothedPeakRef.current = new Array(channelCount).fill(0);\n\n      // Listen for meter data from worklet\n      workletNode.port.onmessage = (event: MessageEvent) => {\n        if (!isMounted) return;\n\n        // MeterMessage layout: [0..N-1] peak, [N..2N-1] rms\n        const data = event.data as MeterMessage;\n        const workletChannels = data.length / 2;\n        const smoothed = smoothedPeakRef.current;\n\n        const peakValues: number[] = [];\n        const rmsValues: number[] = [];\n\n        for (let ch = 0; ch < workletChannels; ch++) {\n          // Smoothed peak: jump up instantly, decay slowly\n          smoothed[ch] = Math.max(data[ch], (smoothed[ch] ?? 0) * PEAK_DECAY);\n          peakValues.push(gainToNormalized(smoothed[ch]));\n          rmsValues.push(gainToNormalized(data[workletChannels + ch]));\n        }\n\n        setLevels(peakValues);\n        setRmsLevels(rmsValues);\n        setPeakLevels((prev) => peakValues.map((val, i) => Math.max(prev[i] ?? 0, val)));\n      };\n    };\n\n    setup().catch((err) => {\n      console.warn('[waveform-playlist] Failed to set up output meter:', String(err));\n      if (isMounted) {\n        setMeterError(err instanceof Error ? err : new Error(String(err)));\n      }\n    });\n\n    return () => {\n      isMounted = false;\n\n      if (workletNodeRef.current) {\n        // Restore default chain: Volume → Gain (removes worklet from path)\n        try {\n          const context = getGlobalContext();\n          context.destination.chain();\n        } catch (err) {\n          console.warn('[waveform-playlist] Failed to restore destination chain:', String(err));\n        }\n        try {\n          workletNodeRef.current.disconnect();\n          workletNodeRef.current.port.close();\n        } catch (err) {\n          console.warn('[waveform-playlist] Output meter disconnect during cleanup:', String(err));\n        }\n        workletNodeRef.current = null;\n      }\n    };\n  }, [channelCount, updateRate]);\n\n  return { levels, peakLevels, rmsLevels, resetPeak, error: meterError };\n}\n","import React from 'react';\nimport { BaseControlButton } from '@waveform-playlist/ui-components';\nimport { usePlaylistData } from '../WaveformPlaylistContext';\nimport {\n  useExportWav,\n  type OfflineEffectsFunction,\n  type OfflineTrackEffectsFunction,\n} from '../hooks/useExportWav';\n\nexport interface ExportWavButtonProps {\n  /** Button label */\n  label?: string;\n  /** Filename for the downloaded file (without extension) */\n  filename?: string;\n  /** Export mode: 'master' for stereo mix, 'individual' for single track */\n  mode?: 'master' | 'individual';\n  /** Track index for individual export */\n  trackIndex?: number;\n  /** Bit depth: 16 or 32 */\n  bitDepth?: 16 | 32;\n  /** Whether to apply effects (fades, etc.) - defaults to true */\n  applyEffects?: boolean;\n  /**\n   * Optional effects function for master effects. When provided, export renders\n   * through the effects chain (WAM entries included). May be async.\n   */\n  effectsFunction?: OfflineEffectsFunction;\n  /**\n   * Optional function to create offline track effects.\n   * Takes a trackId and returns an offline effects function for that track.\n   */\n  createOfflineTrackEffects?: (trackId: string) => OfflineTrackEffectsFunction | undefined;\n  /** CSS class name */\n  className?: string;\n  /** Callback when export completes */\n  onExportComplete?: (blob: Blob) => void;\n  /** Callback when export fails */\n  onExportError?: (error: Error) => void;\n}\n\nexport const ExportWavButton: React.FC<ExportWavButtonProps> = ({\n  label = 'Export WAV',\n  filename = 'export',\n  mode = 'master',\n  trackIndex,\n  bitDepth = 16,\n  applyEffects = true,\n  effectsFunction,\n  createOfflineTrackEffects,\n  className,\n  onExportComplete,\n  onExportError,\n}) => {\n  const { tracks, trackStates } = usePlaylistData();\n  const { exportWav, isExporting, progress } = useExportWav();\n\n  const handleExport = async () => {\n    try {\n      const result = await exportWav(tracks, trackStates, {\n        filename,\n        mode,\n        trackIndex,\n        bitDepth,\n        applyEffects,\n        effectsFunction,\n        createOfflineTrackEffects,\n        autoDownload: true,\n      });\n      onExportComplete?.(result.blob);\n    } catch (error) {\n      onExportError?.(error instanceof Error ? error : new Error('Export failed'));\n    }\n  };\n\n  const buttonLabel = isExporting ? `Exporting ${Math.round(progress * 100)}%` : label;\n\n  return (\n    <BaseControlButton\n      onClick={handleExport}\n      disabled={isExporting || tracks.length === 0}\n      className={className}\n    >\n      {buttonLabel}\n    </BaseControlButton>\n  );\n};\n","import React, {\n  createContext,\n  useContext,\n  useState,\n  useEffect,\n  useRef,\n  useCallback,\n  useMemo,\n  type ReactNode,\n} from 'react';\nimport { ThemeProvider } from 'styled-components';\nimport type {\n  EffectsFunction,\n  TrackEffectsFunction,\n  SoundFontCache,\n} from '@waveform-playlist/playout';\nimport { resolvePlayoutAdapter } from './playout/resolvePlayoutAdapter';\nimport { PlaylistEngine, type EngineState, type PlayoutAdapter } from '@waveform-playlist/engine';\nimport {\n  type ClipTrack,\n  type Fade,\n  type AnnotationAction,\n  type MidiNoteData,\n} from '@waveform-playlist/core';\nimport {\n  type TimeFormat,\n  type WaveformPlaylistTheme,\n  defaultTheme,\n} from '@waveform-playlist/ui-components';\nimport { extractPeaksFromWaveformDataFull } from './waveformDataLoader';\nimport { syncSoundFontCacheToAdapter } from './soundFontSync';\nimport type { TrackDragPreview } from './utils/trackOrderPreview';\nimport type WaveformData from 'waveform-data';\nimport type { PeakData } from '@waveform-playlist/core';\nimport type { AnnotationData } from '@waveform-playlist/core';\nimport {\n  useTimeFormat,\n  useZoomControls,\n  useMasterVolume,\n  useSelectionState,\n  useLoopState,\n  useSelectedTrack,\n  useUndoState,\n  useAnimationFrameLoop,\n  useWaveformDataCache,\n} from './hooks';\n\n// Types\nexport interface ClipPeaks {\n  clipId: string;\n  trackName: string;\n  peaks: PeakData;\n  startSample: number;\n  durationSamples: number;\n  fadeIn?: Fade;\n  fadeOut?: Fade;\n  midiNotes?: MidiNoteData[];\n  sampleRate?: number;\n  offsetSamples?: number;\n}\n\nexport type TrackClipPeaks = ClipPeaks[];\n\nexport type { TrackDragPreview };\n\n// Legacy WaveformTrack type - kept for reference but deprecated\n// @deprecated Use ClipTrack from @waveform-playlist/core instead\nexport interface WaveformTrack {\n  src: string | AudioBuffer;\n  name?: string;\n  effects?: TrackEffectsFunction;\n}\n\nexport interface TrackState {\n  name: string;\n  muted: boolean;\n  soloed: boolean;\n  volume: number;\n  pan: number;\n}\n\n// Split contexts for performance optimization\n// Animation context contains playback state and timing refs — no per-frame state updates\n\n/** Per-frame data passed to registered animation callbacks. */\nexport interface FrameData {\n  /** Raw engine time (for state/logic — NOT for visual positioning). */\n  readonly time: number;\n  /** Visually-aligned time for DOM positioning: engine.getAudibleTime() while\n   *  playing (matches speaker output), raw time when resting. */\n  readonly visualTime: number;\n  readonly sampleRate: number;\n  readonly samplesPerPixel: number;\n}\n\nexport interface PlaybackAnimationContextValue {\n  isPlaying: boolean;\n  currentTime: number;\n  currentTimeRef: React.RefObject<number>;\n  /**\n   * Visually-aligned playback time (engine.getAudibleTime() while playing;\n   * raw resting time otherwise). Kept current by the animation loop during playback\n   * and by pause/seek/stop paths when stopped. Read from this for any visual\n   * positioning that should match the audible output.\n   */\n  visualTimeRef: React.RefObject<number>;\n  // Refs for direct time calculation in animated components (avoids timing drift)\n  playbackStartTimeRef: React.RefObject<number>; // context.currentTime when playback started\n  audioStartPositionRef: React.RefObject<number>; // Audio position when playback started\n  /** Returns raw playback time from engine (auto-wraps at loop boundaries). */\n  getPlaybackTime: () => number;\n  /** Current time of the adapter's AudioContext, in seconds. */\n  getAudioContextTime: () => number;\n  /**\n   * Returns the current adapter scheduler lookahead (Tone ~0.1s, native 0).\n   * Use this for any audible-latency calculation that must match the playhead\n   * (e.g., recording live-preview peak slicing).\n   */\n  getLookAhead: () => number;\n  /**\n   * Returns the adapter AudioContext's output latency in seconds.\n   * Use this for audible-latency calculations (e.g., recording live-preview peak slicing).\n   */\n  getOutputLatency: () => number;\n  /** Register a per-frame callback driven by the single animation loop. */\n  registerFrameCallback: (id: string, cb: (data: FrameData) => void) => void;\n  /** Unregister a per-frame callback. */\n  unregisterFrameCallback: (id: string) => void;\n}\n\nexport interface PlaylistStateContextValue {\n  continuousPlay: boolean;\n  linkEndpoints: boolean;\n  annotationsEditable: boolean;\n  isAutomaticScroll: boolean;\n  isLoopEnabled: boolean;\n  annotations: AnnotationData[];\n  activeAnnotationId: string | null;\n  selectionStart: number;\n  selectionEnd: number;\n  selectedTrackId: string | null; // ID of currently selected track for editing operations\n  // Loop region (separate from selection) - Audacity-style loop points\n  loopStart: number;\n  loopEnd: number;\n  /** Whether playback continues past the end of loaded audio instead of\n   *  auto-stopping (implies the fillViewport layout) */\n  indefinitePlayback: boolean;\n  /** Whether the timeline visually fills the scroll container even when the\n   *  audio is shorter (layout only — no effect on playback) */\n  fillViewport: boolean;\n  /** Whether undo is available */\n  canUndo: boolean;\n  /** Whether redo is available */\n  canRedo: boolean;\n}\n\nexport interface PlaylistControlsContextValue {\n  // Playback controls\n  play: (startTime?: number, playDuration?: number) => Promise<void>;\n  pause: () => void;\n  stop: () => void;\n  seekTo: (time: number) => void;\n  setCurrentTime: (time: number) => void;\n\n  // Track controls\n  setTrackMute: (trackIndex: number, muted: boolean) => void;\n  setTrackSolo: (trackIndex: number, soloed: boolean) => void;\n  setTrackVolume: (trackIndex: number, volume: number) => void;\n  setTrackPan: (trackIndex: number, pan: number) => void;\n\n  /** Move a track to a new index in the vertical track order. Purely\n   *  organizational — playback is not interrupted. The reordered tracks\n   *  array flows back through onTracksChange. */\n  reorderTrack: (trackId: string, toIndex: number) => void;\n\n  /** Internal: call after every track-reorder DRAG interaction ends\n   *  (committed or canceled) to force PlaylistVisualization to remount the\n   *  track-controls slots, discarding any @dnd-kit sortable-plugin DOM\n   *  corruption. Not needed for button-driven reorderTrack calls (they never\n   *  touch dnd-kit's DOM). See bumpTrackReorderEpoch in the provider. */\n  bumpTrackReorderEpoch: () => void;\n\n  /** INTERNAL wiring for ClipInteractionProvider: publishes/clears the live\n   *  track-reorder drag preview. Custom drag integrations may call it, but\n   *  most consumers only ever READ trackDragPreview from usePlaylistData(). */\n  setTrackDragPreview: React.Dispatch<React.SetStateAction<TrackDragPreview | null>>;\n\n  // Selection\n  setSelection: (start: number, end: number) => void;\n  setSelectedTrackId: (trackId: string | null) => void;\n\n  // Time format\n  setTimeFormat: (format: TimeFormat) => void;\n  formatTime: (seconds: number) => string;\n\n  // Zoom\n  zoomIn: () => void;\n  zoomOut: () => void;\n\n  // Master volume\n  setMasterVolume: (volume: number) => void;\n\n  // Automatic scroll\n  setAutomaticScroll: (enabled: boolean) => void;\n  setScrollContainer: (element: HTMLDivElement | null) => void;\n  scrollContainerRef: React.RefObject<HTMLDivElement | null>;\n\n  // Annotation controls\n  setContinuousPlay: (enabled: boolean) => void;\n  setLinkEndpoints: (enabled: boolean) => void;\n  setAnnotationsEditable: (enabled: boolean) => void;\n  setAnnotations: React.Dispatch<React.SetStateAction<AnnotationData[]>>;\n  setActiveAnnotationId: (id: string | null) => void;\n\n  // Loop controls\n  setLoopEnabled: (enabled: boolean) => void;\n  setLoopRegion: (start: number, end: number) => void;\n  setLoopRegionFromSelection: () => void;\n  clearLoopRegion: () => void;\n\n  // Undo/redo\n  undo: () => void;\n  redo: () => void;\n\n  // Recording\n  /** Mark a recording session active/inactive. While active: (1) the\n   *  end-of-audio auto-stop is suppressed so overdub playback runs past the\n   *  end of existing material, and (2) with an `armedTrackId`, that track's\n   *  existing content is transiently muted — punch-in recording replaces\n   *  whatever the take overlaps (#579), so the doomed material must not play\n   *  under the overdub; its previous mute state is restored when the session\n   *  ends (audio-only, the UI mute control is untouched). Wired automatically\n   *  from the Waveform / PlaylistVisualization `recordingState` prop; overdub\n   *  flows should also call it eagerly (before `play()`). */\n  setRecordingActive: (active: boolean, armedTrackId?: string | null) => void;\n}\n\nexport interface PlaylistDataContextValue {\n  duration: number;\n  audioBuffers: AudioBuffer[];\n  peaksDataArray: TrackClipPeaks[]; // Array of tracks, each containing array of clip peaks\n  trackStates: TrackState[];\n  tracks: ClipTrack[]; // Original tracks array with IDs\n  sampleRate: number;\n  waveHeight: number;\n  timeScaleHeight: number;\n  minimumPlaylistHeight: number;\n  controls: { show: boolean; width: number };\n  playoutRef: React.RefObject<PlaylistEngine | null>;\n  samplesPerPixel: number;\n  timeFormat: TimeFormat;\n  masterVolume: number;\n  canZoomIn: boolean;\n  canZoomOut: boolean;\n  barWidth: number;\n  barGap: number;\n  /** Draw bars with pill-shaped rounded caps (radius barWidth/2). */\n  roundedBars: boolean;\n  /** Width in pixels of progress bars. Defaults to barWidth + barGap (fills gaps). */\n  progressBarWidth: number;\n  /** Whether the playlist has finished loading all tracks */\n  isReady: boolean;\n  /** Internal: incremented after every track-reorder drag interaction ends.\n   *  PlaylistVisualization folds this into the track-controls slot `key` to\n   *  force a remount, working around @dnd-kit sortable-plugin DOM corruption\n   *  (see bumpTrackReorderEpoch in PlaylistControlsContextValue). */\n  trackReorderEpoch: number;\n  /** Non-null while a track-reorder drag is live: the dragged track and the\n   *  index it would drop at. Both playlist columns derive their display\n   *  layout from it; custom renderTrackControls consumers can read it to\n   *  mirror the preview in their own UI. */\n  trackDragPreview: TrackDragPreview | null;\n  /** Whether tracks are rendered in mono mode */\n  mono: boolean;\n  /** Ref set by useClipDragHandlers during boundary trim drags.\n   *  When true, loadAudio skips engine rebuild — visual updates flow via React state only. */\n  isDraggingRef: React.MutableRefObject<boolean>;\n  onTracksChange: ((tracks: ClipTrack[]) => void) | undefined;\n}\n\n// Create the 4 separate contexts\nconst PlaybackAnimationContext = createContext<PlaybackAnimationContextValue | null>(null);\nconst PlaylistStateContext = createContext<PlaylistStateContextValue | null>(null);\nconst PlaylistControlsContext = createContext<PlaylistControlsContextValue | null>(null);\nconst PlaylistDataContext = createContext<PlaylistDataContextValue | null>(null);\n\nexport interface WaveformPlaylistProviderProps {\n  tracks: ClipTrack[]; // Updated to use clip-based model\n  timescale?: boolean;\n  mono?: boolean;\n  waveHeight?: number;\n  samplesPerPixel?: number;\n  zoomLevels?: number[]; // Array of zoom levels in samples per pixel (lower = more zoomed in)\n  automaticScroll?: boolean;\n  theme?: Partial<WaveformPlaylistTheme>;\n  controls?: {\n    show: boolean;\n    width: number;\n  };\n  annotationList?: {\n    annotations?: AnnotationData[];\n    editable?: boolean;\n    isContinuousPlay?: boolean;\n    linkEndpoints?: boolean;\n    controls?: AnnotationAction[];\n  };\n  effects?: EffectsFunction;\n  onReady?: () => void;\n  /** Called when audio/engine initialization fails (e.g. a missing optional peer,\n   *  a throwing `createAdapter`, or an invalid `zoomLevels`/`samplesPerPixel`). The\n   *  provider already logs the error; use this to surface it in your UI. */\n  onError?: (err: Error) => void;\n  /** @deprecated Use onAnnotationsChange instead */\n  onAnnotationUpdate?: (annotations: AnnotationData[]) => void;\n  /** Callback when annotations are changed (drag, edit, etc.) */\n  onAnnotationsChange?: (annotations: AnnotationData[]) => void;\n  /** Width in pixels of waveform bars. Default: 1 */\n  barWidth?: number;\n  /** Spacing in pixels between waveform bars. Default: 0 */\n  barGap?: number;\n  /** Draw bars with pill-shaped rounded caps (radius barWidth/2). Default: false */\n  roundedBars?: boolean;\n  /** Width in pixels of progress bars. Default: barWidth + barGap (fills gaps). */\n  progressBarWidth?: number;\n  /** Callback when engine clip operations (move, trim, split) change tracks.\n   * The provider calls this so the parent can update its tracks state without\n   * triggering a full engine rebuild.\n   *\n   * **Important:** The parent must pass the received `tracks` reference back as\n   * the `tracks` prop (i.e. `setState(tracks)`). The provider uses reference\n   * identity (`tracks === engineTracksRef.current`) to detect engine-originated\n   * updates and skip the expensive `loadAudio` rebuild. */\n  onTracksChange?: (tracks: ClipTrack[]) => void;\n  /** SoundFont cache for sample-based MIDI playback. When provided, MIDI clips\n   *  use SoundFont samples instead of PolySynth synthesis. */\n  soundFontCache?: SoundFontCache;\n  /** When true, tracks render visually but the engine build is deferred.\n   *  Use this during progressive loading to avoid rebuilding the engine for\n   *  each track — flip to false when all tracks are ready for a single build. */\n  deferEngineRebuild?: boolean;\n  /** Disable the automatic stop when the cursor reaches the end of the\n   *  longest track — the transport rolls until an explicit stop/pause\n   *  (DAW-style). Implies the fillViewport layout. Recording sessions\n   *  suppress the auto-stop automatically, so most recording UIs don't\n   *  need this. */\n  indefinitePlayback?: boolean;\n  /** Extend the timeline (background + timescale) to fill the visible scroll\n   *  container even when the audio is shorter. Layout only. Recording UIs\n   *  typically want this so the empty timeline doesn't collapse to the audio\n   *  width. */\n  fillViewport?: boolean;\n  /** Desired AudioContext sample rate. Creates a cross-browser AudioContext at\n   *  this rate via standardized-audio-context. Pre-computed peaks (.dat files)\n   *  render instantly when they match. On mismatch, falls back to worker. */\n  sampleRate?: number;\n  /** Factory for a custom PlayoutAdapter. When omitted, the Tone.js engine\n   *  (@waveform-playlist/playout) is dynamically imported — so a custom adapter\n   *  lets a consumer use neither @waveform-playlist/playout nor tone. Called once\n   *  per engine rebuild; the provider owns and disposes the returned instance.\n   *  Pass a stable reference (module-level or useCallback) — it is read directly\n   *  inside the curated `loadAudio` effect, not via its dependency array. */\n  createAdapter?: () => PlayoutAdapter;\n  children: ReactNode;\n}\n\nexport const WaveformPlaylistProvider: React.FC<WaveformPlaylistProviderProps> = ({\n  tracks,\n  timescale = false,\n  mono = false,\n  waveHeight = 80,\n  samplesPerPixel: initialSamplesPerPixel = 1024,\n  zoomLevels,\n  automaticScroll = false,\n  theme: userTheme,\n  controls = { show: false, width: 0 },\n  annotationList,\n  effects,\n  onReady,\n  onError,\n  onAnnotationUpdate: _onAnnotationUpdate,\n  onAnnotationsChange,\n  barWidth = 1,\n  barGap = 0,\n  roundedBars = false,\n  progressBarWidth: progressBarWidthProp,\n  onTracksChange,\n  soundFontCache,\n  deferEngineRebuild = false,\n  indefinitePlayback = false,\n  fillViewport = false,\n  sampleRate: sampleRateProp,\n  createAdapter,\n  children,\n}) => {\n  // Default progressBarWidth to barWidth + barGap (fills gaps)\n  const progressBarWidth = progressBarWidthProp ?? barWidth + barGap;\n\n  // Stabilize zoomLevels reference — inline arrays (e.g. zoomLevels={[256, 512]})\n  // create a new reference every render, which would trigger engine rebuild via\n  // loadAudio deps. Content-based comparison avoids spurious rebuilds.\n  const stableZoomLevels = useMemo(\n    () => zoomLevels,\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [zoomLevels?.join(',')]\n  );\n\n  // Annotations are derived from prop (single source of truth in parent)\n  // In v6, annotations must be pre-parsed (numeric start/end). Use parseAeneas() from @waveform-playlist/annotations before passing.\n  const annotations = useMemo(() => {\n    if (!annotationList?.annotations) return [];\n    if (process.env.NODE_ENV !== 'production' && annotationList.annotations.length > 0) {\n      const first = annotationList.annotations[0] as unknown as Record<string, unknown>;\n      if (typeof first.start !== 'number' || typeof first.end !== 'number') {\n        console.error(\n          '[waveform-playlist] Annotations must have numeric start/end values. ' +\n            'In v6, use parseAeneas() from @waveform-playlist/annotations before passing annotations. ' +\n            'Received start type: ' +\n            typeof first.start\n        );\n        return [];\n      }\n    }\n    return annotationList.annotations;\n  }, [annotationList?.annotations]);\n\n  // Ref for animation loop (avoids restarting loop on annotation change)\n  const annotationsRef = useRef<AnnotationData[]>(annotations);\n  annotationsRef.current = annotations;\n\n  // State\n  const [activeAnnotationId, setActiveAnnotationIdState] = useState<string | null>(null);\n  const [isPlaying, setIsPlaying] = useState(false);\n  const [currentTime, setCurrentTime] = useState(0);\n  const [duration, setDuration] = useState(0);\n  const [audioBuffers, setAudioBuffers] = useState<AudioBuffer[]>([]);\n  const [peaksDataArray, setPeaksDataArray] = useState<TrackClipPeaks[]>([]); // Updated for clip-based peaks\n  const [trackStates, setTrackStates] = useState<TrackState[]>([]);\n  const [isAutomaticScroll, setIsAutomaticScroll] = useState(automaticScroll);\n  const [continuousPlay, setContinuousPlayState] = useState(\n    annotationList?.isContinuousPlay ?? false\n  );\n  const [linkEndpoints, setLinkEndpoints] = useState(annotationList?.linkEndpoints ?? false);\n  const [annotationsEditable, setAnnotationsEditable] = useState(annotationList?.editable ?? false);\n  const [isReady, setIsReady] = useState(false);\n  // Bumped after every track-reorder DRAG interaction (committed or not) —\n  // see bumpTrackReorderEpoch below for the full rationale.\n  const [trackReorderEpoch, setTrackReorderEpoch] = useState(0);\n  // Live track-reorder drag preview (view-level only; see the\n  // \"Track Reordering\" section of packages/browser/CLAUDE.md). Set by\n  // ClipInteractionProvider's onDragOver, cleared on drag end/cancel.\n  // The engine's committed order is untouched until drop.\n  const [trackDragPreview, setTrackDragPreview] = useState<TrackDragPreview | null>(null);\n\n  // Refs\n  // Engine owns selection, loop, selectedTrackId, zoom, and masterVolume state.\n  // React subscribes to engine statechange and mirrors into useState/refs.\n  // Playback timing (currentTime, isPlaying) remains in React for animation loop.\n  const engineRef = useRef<PlaylistEngine | null>(null);\n  const adapterRef = useRef<PlayoutAdapter | null>(null);\n  const audioInitializedRef = useRef<boolean>(false);\n\n  // Ref for animation loop access (avoids adding the prop to useCallback deps)\n  const indefinitePlaybackRef = useRef(indefinitePlayback);\n  indefinitePlaybackRef.current = indefinitePlayback;\n\n  // Recording session state. While active:\n  // 1. The animation loop's end-of-audio auto-stop is suppressed so overdub\n  //    playback runs past the end of existing material (#589/#590).\n  // 2. The armed track's existing content is transiently muted: punch-in\n  //    replaces whatever the take overlaps (#579), so the doomed material\n  //    must not play under the overdub. Engine-level mute (survives adapter\n  //    rebuilds), captured/restored around the session; the track's UI mute\n  //    state is untouched.\n  // Auto-wired from PlaylistVisualization's recordingState prop; overdub\n  // flows also call it eagerly before play() — a render round-trip loses the\n  // race against the first animation frame (empty timeline: time 0 >=\n  // duration 0 would stop playback instantly).\n  const recordingActiveRef = useRef(false);\n  const armedTrackMuteRef = useRef<{ trackId: string; prevMuted: boolean } | null>(null);\n  const setRecordingActive = useCallback((active: boolean, armedTrackId?: string | null) => {\n    recordingActiveRef.current = active;\n    const engine = engineRef.current;\n    if (!engine) return;\n    if (active && armedTrackId) {\n      if (armedTrackMuteRef.current?.trackId === armedTrackId) return; // already armed\n      if (armedTrackMuteRef.current) {\n        // Armed track changed mid-session — restore the previous one first.\n        engine.setTrackMute(armedTrackMuteRef.current.trackId, armedTrackMuteRef.current.prevMuted);\n      }\n      const prevMuted = engine.getState().tracks.find((t) => t.id === armedTrackId)?.muted ?? false;\n      armedTrackMuteRef.current = { trackId: armedTrackId, prevMuted };\n      engine.setTrackMute(armedTrackId, true);\n    } else if (!active && armedTrackMuteRef.current) {\n      engine.setTrackMute(armedTrackMuteRef.current.trackId, armedTrackMuteRef.current.prevMuted);\n      armedTrackMuteRef.current = null;\n    }\n  }, []);\n  const isPlayingRef = useRef<boolean>(false);\n  isPlayingRef.current = isPlaying;\n  const playStartPositionRef = useRef<number>(0);\n  const currentTimeRef = useRef<number>(0);\n  // Visually-aligned playback time (see toVisualTime / getAudibleTime). The\n  // animation loop updates this per frame. Pause/seek/stop paths sync it\n  // explicitly so the static playhead reflects the commanded resting position.\n  const visualTimeRef = useRef<number>(0);\n  const tracksRef = useRef<ClipTrack[]>(tracks);\n  const soundFontCacheRef = useRef(soundFontCache);\n  soundFontCacheRef.current = soundFontCache;\n  const trackStatesRef = useRef<TrackState[]>(trackStates);\n  const playbackStartTimeRef = useRef<number>(0); // context.currentTime when playback started\n  const audioStartPositionRef = useRef<number>(0); // Audio position when playback started\n  const playbackEndTimeRef = useRef<number | null>(null); // Audio position where playback should stop (for selections)\n  // Suppresses the engine 'stop' mirror (below) around provider-initiated\n  // engine.stop() calls that immediately seek/restart playback in the same\n  // synchronous block (play(), setSelection, reschedulePlayback, the\n  // engine-rebuild-with-resume path). Without this, the mirror's ref writes\n  // would race the restart's own writes and briefly show the pre-restart\n  // position/isPlaying=false. Stop paths that don't restart afterward don't\n  // need suppression — see the mirror's own comment for why.\n  const suppressEngineStopMirrorRef = useRef(false);\n  const scrollContainerRef = useRef<HTMLDivElement | null>(null);\n  const isAutomaticScrollRef = useRef<boolean>(false);\n  // Animation frame callback registry — components register per-frame DOM update\n  // callbacks instead of running their own rAF loops. Single loop drives all.\n  const frameCallbacksRef = useRef<Map<string, (data: FrameData) => void>>(new Map());\n  const continuousPlayRef = useRef<boolean>(annotationList?.isContinuousPlay ?? false);\n  const activeAnnotationIdRef = useRef<string | null>(null);\n  // Engine clip operations guard: tracks reference from the last engine statechange.\n  // When engine methods (moveClip, trimClip, splitClip) mutate tracks, the statechange\n  // handler stores state.tracks here and calls onTracksChange with the same reference.\n  // loadAudio checks tracks === engineTracksRef.current to skip the full rebuild.\n  const engineTracksRef = useRef<ClipTrack[] | null>(null);\n  // Monotonic counter from engine.getState().tracksVersion — used to detect\n  // track mutations (move/trim/split/add/remove) vs other statechange events.\n  const lastTracksVersionRef = useRef(0);\n  // Flag set during render to prevent the previous effect cleanup from disposing the\n  // engine when we're skipping the rebuild. React runs previous cleanup before the\n  // current effect body, so the flag must be set during render (which precedes cleanup).\n  const skipEngineDisposeRef = useRef(false);\n  // Set by useClipDragHandlers during boundary trim drags. When true,\n  // loadAudio skips the full engine rebuild — visual updates flow via React\n  // state only. On drag end, engine.trimClip() commits the final delta.\n  const isDraggingRef = useRef(false);\n  // Snapshot of tracks from the previous loadAudio run, used to detect\n  // additive-only changes (new tracks appended, existing unchanged).\n  const prevTracksRef = useRef<ClipTrack[]>([]);\n  // Provider-level ref for scroll-position math and animation loop pixel\n  // calculation. Distinct from useZoomControls's internal ref (statechange guard).\n  const samplesPerPixelRef = useRef<number>(initialSamplesPerPixel);\n  // Provisional sample rate until the adapter (which owns the AudioContext) is\n  // created in loadAudio. Reconciled from adapter.audioContext.sampleRate there.\n  // (#510: avoids a static @waveform-playlist/playout import at mount.)\n  const [initialSampleRate] = useState<number>(() => sampleRateProp ?? 48000);\n  const sampleRateRef = useRef<number>(initialSampleRate);\n\n  // Custom hooks — engine-owned state delegated to hooks with onEngineState() pattern\n  const { timeFormat, setTimeFormat, formatTime } = useTimeFormat();\n  const zoom = useZoomControls({ engineRef, initialSamplesPerPixel });\n  const { samplesPerPixel, onEngineState: onZoomEngineState } = zoom;\n  const volume = useMasterVolume({ engineRef, initialVolume: 1.0 });\n  const {\n    masterVolume,\n    setMasterVolume,\n    masterVolumeRef,\n    onEngineState: onVolumeEngineState,\n  } = volume;\n  const {\n    selectionStart,\n    selectionEnd,\n    setSelection: setSelectionEngine,\n    selectionStartRef,\n    selectionEndRef,\n    onEngineState: onSelectionEngineState,\n  } = useSelectionState({ engineRef });\n  const {\n    isLoopEnabled,\n    loopStart,\n    loopEnd,\n    setLoopEnabled,\n    setLoopRegion,\n    clearLoopRegion,\n    isLoopEnabledRef,\n    loopStartRef,\n    loopEndRef,\n    onEngineState: onLoopEngineState,\n  } = useLoopState({ engineRef });\n  const {\n    selectedTrackId,\n    setSelectedTrackId: setSelectedTrackIdControl,\n    onEngineState: onSelectedTrackEngineState,\n    selectedTrackIdRef,\n  } = useSelectedTrack({ engineRef });\n  const {\n    canUndo,\n    canRedo,\n    undo,\n    redo,\n    onEngineState: onUndoEngineState,\n  } = useUndoState({ engineRef });\n  const { animationFrameRef, startAnimationFrameLoop, stopAnimationFrameLoop } =\n    useAnimationFrameLoop();\n\n  // Worker-based WaveformData cache for fast zoom resampling\n  const baseScale = useMemo(\n    () => Math.min(...(stableZoomLevels ?? [256, 512, 1024, 2048, 4096, 8192])),\n    [stableZoomLevels]\n  );\n  const { cache: waveformDataCache } = useWaveformDataCache(tracks, baseScale);\n\n  // Custom setter for continuousPlay that updates BOTH state and ref synchronously\n  // This ensures the ref is updated immediately, before the animation loop can read it\n  const setContinuousPlay = useCallback((value: boolean) => {\n    continuousPlayRef.current = value; // Update ref synchronously\n    setContinuousPlayState(value); // Update state (triggers re-render)\n  }, []);\n\n  // Custom setter for activeAnnotationId that updates BOTH state and ref synchronously\n  const setActiveAnnotationId = useCallback((value: string | null) => {\n    activeAnnotationIdRef.current = value; // Update ref synchronously\n    setActiveAnnotationIdState(value); // Update state (triggers re-render)\n  }, []);\n\n  // Cross-hook concern: reads selection refs to set loop region\n  const setLoopRegionFromSelection = useCallback(() => {\n    const start = selectionStartRef.current ?? 0;\n    const end = selectionEndRef.current ?? 0;\n    if (start !== end && end > start) {\n      setLoopRegion(start, end);\n    }\n  }, [setLoopRegion, selectionStartRef, selectionEndRef]);\n\n  // Keep refs in sync with state\n  useEffect(() => {\n    isAutomaticScrollRef.current = isAutomaticScroll;\n  }, [isAutomaticScroll]);\n\n  useEffect(() => {\n    trackStatesRef.current = trackStates;\n  }, [trackStates]);\n\n  tracksRef.current = tracks;\n\n  // Render-phase guard: detect when tracks prop came from an engine clip operation.\n  // Must be set before effects run so the previous effect cleanup can skip disposal.\n  // Also skip disposal during active boundary trim drags — onDragMove updates React\n  // tracks per-frame, triggering effect re-runs whose cleanup would dispose the engine.\n  const isEngineTracks = tracks === engineTracksRef.current;\n\n  // Detect additive-only track changes: new tracks appended, existing tracks unchanged.\n  // Uses reference equality on individual tracks — if any existing track object changed\n  // (e.g., clip added to it), this is false and triggers a full rebuild.\n  const prevTracks = prevTracksRef.current;\n  const isIncrementalAdd =\n    engineRef.current !== null &&\n    tracks.length > prevTracks.length &&\n    prevTracks.every((pt) => {\n      const current = tracks.find((t) => t.id === pt.id);\n      return current === pt; // reference equality — existing tracks must be untouched\n    });\n  // Note: prevTracks may be empty here (e.g. recording's first track on a\n  // persistent empty-timeline engine) — that's a valid 0→1 incremental add, so\n  // it must NOT require prevTracks.length > 0. The engineRef !== null guard keeps\n  // it correct: a lazy/no-engine consumer still takes the full build path.\n\n  skipEngineDisposeRef.current = isEngineTracks || isDraggingRef.current || isIncrementalAdd;\n\n  // Adjust scroll position proportionally when zoom changes\n  useEffect(() => {\n    if (!scrollContainerRef.current || duration === 0) return;\n\n    const container = scrollContainerRef.current;\n    const oldSamplesPerPixel = samplesPerPixelRef.current;\n    const newSamplesPerPixel = samplesPerPixel;\n\n    if (oldSamplesPerPixel === newSamplesPerPixel) return;\n\n    // Calculate the current center time in the viewport\n    const containerWidth = container.clientWidth;\n    const currentScrollLeft = container.scrollLeft;\n    const centerPixel = currentScrollLeft + containerWidth / 2;\n    const sr = sampleRateRef.current;\n    const centerTime = (centerPixel * oldSamplesPerPixel) / sr;\n\n    // Calculate new scroll position to keep the same center time\n    const newCenterPixel = (centerTime * sr) / newSamplesPerPixel;\n    const newScrollLeft = Math.max(0, newCenterPixel - containerWidth / 2);\n\n    container.scrollLeft = newScrollLeft;\n    samplesPerPixelRef.current = newSamplesPerPixel;\n  }, [samplesPerPixel, duration]);\n\n  // Track pending playback resume after tracks change\n  const pendingResumeRef = useRef<{ position: number } | null>(null);\n\n  // Load audio from clips (only when tracks change)\n  useEffect(() => {\n    // Guard: skip full rebuild when tracks came from an engine clip operation\n    // (moveClip, trimClip, splitClip). The engine and adapter already have the\n    // correct tracks — we just need to update duration for the timeline.\n    // Also skip during active boundary trim drags — visual updates flow via\n    // React state only; engine.trimClip() commits the final delta on drag end.\n    if (isEngineTracks || isDraggingRef.current) {\n      if (isEngineTracks) {\n        engineTracksRef.current = null;\n      }\n\n      // Recalculate duration from updated clip positions\n      let maxDuration = 0;\n      tracks.forEach((track) => {\n        track.clips.forEach((clip) => {\n          const clipEnd = (clip.startSample + clip.durationSamples) / clip.sampleRate;\n          maxDuration = Math.max(maxDuration, clipEnd);\n        });\n      });\n      setDuration(maxDuration);\n      prevTracksRef.current = tracks;\n      return;\n    }\n\n    // Guard: incremental track addition — only new tracks appended, existing\n    // tracks unchanged. Add each new track to the existing engine without\n    // tearing down the playout. This avoids disposing and recreating all\n    // audio nodes when dropping a file or finishing a recording.\n    if (isIncrementalAdd && engineRef.current) {\n      try {\n        // Use prevTracks captured during render (not prevTracksRef.current) to ensure\n        // the guard condition and inner logic operate on the same snapshot.\n        const prevIds = new Set(prevTracks.map((t) => t.id));\n        const addedTracks = tracks.filter((t) => !prevIds.has(t.id));\n\n        // Merge current UI state into new tracks before adding to engine\n        const currentTrackStates = trackStatesRef.current;\n        for (const track of addedTracks) {\n          const trackIndex = tracks.indexOf(track);\n          const trackState = currentTrackStates[trackIndex];\n          const trackWithState = {\n            ...track,\n            volume: trackState?.volume ?? track.volume,\n            muted: trackState?.muted ?? track.muted,\n            soloed: trackState?.soloed ?? track.soloed,\n            pan: trackState?.pan ?? track.pan,\n          };\n          engineRef.current!.addTrack(trackWithState);\n        }\n\n        // Update duration from all tracks (including newly added)\n        let maxDuration = 0;\n        tracks.forEach((track) => {\n          track.clips.forEach((clip) => {\n            const clipEnd = (clip.startSample + clip.durationSamples) / clip.sampleRate;\n            maxDuration = Math.max(maxDuration, clipEnd);\n          });\n        });\n        setDuration(maxDuration);\n\n        // Initialize track states for the new tracks (preserve existing)\n        setTrackStates((prev) => {\n          if (prev.length === tracks.length) return prev;\n          const newStates = [...prev];\n          for (const track of addedTracks) {\n            newStates.push({\n              name: track.name,\n              muted: track.muted,\n              soloed: track.soloed,\n              volume: track.volume,\n              pan: track.pan,\n            });\n          }\n          return newStates;\n        });\n\n        // Extract audio buffers from all clips across all tracks\n        const buffers: AudioBuffer[] = [];\n        tracks.forEach((track) => {\n          track.clips.forEach((clip) => {\n            if (clip.audioBuffer) {\n              buffers.push(clip.audioBuffer);\n            }\n          });\n        });\n        setAudioBuffers(buffers);\n\n        prevTracksRef.current = tracks;\n        return;\n      } catch (err) {\n        console.warn(\n          '[waveform-playlist] Incremental add failed, falling through to full rebuild:',\n          String(err)\n        );\n        // Fall through to full loadAudio path\n      }\n    }\n\n    // Defer engine rebuild during progressive loading — tracks render visually\n    // (controls, clip shapes, peaks via worker) but the engine isn't built yet.\n    // When deferEngineRebuild flips to false, this effect re-runs and builds once.\n    if (deferEngineRebuild) {\n      // Still update duration so the timeline renders at the correct width\n      let maxDuration = 0;\n      tracks.forEach((track) => {\n        track.clips.forEach((clip) => {\n          const clipEnd = (clip.startSample + clip.durationSamples) / clip.sampleRate;\n          maxDuration = Math.max(maxDuration, clipEnd);\n        });\n      });\n      setDuration(maxDuration);\n      return;\n    }\n\n    // Reset ready state for full rebuild\n    setIsReady(false);\n\n    if (tracks.length === 0) {\n      // Clear content state when all tracks are removed.\n      setAudioBuffers([]);\n      setDuration(0);\n      setTrackStates([]);\n      setPeaksDataArray([]);\n\n      // Always keep a persistent engine alive — even with no tracks — so the\n      // Transport is ready before playback/recording starts. Without this the\n      // engine is rebuilt asynchronously when the first track is added, and\n      // play() races that rebuild — intermittently no-opping (frozen\n      // playhead/time) and paying first-record latency. The engine is still\n      // created via dynamic import (lazy module load preserved, #510); only its\n      // lifecycle is eager.\n      if (engineRef.current) {\n        // Keep the existing engine; just clear its tracks.\n        engineRef.current.setTracks([]);\n        prevTracksRef.current = tracks;\n        return;\n      }\n      // No engine yet (initial mount with no tracks): fall through to the\n      // loadAudio path below, which builds a persistent engine for 0 tracks.\n    }\n\n    // Capture playback state before rebuilding playout (read from ref, not state,\n    // so isPlaying can be excluded from the dep array — play/pause must not trigger rebuilds)\n    const wasPlaying = isPlayingRef.current;\n    const resumePosition = currentTimeRef.current;\n\n    // Stop current playback and animation before disposing\n    if (engineRef.current && wasPlaying) {\n      // Suppressed: resumePlayback() (below) restarts from resumePosition once\n      // the rebuilt engine is ready — an unguarded mirror would clobber the\n      // refs with this (old, about-to-be-replaced) engine's rewound position\n      // in the meantime. See suppressEngineStopMirrorRef declaration.\n      suppressEngineStopMirrorRef.current = true;\n      engineRef.current.stop();\n      suppressEngineStopMirrorRef.current = false;\n      stopAnimationFrameLoop();\n      // Mark that we need to resume playback after playout is rebuilt\n      pendingResumeRef.current = { position: resumePosition };\n    }\n\n    let cancelled = false;\n\n    const loadAudio = async () => {\n      try {\n        // Extract all audio buffers from all clips across all tracks\n        const buffers: AudioBuffer[] = [];\n\n        tracks.forEach((track) => {\n          track.clips.forEach((clip) => {\n            if (clip.audioBuffer) {\n              buffers.push(clip.audioBuffer);\n            }\n          });\n        });\n\n        // Calculate total timeline duration from all clips across all tracks\n        // Use clip.sampleRate which is always defined (works for peaks-only clips too)\n        let maxDuration = 0;\n        tracks.forEach((track) => {\n          track.clips.forEach((clip) => {\n            const sampleRate = clip.sampleRate;\n            const clipEndSample = clip.startSample + clip.durationSamples;\n            const clipEnd = clipEndSample / sampleRate;\n            maxDuration = Math.max(maxDuration, clipEnd);\n          });\n        });\n\n        setAudioBuffers(buffers);\n        setDuration(maxDuration);\n\n        // Initialize or update track states, preserving existing UI state (mute/solo/volume/pan)\n        // Only initialize from ClipTrack props when trackStates is empty or track count changes\n        setTrackStates((prevStates) => {\n          if (prevStates.length === tracks.length) {\n            // Same number of tracks - preserve existing UI state, just update names\n            return prevStates.map((state, i) => ({\n              ...state,\n              name: tracks[i].name,\n            }));\n          }\n          // Track count changed - reinitialize from ClipTrack properties\n          return tracks.map((track) => ({\n            name: track.name,\n            muted: track.muted,\n            soloed: track.soloed,\n            volume: track.volume,\n            pan: track.pan,\n          }));\n        });\n\n        // Dispose old engine before creating new one\n        if (engineRef.current) {\n          engineRef.current.dispose();\n        }\n\n        // Reset tracks version tracking for the new engine\n        lastTracksVersionRef.current = 0;\n        engineTracksRef.current = null;\n\n        // Create engine adapter — custom if provided, else Tone.js (dynamically imported).\n        // Reset init flag — new adapter needs AudioContext resume on first play.\n        audioInitializedRef.current = false;\n        const adapter = await resolvePlayoutAdapter({\n          createAdapter,\n          effects,\n          soundFontCache: soundFontCacheRef.current,\n          sampleRate: sampleRateProp,\n        });\n        if (cancelled) {\n          adapter.dispose();\n          return;\n        }\n        // Reconcile sample rate from the adapter's own AudioContext. The setState\n        // calls later in loadAudio trigger a re-render that propagates this via\n        // `const sampleRate = sampleRateRef.current` at render.\n        sampleRateRef.current = adapter.audioContext.sampleRate;\n        adapterRef.current = adapter;\n        const engine = new PlaylistEngine({\n          adapter,\n          samplesPerPixel: samplesPerPixelRef.current,\n          zoomLevels: stableZoomLevels,\n        });\n\n        // Seed engine with current UI state so a fresh engine doesn't\n        // reset selection/loop/selectedTrack to defaults on the first statechange emission.\n        engine.setSelection(selectionStartRef.current ?? 0, selectionEndRef.current ?? 0);\n        engine.setLoopRegion(loopStartRef.current ?? 0, loopEndRef.current ?? 0);\n        if (isLoopEnabledRef.current) engine.setLoopEnabled(true);\n        engine.setMasterVolume(masterVolumeRef.current ?? 1.0);\n        if (selectedTrackIdRef.current) engine.selectTrack(selectedTrackIdRef.current);\n\n        // Merge current UI state into tracks before passing to engine\n        const currentTrackStates = trackStatesRef.current;\n        const tracksWithState = tracks.map((track, index) => {\n          const trackState = currentTrackStates[index];\n          return {\n            ...track,\n            volume: trackState?.volume ?? track.volume,\n            muted: trackState?.muted ?? track.muted,\n            soloed: trackState?.soloed ?? track.soloed,\n            pan: trackState?.pan ?? track.pan,\n          };\n        });\n\n        // Subscribe to engine statechange BEFORE setTracks() so the first\n        // emission (which carries correct canZoomIn/canZoomOut) is captured.\n        // Seeding statechanges fired above are harmless — they contain values\n        // that already match the hooks' current state, so onEngineState()\n        // ref guards skip them.\n        // Suppress tracks mirroring during the initial setTracks — the parent\n        // already has these tracks. The flag is local so it becomes false\n        // after setTracks returns (statechange fires synchronously).\n        let suppressTracksMirroring = true;\n        engine.on('statechange', (state: EngineState) => {\n          onSelectionEngineState(state);\n          onLoopEngineState(state);\n          onSelectedTrackEngineState(state);\n          onZoomEngineState(state);\n          onVolumeEngineState(state);\n          onUndoEngineState(state);\n\n          // Mirror engine tracks changes to parent via onTracksChange.\n          // tracksVersion only increments on structural track mutations (move,\n          // trim, split, setTracks, addTrack, removeTrack), not on\n          // selection/zoom/volume changes. Per-track mixer edits bump the\n          // engine's separate mixerVersion (#501) and are persisted here via\n          // trackStates, so they intentionally do not flow through onTracksChange.\n          if (!suppressTracksMirroring && state.tracksVersion !== lastTracksVersionRef.current) {\n            lastTracksVersionRef.current = state.tracksVersion;\n            engineTracksRef.current = state.tracks;\n\n            // trackStates is positionally indexed (no track id of its own) and\n            // is otherwise never touched by reorderTrack — the engine reorder is\n            // \"purely organizational\" (see PlaylistEngine.reorderTrack) and never\n            // rebuilds. Without this, a track's mixer identity (name/mute/solo/\n            // volume/pan) stays pinned to its OLD index after a reorder, so the\n            // control panel silently shows/controls the wrong track. Re-derive an\n            // id->state map from the pre-reorder pairing (tracksRef/trackStatesRef\n            // reflect the last committed render, i.e. before this mutation) and\n            // re-project it onto the new track order. Guarded to same-length\n            // arrays (addTrack/removeTrack already flow through a full rebuild\n            // that reinitializes trackStates) and to an actual order change (so\n            // non-reorder structural mutations like moveClip/trimClip/splitClip\n            // — which don't change track order — are a no-op here).\n            const oldTracks = tracksRef.current;\n            const oldStates = trackStatesRef.current;\n            if (oldTracks.length === oldStates.length && oldTracks.length === state.tracks.length) {\n              const idToState = new Map(oldTracks.map((t, i) => [t.id, oldStates[i]]));\n              const reordered = state.tracks.map((t, i) => idToState.get(t.id) ?? oldStates[i]);\n              const orderChanged = reordered.some((s, i) => s !== oldStates[i]);\n              if (orderChanged) {\n                setTrackStates(reordered);\n              }\n            }\n\n            if (onTracksChange) {\n              onTracksChange(state.tracks);\n            } else {\n              console.warn(\n                '[waveform-playlist] Engine tracks changed but onTracksChange prop is not set — ' +\n                  'UI will revert on next render. Pass onTracksChange to WaveformPlaylistProvider.'\n              );\n            }\n          }\n        });\n\n        // Engine-initiated stop mirror (final-review CRITICAL 1, #608 follow-up).\n        // PlaylistEngine self-stops when a bounded play(start, end) completes on\n        // an adapter that reports onPlaybackEnded (Tone) — see engine CLAUDE.md\n        // \"onPlaybackEnded hook\". That self-stop rewinds the engine's own current\n        // time to playStartPosition BEFORE the rAF loop's own end-of-playback\n        // checks (below) can observe it, so the loop's `time >= playbackEndTimeRef`\n        // / `time >= duration` comparisons never fire — React isPlaying stays\n        // true and the loop spins forever at the rewound position. Mirroring the\n        // engine's 'stop' event makes React authoritative-from-engine for ANY\n        // engine-initiated stop, closing that race. The engine already rewound\n        // its own time (`_currentTime = _playStartPosition`) — we only mirror\n        // that into React's refs/state, we never seek() again here.\n        //\n        // Suppressed via suppressEngineStopMirrorRef around provider-initiated\n        // engine.stop() calls that immediately seek/restart playback (play(),\n        // setSelection, reschedulePlayback, engine-rebuild-with-resume) — those\n        // already set the correct target position themselves; an unguarded\n        // mirror would clobber it mid-transition with the pre-restart position.\n        // Stop paths that terminate playback (stop(), the rAF loop's own\n        // annotation/selection/duration-end branches) don't need suppression:\n        // they either write the identical value the mirror would (both read\n        // _playStartPosition), or run their own setCurrentTimeRefs call\n        // synchronously right after this listener, which — as plain refs, not\n        // batched state — simply overwrites the mirror's write with no other\n        // code observing the transient value in between.\n        engine.on('stop', () => {\n          if (suppressEngineStopMirrorRef.current) return;\n          setIsPlaying(false);\n          stopAnimationFrameLoop();\n          const stoppedAt = engine.getCurrentTime();\n          setCurrentTimeRefs(stoppedAt);\n          setCurrentTime(stoppedAt);\n        });\n\n        engine.setTracks(tracksWithState);\n        suppressTracksMirroring = false;\n        lastTracksVersionRef.current = engine.getState().tracksVersion;\n        engineRef.current = engine;\n\n        setIsReady(true);\n\n        // Dispatch custom event for external listeners\n        const event = new CustomEvent('waveform-playlist:ready', {\n          detail: {\n            trackCount: tracks.length,\n            duration: maxDuration,\n          },\n        });\n        window.dispatchEvent(event);\n\n        prevTracksRef.current = tracks;\n\n        onReady?.();\n      } catch (error) {\n        // Dispose an adapter created before the engine failed to build (e.g. invalid\n        // zoomLevels make `new PlaylistEngine` throw) — the cleanup skips it because\n        // engineRef was never assigned, so it would otherwise leak a live AudioContext.\n        if (!engineRef.current && adapterRef.current) {\n          try {\n            adapterRef.current.dispose();\n          } catch (disposeErr) {\n            console.warn(\n              '[waveform-playlist] adapter dispose after load failure threw: ' + String(disposeErr)\n            );\n          }\n          adapterRef.current = null;\n        }\n        console.warn('[waveform-playlist] Error loading audio: ' + String(error));\n        onError?.(error instanceof Error ? error : new Error(String(error)));\n      }\n    };\n\n    loadAudio();\n\n    return () => {\n      cancelled = true;\n      // Skip disposal when the next render's guard will keep the engine alive.\n      // skipEngineDisposeRef is set during the render phase (before this cleanup runs).\n      if (skipEngineDisposeRef.current) {\n        skipEngineDisposeRef.current = false;\n        return;\n      }\n      stopAnimationFrameLoop();\n      if (engineRef.current) {\n        engineRef.current.dispose();\n        adapterRef.current = null;\n      }\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [\n    tracks,\n    // isEngineTracks is intentionally excluded — it's a render-phase guard read\n    // inside the effect body, not a trigger. Including it causes a spurious re-run\n    // when it flips from true→false after engineTracksRef is cleared.\n    // isPlaying is intentionally excluded — read from isPlayingRef inside the\n    // effect body. Including it causes a full engine+playout rebuild on every\n    // play/pause/stop, destroying and recreating all audio Players.\n    // soundFontCache is deliberately excluded — late cache changes are forwarded\n    // to the live adapter by the sync effect below; only adapter creation reads it\n    // (via soundFontCacheRef).\n    onReady,\n    onError,\n    effects,\n    stopAnimationFrameLoop,\n    onSelectionEngineState,\n    onLoopEngineState,\n    onSelectedTrackEngineState,\n    onZoomEngineState,\n    onVolumeEngineState,\n    onUndoEngineState,\n    onTracksChange,\n    masterVolumeRef,\n    selectionStartRef,\n    selectionEndRef,\n    loopStartRef,\n    loopEndRef,\n    isLoopEnabledRef,\n    stableZoomLevels,\n    deferEngineRebuild,\n  ]);\n\n  // Forward late-arriving / swapped SoundFontCache to the live adapter so\n  // MIDI tracks upgrade from PolySynth without an engine rebuild. On mount\n  // this may run against a just-created adapter (the loadAudio effect is\n  // declared earlier, runs first, and assigns adapterRef synchronously) or\n  // against null (empty tracks). Both are safe: the helper no-ops on null,\n  // and the adapter skips MIDI tracks whose routing is unchanged.\n  useEffect(() => {\n    syncSoundFontCacheToAdapter(adapterRef.current, soundFontCache);\n  }, [soundFontCache]);\n\n  // Regenerate peaks when zoom, mono, or waveformDataCache changes (without reloading audio)\n  // Peak sources in priority order:\n  //   A) clip.waveformData — external pre-computed peaks (e.g. from BBC audiowaveform)\n  //   B) waveformDataCache — worker-generated WaveformData (fast resample on zoom)\n  //   C) empty peaks — clip is still loading or has no audio data\n  useEffect(() => {\n    if (tracks.length === 0) return;\n\n    const allTrackPeaks: TrackClipPeaks[] = tracks.map((track) => {\n      const clipPeaks: ClipPeaks[] = track.clips.map((clip) => {\n        let peaks: PeakData | undefined;\n\n        // Path A: External pre-computed waveform data (e.g. from audiowaveform .dat file)\n        // When sample rates match, use clip offsets directly.\n        // When rates mismatch (e.g., waveformData at 48000 Hz, clip rebuilt at 44100 Hz\n        // after audio decode), convert offsets to the waveformData's sample space and\n        // adjust samplesPerPixel so peaks fill the correct container width.\n        // Path B (worker) replaces these on next render once cache is populated.\n        if (clip.waveformData) {\n          try {\n            const wdRate = clip.waveformData.sample_rate;\n            const clipRate = clip.sampleRate;\n            let peakOffset = clip.offsetSamples;\n            let peakDuration = clip.durationSamples;\n            let peakSpp = samplesPerPixel;\n            if (wdRate !== clipRate && clipRate > 0 && wdRate > 0) {\n              const ratio = wdRate / clipRate;\n              peakOffset = Math.round(clip.offsetSamples * ratio);\n              peakDuration = Math.round(clip.durationSamples * ratio);\n              peakSpp = Math.max(1, Math.round(samplesPerPixel * ratio));\n            }\n            peaks = extractPeaksFromWaveformDataFull(\n              clip.waveformData as WaveformData,\n              peakSpp,\n              mono,\n              peakOffset,\n              peakDuration\n            );\n          } catch (err) {\n            console.warn(\n              '[waveform-playlist] Failed to extract peaks from waveformData: ' + String(err)\n            );\n          }\n        }\n\n        // Path B: Worker-generated WaveformData cache (fast resample on zoom)\n        if (!peaks) {\n          const cached = waveformDataCache.get(clip.id);\n          if (cached) {\n            try {\n              peaks = extractPeaksFromWaveformDataFull(\n                cached,\n                samplesPerPixel,\n                mono,\n                clip.offsetSamples,\n                clip.durationSamples\n              );\n            } catch (err) {\n              console.warn(\n                '[waveform-playlist] Failed to extract peaks from cache: ' + String(err)\n              );\n            }\n          }\n        }\n\n        // Path C: No peaks data available yet — render empty while worker processes.\n        // Use correct channel count from audioBuffer to prevent track height shift\n        // when peaks arrive (mono mode collapses to 1 channel).\n        if (!peaks) {\n          if (!deferEngineRebuild && !clip.audioBuffer && !clip.waveformData && !clip.midiNotes) {\n            console.warn(\n              `[waveform-playlist] Clip \"${clip.id}\" has no audio data or waveform data`\n            );\n          }\n          const numChannels = mono ? 1 : (clip.audioBuffer?.numberOfChannels ?? 1);\n          // MIDI clips: derive pixel width from sample duration so the clip renders\n          // at the correct width instead of zero-width blank tracks.\n          const pixelLength = clip.midiNotes\n            ? Math.ceil(clip.durationSamples / samplesPerPixel)\n            : 0;\n          peaks = {\n            length: pixelLength,\n            data: Array.from({ length: numChannels }, () => new Int16Array(pixelLength * 2)),\n            bits: 16,\n          };\n        }\n\n        return {\n          clipId: clip.id,\n          trackName: track.name,\n          peaks,\n          startSample: clip.startSample,\n          durationSamples: clip.durationSamples,\n          fadeIn: clip.fadeIn,\n          fadeOut: clip.fadeOut,\n          midiNotes: clip.midiNotes,\n          sampleRate: clip.sampleRate,\n          offsetSamples: clip.offsetSamples,\n        };\n      });\n\n      return clipPeaks;\n    });\n\n    setPeaksDataArray(allTrackPeaks);\n  }, [tracks, samplesPerPixel, mono, waveformDataCache, deferEngineRebuild]);\n\n  // Audio clock from the adapter's own AudioContext (#510 — replaces tone's\n  // getContext()). Stable; reads a ref so identity never changes.\n  const getAudioContextTime = useCallback(\n    () => adapterRef.current?.audioContext.currentTime ?? 0,\n    []\n  );\n\n  // Returns current playback time from engine (auto-wraps at loop boundaries).\n  // Falls back to manual calculation when engine is unavailable.\n  const getPlaybackTimeFallbackWarnedRef = useRef(false);\n  const getPlaybackTime = useCallback(() => {\n    if (engineRef.current) {\n      return engineRef.current.getCurrentTime();\n    }\n    // Fallback: manual calculation (does not handle loop wrapping)\n    if (!getPlaybackTimeFallbackWarnedRef.current) {\n      getPlaybackTimeFallbackWarnedRef.current = true;\n      console.warn(\n        '[waveform-playlist] getPlaybackTime called without engine. ' +\n          'Falling back to manual elapsed time (loop wrapping will not work).'\n      );\n    }\n    const elapsed = getAudioContextTime() - (playbackStartTimeRef.current ?? 0);\n    return (audioStartPositionRef.current ?? 0) + elapsed;\n  }, [getAudioContextTime]);\n\n  // Resting (non-playing) cursor positions display the raw time unchanged.\n  // Latency compensation (outputLatency + lookAhead) models the gap between\n  // the Transport's scheduling position and audible output — a property of\n  // *playing*, not of position. A seeked/paused/stopped cursor has no audible\n  // counterpart; the commanded position is the truth. During playback the\n  // animation loop uses engine.getAudibleTime() instead.\n  const toVisualTime = useCallback((rawTime: number): number => {\n    return Number.isFinite(rawTime) ? Math.max(0, rawTime) : 0;\n  }, []);\n\n  // Pair-write helper for the dual-ref pattern. Storage stays raw on\n  // `currentTimeRef`; display gets the guard-clamped raw version on `visualTimeRef`\n  // (compensation happens in the animation loop via engine.getAudibleTime() while playing).\n  // Use this at every pause/seek/stop/loop site — never assign one ref alone\n  // (browser/CLAUDE.md: \"Display uses visualTimeRef\").\n  const setCurrentTimeRefs = useCallback(\n    (rawTime: number) => {\n      currentTimeRef.current = rawTime;\n      visualTimeRef.current = toVisualTime(rawTime);\n    },\n    [toVisualTime]\n  );\n\n  // Read the engine's scheduler lookahead (Tone adapter ~0.1s, native 0).\n  // Exposed through PlaybackAnimationContextValue so consumers (e.g., recording\n  // live preview) compute audible-latency from the same source as the playhead.\n  const getLookAhead = useCallback((): number => {\n    return engineRef.current?.lookAhead ?? 0;\n  }, []);\n\n  const getOutputLatency = useCallback((): number => {\n    const audioCtx = adapterRef.current?.audioContext;\n    return audioCtx && 'outputLatency' in audioCtx ? (audioCtx as AudioContext).outputLatency : 0;\n  }, []);\n\n  const registerFrameCallback = useCallback((id: string, cb: (data: FrameData) => void) => {\n    frameCallbacksRef.current.set(id, cb);\n  }, []);\n\n  const unregisterFrameCallback = useCallback((id: string) => {\n    frameCallbacksRef.current.delete(id);\n  }, []);\n\n  // Animation loop\n  const startAnimationLoop = useCallback(() => {\n    const updateTime = () => {\n      // Get current time from engine (auto-wraps at loop boundaries via Transport.seconds)\n      const time = getPlaybackTime();\n      currentTimeRef.current = time;\n\n      // Compute visual time once — all visual consumers use this same value.\n      // engine.getAudibleTime() subtracts outputLatency (hardware DAC delay)\n      // and adapter.lookAhead (Tone.js Transport runs ~100ms ahead of audible),\n      // holding at the play-start position during the pre-roll window. Native\n      // adapters report lookAhead 0, so this is a near no-op there.\n      const visualRaw = engineRef.current ? engineRef.current.getAudibleTime() : time;\n      const visualTime = Number.isFinite(visualRaw) ? Math.max(0, visualRaw) : 0;\n      visualTimeRef.current = visualTime;\n\n      // Drive registered per-frame callbacks BEFORE stop checks so the\n      // final frame renders at the correct stop position (not one frame behind).\n      const sr = sampleRateRef.current;\n      const spp = samplesPerPixelRef.current;\n      const frameData: FrameData = {\n        time,\n        visualTime,\n        sampleRate: sr,\n        samplesPerPixel: spp,\n      };\n      for (const cb of frameCallbacksRef.current.values()) {\n        cb(frameData);\n      }\n\n      // Handle annotation playback based on continuous play mode\n      const currentAnnotations = annotationsRef.current;\n      if (currentAnnotations.length > 0) {\n        const currentAnnotation = currentAnnotations.find(\n          (ann) => time >= ann.start && time < ann.end\n        );\n\n        if (continuousPlayRef.current) {\n          // Continuous play ON: update active annotation, let audio play to the end\n          if (currentAnnotation && currentAnnotation.id !== activeAnnotationIdRef.current) {\n            setActiveAnnotationId(currentAnnotation.id);\n          } else if (!currentAnnotation && activeAnnotationIdRef.current !== null) {\n            // Clear the active annotation when we're past it, but don't stop playback\n            // Let playback continue until the audio actually ends (handled by duration check)\n            setActiveAnnotationId(null);\n          }\n        } else {\n          // Continuous play OFF: stop at end of current annotation\n          if (activeAnnotationIdRef.current) {\n            const activeAnnotation = currentAnnotations.find(\n              (ann) => ann.id === activeAnnotationIdRef.current\n            );\n            if (activeAnnotation && time >= activeAnnotation.end) {\n              // Stop playback at end of current annotation\n              if (engineRef.current) {\n                engineRef.current.stop();\n              }\n              setIsPlaying(false);\n              setCurrentTimeRefs(playStartPositionRef.current);\n              setCurrentTime(playStartPositionRef.current);\n              return;\n            }\n          } else {\n            // If no active annotation ID is set, use the current annotation\n            if (currentAnnotation) {\n              setActiveAnnotationId(currentAnnotation.id);\n            }\n          }\n        }\n      }\n\n      // Handle automatic scroll — uses shared visualTime for alignment with playhead\n      if (isAutomaticScrollRef.current && scrollContainerRef.current && duration > 0) {\n        const container = scrollContainerRef.current;\n        const pixelPosition = (visualTime * sr) / spp;\n        const containerWidth = container.clientWidth;\n\n        // Continuously scroll to keep playhead centered.\n        // Math.round prevents sub-pixel jitter from browser integer rounding.\n        const targetScrollLeft = Math.round(Math.max(0, pixelPosition - containerWidth / 2));\n        container.scrollLeft = targetScrollLeft;\n      }\n\n      // Check if we've reached the playback end time (for selection playback)\n      if (playbackEndTimeRef.current !== null && time >= playbackEndTimeRef.current) {\n        // Stop playback at selection end (selection playback is separate from looping)\n        if (engineRef.current) {\n          engineRef.current.stop();\n        }\n        setIsPlaying(false);\n        setCurrentTimeRefs(playbackEndTimeRef.current);\n        setCurrentTime(playbackEndTimeRef.current);\n        playbackEndTimeRef.current = null; // Clear the end time\n        return;\n      }\n\n      // Loop is handled natively by Tone.js Transport (Transport.loop/loopStart/loopEnd).\n      // Transport.seconds auto-wraps at loop boundaries, so getPlaybackTime() returns\n      // the correct position without manual detection here.\n\n      // Auto-stop at the end of the timeline (player-style default), unless:\n      // - indefinitePlayback: the transport rolls until an explicit\n      //   stop/pause (DAW-style, matching dawcore's indefinite-playback), or\n      // - a recording session is active: overdub playback must run past the\n      //   end of existing material (#589/#590).\n      // Explicit ends (selection playback, annotation boundaries) are\n      // handled above regardless.\n      if (time >= duration && !indefinitePlaybackRef.current && !recordingActiveRef.current) {\n        // Stop playback - inline to avoid circular dependency\n        if (engineRef.current) {\n          engineRef.current.stop();\n        }\n        setIsPlaying(false);\n        setCurrentTimeRefs(playStartPositionRef.current);\n        setCurrentTime(playStartPositionRef.current);\n        setActiveAnnotationId(null);\n        return;\n      }\n      startAnimationFrameLoop(updateTime);\n    };\n    startAnimationFrameLoop(updateTime);\n  }, [\n    duration,\n    setActiveAnnotationId,\n    startAnimationFrameLoop,\n    getPlaybackTime,\n    setCurrentTimeRefs,\n  ]);\n\n  const stopAnimationLoop = stopAnimationFrameLoop;\n\n  // Restart animation loop and reschedule playout when continuousPlay changes during playback\n  // This ensures the loop always has the current continuousPlay value\n  // and removes duration limits when switching to continuous play\n  useEffect(() => {\n    const reschedulePlayback = async () => {\n      if (isPlaying && animationFrameRef.current && engineRef.current) {\n        // When toggling continuous play ON, reschedule playout without duration limit\n        // so audio continues past the current annotation boundary\n        if (continuousPlay) {\n          const currentPos = currentTimeRef.current;\n\n          // Stop current playout (which may have duration limit + pause callback).\n          // Suppressed: this immediately restarts playback below — see\n          // suppressEngineStopMirrorRef declaration.\n          suppressEngineStopMirrorRef.current = true;\n          engineRef.current.stop();\n          suppressEngineStopMirrorRef.current = false;\n          stopAnimationLoop();\n\n          const timeNow = getAudioContextTime();\n          playbackStartTimeRef.current = timeNow;\n          audioStartPositionRef.current = currentPos;\n\n          // Play without duration - will play to end of track\n          engineRef.current.play(currentPos);\n          startAnimationLoop();\n        } else {\n          // Just restart animation loop for continuous play OFF\n          stopAnimationLoop();\n          startAnimationLoop();\n        }\n      }\n    };\n\n    reschedulePlayback().catch((err) => {\n      console.warn('[waveform-playlist] Failed to reschedule playback:', err);\n      setIsPlaying(false);\n      stopAnimationLoop();\n    });\n  }, [\n    continuousPlay,\n    isPlaying,\n    startAnimationLoop,\n    stopAnimationLoop,\n    animationFrameRef,\n    getAudioContextTime,\n  ]);\n\n  // Resume playback after tracks change (e.g., after splitting a clip during playback)\n  useEffect(() => {\n    const resumePlayback = async () => {\n      if (pendingResumeRef.current && engineRef.current) {\n        const { position } = pendingResumeRef.current;\n        pendingResumeRef.current = null;\n\n        const timeNow = getAudioContextTime();\n        playbackStartTimeRef.current = timeNow;\n        audioStartPositionRef.current = position;\n\n        if (!audioInitializedRef.current) {\n          await engineRef.current.init();\n          audioInitializedRef.current = true;\n        }\n\n        engineRef.current.play(position);\n        setIsPlaying(true);\n        startAnimationLoop();\n      }\n    };\n\n    resumePlayback().catch((err) => {\n      console.warn('[waveform-playlist] Failed to resume playback after track change:', err);\n      setIsPlaying(false);\n      stopAnimationLoop();\n    });\n  }, [tracks, startAnimationLoop, stopAnimationLoop, getAudioContextTime]);\n\n  // Playback controls\n  const play = useCallback(\n    async (startTime?: number, playDuration?: number) => {\n      if (!engineRef.current) return;\n\n      const actualStartTime = startTime ?? currentTimeRef.current;\n      playStartPositionRef.current = actualStartTime;\n\n      // Update currentTimeRef to match the actual start position\n      // This ensures the animation loop starts from the correct position\n      setCurrentTimeRefs(actualStartTime);\n\n      // Stop existing playback and animation loop before restarting.\n      // engine.stop() resets engine._currentTime to _playStartPosition (could be 0)\n      // and sets _isPlaying=false. AnimatedPlayhead's RAF may fire during this\n      // window and call engine.getCurrentTime() which would return 0.\n      // The seek() immediately sets _currentTime to the intended start position,\n      // preventing the cursor flash.\n      // Suppressed: seek()+play() below immediately re-establish the correct\n      // position — see suppressEngineStopMirrorRef declaration.\n      suppressEngineStopMirrorRef.current = true;\n      engineRef.current.stop();\n      suppressEngineStopMirrorRef.current = false;\n      engineRef.current.seek(actualStartTime);\n      stopAnimationLoop();\n\n      // Record timing for accurate position tracking using the adapter's AudioContext\n      const startTimeNow = getAudioContextTime();\n      playbackStartTimeRef.current = startTimeNow;\n      audioStartPositionRef.current = actualStartTime;\n\n      // Set playback end time if playing with duration (e.g., selection playback)\n      playbackEndTimeRef.current =\n        playDuration !== undefined ? actualStartTime + playDuration : null;\n\n      // Don't set up playback complete callback for annotations\n      // The animation loop handles stopping at annotation boundaries\n      // This avoids callback timing issues when switching between annotations\n\n      // Ensure AudioContext is resumed (requires user gesture, no-op after first call).\n      // Guarded by ref so subsequent plays skip the await entirely.\n      if (!audioInitializedRef.current) {\n        await engineRef.current.init();\n        audioInitializedRef.current = true;\n      }\n\n      const endTime = playDuration !== undefined ? actualStartTime + playDuration : undefined;\n      try {\n        engineRef.current.play(actualStartTime, endTime);\n      } catch (err) {\n        console.warn('[waveform-playlist] Playback failed to start:', err);\n        stopAnimationLoop();\n        return;\n      }\n      setIsPlaying(true);\n      startAnimationLoop();\n    },\n    [startAnimationLoop, stopAnimationLoop, setCurrentTimeRefs, getAudioContextTime]\n  );\n\n  const pause = useCallback(() => {\n    if (!engineRef.current) return;\n\n    // Get current position from engine (auto-wraps at loop boundaries)\n    const pauseTime = getPlaybackTime();\n\n    engineRef.current.pause();\n    setIsPlaying(false);\n    stopAnimationLoop();\n\n    // Update to the calculated pause position\n    setCurrentTimeRefs(pauseTime);\n    setCurrentTime(pauseTime);\n  }, [stopAnimationLoop, getPlaybackTime, setCurrentTimeRefs]);\n\n  const stop = useCallback(() => {\n    if (!engineRef.current) return;\n\n    engineRef.current.stop();\n    setIsPlaying(false);\n    stopAnimationLoop();\n\n    // Return cursor to where playback started (Audacity-style)\n    setCurrentTimeRefs(playStartPositionRef.current);\n    setCurrentTime(playStartPositionRef.current);\n    setActiveAnnotationId(null);\n  }, [stopAnimationLoop, setActiveAnnotationId, setCurrentTimeRefs]);\n\n  // Seek to a specific time - works whether playing or stopped\n  const seekTo = useCallback(\n    (time: number) => {\n      // Clamp time to valid range\n      const clampedTime = Math.max(0, Math.min(time, duration));\n\n      // Update the current time state\n      setCurrentTimeRefs(clampedTime);\n      setCurrentTime(clampedTime);\n\n      // If currently playing, restart at the new position.\n      // play() handles stop+seek+restart internally.\n      if (isPlaying && engineRef.current) {\n        play(clampedTime);\n      }\n    },\n    [duration, isPlaying, play, setCurrentTimeRefs]\n  );\n\n  // Track controls\n  const setTrackMute = useCallback(\n    (trackIndex: number, muted: boolean) => {\n      const trackId = tracksRef.current[trackIndex]?.id;\n      if (!trackId) return;\n\n      const newStates = [...trackStates];\n      newStates[trackIndex] = { ...newStates[trackIndex], muted };\n      setTrackStates(newStates);\n\n      if (engineRef.current) {\n        engineRef.current.setTrackMute(trackId, muted);\n      }\n    },\n    [trackStates]\n  );\n\n  const setTrackSolo = useCallback(\n    (trackIndex: number, soloed: boolean) => {\n      const trackId = tracksRef.current[trackIndex]?.id;\n      if (!trackId) return;\n\n      const newStates = [...trackStates];\n      newStates[trackIndex] = { ...newStates[trackIndex], soloed };\n      setTrackStates(newStates);\n\n      if (engineRef.current) {\n        engineRef.current.setTrackSolo(trackId, soloed);\n      }\n    },\n    [trackStates]\n  );\n\n  const setTrackVolume = useCallback(\n    (trackIndex: number, volume: number) => {\n      const trackId = tracksRef.current[trackIndex]?.id;\n      if (!trackId) return;\n\n      const newStates = [...trackStates];\n      newStates[trackIndex] = { ...newStates[trackIndex], volume };\n      setTrackStates(newStates);\n\n      if (engineRef.current) {\n        engineRef.current.setTrackVolume(trackId, volume);\n      }\n    },\n    [trackStates]\n  );\n\n  const setTrackPan = useCallback(\n    (trackIndex: number, pan: number) => {\n      const trackId = tracksRef.current[trackIndex]?.id;\n      if (!trackId) return;\n\n      const newStates = [...trackStates];\n      newStates[trackIndex] = { ...newStates[trackIndex], pan };\n      setTrackStates(newStates);\n\n      if (engineRef.current) {\n        engineRef.current.setTrackPan(trackId, pan);\n      }\n    },\n    [trackStates]\n  );\n\n  const reorderTrack = useCallback(\n    (trackId: string, toIndex: number) => {\n      if (!engineRef.current) {\n        console.warn('[waveform-playlist] reorderTrack: engine not ready, call ignored');\n        return;\n      }\n      engineRef.current.reorderTrack(trackId, toIndex);\n    },\n    [engineRef]\n  );\n\n  // @dnd-kit/react's Feedback and OptimisticSortingPlugin sortable plugins\n  // perform raw, React-bypassing DOM node moves (insertAdjacentElement /\n  // replaceWith) as part of their live drag-preview and collision-commit\n  // mechanics — confirmed via MutationObserver instrumentation to physically\n  // relocate the React-owned track-controls row during a track-reorder drag,\n  // leaving its parent/nextSibling different from what React's fiber tree\n  // still believes. That desync is permanent for the session: every LATER\n  // React-driven reorder (button OR drag) computes correct data but silently\n  // fails to patch the DOM for these rows. `SortableTrackControls` already\n  // sets `feedback: 'move'` to avoid the placeholder-splice half of this (see\n  // its comment), but OptimisticSortingPlugin's own `reorder()` DOM splice\n  // fires independently on any drag that commits a real index change and\n  // isn't configurable away without losing live index tracking. Bumping this\n  // epoch forces PlaylistVisualization to re-key (remount) the track-controls\n  // slots after every track-reorder drag ends — committed or canceled, since\n  // corruption can occur before a final index is even decided — discarding\n  // any corrupted fiber state so the NEXT reorder (drag or button) starts\n  // from a clean DOM. Called from ClipInteractionProvider's onDragEnd.\n  const bumpTrackReorderEpoch = useCallback(() => {\n    setTrackReorderEpoch((epoch) => epoch + 1);\n  }, []);\n\n  // Selection — wraps hook setter with playback side-effects (provider concern).\n  const setSelection = useCallback(\n    (start: number, end: number) => {\n      setSelectionEngine(start, end);\n      setCurrentTimeRefs(start);\n      setCurrentTime(start);\n\n      if (isPlaying && engineRef.current) {\n        // Stop + seek before restarting playback from selection start.\n        // engine.stop() resets _currentTime and _isPlaying, creating a window\n        // where getCurrentTime() returns wrong value. seek() immediately\n        // corrects _currentTime so any RAF firing in between reads the\n        // intended position. engine.play() then restarts everything.\n        // Suppressed: seek()+play() below immediately re-establish the correct\n        // position — see suppressEngineStopMirrorRef declaration. Unguarded,\n        // the mirror would also flip isPlaying to false with nothing here to\n        // set it back true (isPlaying was already true on entry to this branch).\n        suppressEngineStopMirrorRef.current = true;\n        engineRef.current.stop();\n        suppressEngineStopMirrorRef.current = false;\n        engineRef.current.seek(start);\n        engineRef.current.play(start);\n      }\n    },\n    [isPlaying, setSelectionEngine, setCurrentTimeRefs]\n  );\n\n  // Memoize setScrollContainer callback\n  const setScrollContainer = useCallback((element: HTMLDivElement | null) => {\n    scrollContainerRef.current = element;\n  }, []);\n\n  // Stable callback ref for onAnnotationsChange to avoid re-creating controls context\n  const onAnnotationsChangeRef = useRef(onAnnotationsChange);\n  onAnnotationsChangeRef.current = onAnnotationsChange;\n\n  const setAnnotations: React.Dispatch<React.SetStateAction<AnnotationData[]>> = useCallback(\n    (action) => {\n      const updated = typeof action === 'function' ? action(annotationsRef.current) : action;\n      if (!onAnnotationsChangeRef.current) {\n        if (process.env.NODE_ENV !== 'production') {\n          console.warn(\n            'waveform-playlist: setAnnotations was called but no onAnnotationsChange callback is provided. ' +\n              'Annotation edits will not persist. Pass onAnnotationsChange to WaveformPlaylistProvider to handle annotation updates.'\n          );\n        }\n        return;\n      }\n      onAnnotationsChangeRef.current(updated);\n    },\n    []\n  );\n\n  const sampleRate = sampleRateRef.current;\n  const timeScaleHeight = timescale ? 30 : 0;\n  const minimumPlaylistHeight = tracks.length * waveHeight + timeScaleHeight;\n\n  // Split context values for performance optimization\n  // Animation context only re-renders consumers on discrete events\n  // (play/pause/stop/seek), never during the animation loop itself\n\n  const animationValue: PlaybackAnimationContextValue = useMemo(\n    () => ({\n      isPlaying,\n      currentTime,\n      currentTimeRef,\n      visualTimeRef,\n      playbackStartTimeRef,\n      audioStartPositionRef,\n      getPlaybackTime,\n      getAudioContextTime,\n      getLookAhead,\n      getOutputLatency,\n      registerFrameCallback,\n      unregisterFrameCallback,\n    }),\n    [\n      isPlaying,\n      currentTime,\n      currentTimeRef,\n      visualTimeRef,\n      playbackStartTimeRef,\n      audioStartPositionRef,\n      getPlaybackTime,\n      getAudioContextTime,\n      getLookAhead,\n      getOutputLatency,\n      registerFrameCallback,\n      unregisterFrameCallback,\n    ]\n  );\n\n  const stateValue: PlaylistStateContextValue = useMemo(\n    () => ({\n      continuousPlay,\n      linkEndpoints,\n      annotationsEditable,\n      isAutomaticScroll,\n      isLoopEnabled,\n      annotations,\n      activeAnnotationId,\n      selectionStart,\n      selectionEnd,\n      selectedTrackId,\n      loopStart,\n      loopEnd,\n      indefinitePlayback,\n      fillViewport,\n      canUndo,\n      canRedo,\n    }),\n    [\n      continuousPlay,\n      linkEndpoints,\n      annotationsEditable,\n      isAutomaticScroll,\n      isLoopEnabled,\n      annotations,\n      activeAnnotationId,\n      selectionStart,\n      selectionEnd,\n      selectedTrackId,\n      loopStart,\n      loopEnd,\n      indefinitePlayback,\n      fillViewport,\n      canUndo,\n      canRedo,\n    ]\n  );\n\n  const setCurrentTimeControl = useCallback(\n    (time: number) => {\n      setCurrentTimeRefs(time);\n      setCurrentTime(time);\n    },\n    [setCurrentTimeRefs]\n  );\n\n  const setAutomaticScrollControl = useCallback((enabled: boolean) => {\n    setIsAutomaticScroll(enabled);\n  }, []);\n\n  const controlsValue: PlaylistControlsContextValue = useMemo(\n    () => ({\n      // Playback controls\n      play,\n      pause,\n      stop,\n      seekTo,\n      setCurrentTime: setCurrentTimeControl,\n\n      // Track controls\n      setTrackMute,\n      setTrackSolo,\n      setTrackVolume,\n      setTrackPan,\n      reorderTrack,\n      bumpTrackReorderEpoch,\n      setTrackDragPreview,\n\n      // Selection\n      setSelection,\n      setSelectedTrackId: setSelectedTrackIdControl,\n\n      // Time format\n      setTimeFormat,\n      formatTime,\n\n      // Zoom\n      zoomIn: zoom.zoomIn,\n      zoomOut: zoom.zoomOut,\n\n      // Master volume\n      setMasterVolume,\n\n      // Automatic scroll\n      setAutomaticScroll: setAutomaticScrollControl,\n      setScrollContainer,\n      scrollContainerRef,\n\n      // Annotation controls\n      setContinuousPlay,\n      setLinkEndpoints,\n      setAnnotationsEditable,\n      setAnnotations,\n      setActiveAnnotationId,\n\n      // Loop controls\n      setLoopEnabled,\n      setLoopRegion,\n      setLoopRegionFromSelection,\n      clearLoopRegion,\n\n      // Undo/redo\n      undo,\n      redo,\n\n      // Recording\n      setRecordingActive,\n    }),\n    [\n      play,\n      pause,\n      stop,\n      seekTo,\n      setCurrentTimeControl,\n      setTrackMute,\n      setTrackSolo,\n      setTrackVolume,\n      setTrackPan,\n      reorderTrack,\n      bumpTrackReorderEpoch,\n      setTrackDragPreview,\n      setSelection,\n      setSelectedTrackIdControl,\n      setTimeFormat,\n      formatTime,\n      zoom.zoomIn,\n      zoom.zoomOut,\n      setMasterVolume,\n      setAutomaticScrollControl,\n      setScrollContainer,\n      scrollContainerRef,\n      setContinuousPlay,\n      setLinkEndpoints,\n      setAnnotationsEditable,\n      setAnnotations,\n      setActiveAnnotationId,\n      setLoopEnabled,\n      setLoopRegion,\n      setLoopRegionFromSelection,\n      clearLoopRegion,\n      undo,\n      redo,\n      setRecordingActive,\n    ]\n  );\n\n  const dataValue: PlaylistDataContextValue = useMemo(\n    () => ({\n      duration,\n      audioBuffers,\n      peaksDataArray,\n      trackStates,\n      tracks,\n      sampleRate,\n      waveHeight,\n      timeScaleHeight,\n      minimumPlaylistHeight,\n      controls,\n      playoutRef: engineRef,\n      samplesPerPixel,\n      timeFormat,\n      masterVolume,\n      canZoomIn: zoom.canZoomIn,\n      canZoomOut: zoom.canZoomOut,\n      barWidth,\n      barGap,\n      roundedBars,\n      progressBarWidth,\n      isReady,\n      trackReorderEpoch,\n      trackDragPreview,\n      mono,\n      isDraggingRef,\n      onTracksChange,\n    }),\n    [\n      duration,\n      audioBuffers,\n      peaksDataArray,\n      trackStates,\n      tracks,\n      sampleRate,\n      waveHeight,\n      timeScaleHeight,\n      minimumPlaylistHeight,\n      controls,\n      engineRef,\n      samplesPerPixel,\n      timeFormat,\n      masterVolume,\n      zoom.canZoomIn,\n      zoom.canZoomOut,\n      barWidth,\n      barGap,\n      roundedBars,\n      progressBarWidth,\n      isReady,\n      trackReorderEpoch,\n      trackDragPreview,\n      mono,\n      isDraggingRef,\n      onTracksChange,\n    ]\n  );\n\n  // Merge user theme with default theme\n  const mergedTheme = { ...defaultTheme, ...userTheme };\n\n  return (\n    <ThemeProvider theme={mergedTheme}>\n      <PlaybackAnimationContext.Provider value={animationValue}>\n        <PlaylistStateContext.Provider value={stateValue}>\n          <PlaylistControlsContext.Provider value={controlsValue}>\n            <PlaylistDataContext.Provider value={dataValue}>\n              {children}\n            </PlaylistDataContext.Provider>\n          </PlaylistControlsContext.Provider>\n        </PlaylistStateContext.Provider>\n      </PlaybackAnimationContext.Provider>\n    </ThemeProvider>\n  );\n};\n\n// Individual hooks for each context - use these for optimal performance\n// Components only re-render when their specific context data changes\n\nexport const usePlaybackAnimation = () => {\n  const context = useContext(PlaybackAnimationContext);\n  if (!context) {\n    throw new Error('usePlaybackAnimation must be used within WaveformPlaylistProvider');\n  }\n  return context;\n};\n\nexport const usePlaylistState = () => {\n  const context = useContext(PlaylistStateContext);\n  if (!context) {\n    throw new Error('usePlaylistState must be used within WaveformPlaylistProvider');\n  }\n  return context;\n};\n\nexport const usePlaylistControls = () => {\n  const context = useContext(PlaylistControlsContext);\n  if (!context) {\n    throw new Error('usePlaylistControls must be used within WaveformPlaylistProvider');\n  }\n  return context;\n};\n\nexport const usePlaylistData = () => {\n  const context = useContext(PlaylistDataContext);\n  if (!context) {\n    throw new Error('usePlaylistData must be used within WaveformPlaylistProvider');\n  }\n  return context;\n};\n\n/**\n * Like {@link usePlaylistData} but returns `null` instead of throwing when there\n * is no WaveformPlaylistProvider ancestor. Use this in hooks/components that may\n * also run in the MediaElement path (MediaElementPlaylistProvider), where the\n * WebAudio playlist context is absent.\n */\nexport const usePlaylistDataOptional = () => useContext(PlaylistDataContext);\n","import React, { useEffect, useRef } from 'react';\nimport type { WamPluginInstance, WamParameterPanelNode } from '@dawcore/wam';\nimport { loadWamModule } from '../effects/loadWam';\n\nexport interface WamEffectGuiProps {\n  /** Live plugin handle from getWamPlugin/getTrackWamPlugin. */\n  plugin: WamPluginInstance | undefined;\n  className?: string;\n}\n\n/**\n * Mounts a WAM plugin's own GUI (plugin.createGui), falling back to the\n * generic parameter panel from @dawcore/wam for headless plugins. The GUI is\n * destroyed on unmount — GUI and audio lifecycles are independent, so this\n * never interrupts sound.\n */\nexport const WamEffectGui: React.FC<WamEffectGuiProps> = ({ plugin, className }) => {\n  const hostRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    const host = hostRef.current;\n    if (!plugin || !host) return;\n    let cancelled = false;\n    let gui: HTMLElement | null = null;\n\n    (async () => {\n      try {\n        if (plugin.createGui) {\n          gui = await plugin.createGui();\n        } else {\n          const wam = await loadWamModule();\n          gui = await wam.createWamParameterPanel(\n            plugin.audioNode as unknown as WamParameterPanelNode\n          );\n        }\n        if (cancelled) {\n          if (gui) {\n            try {\n              plugin.destroyGui?.(gui);\n            } catch (destroyErr) {\n              console.warn(\n                '[waveform-playlist] Error destroying WAM GUI: ' +\n                  (destroyErr instanceof Error ? destroyErr.message : String(destroyErr))\n              );\n            }\n          }\n          gui = null;\n          return;\n        }\n        host.innerHTML = '';\n        host.appendChild(gui);\n      } catch (err) {\n        console.warn(\n          '[waveform-playlist] Failed to create WAM GUI: ' +\n            (err instanceof Error ? err.message : String(err))\n        );\n      }\n    })();\n\n    return () => {\n      cancelled = true;\n      if (gui) {\n        gui.remove();\n        try {\n          plugin.destroyGui?.(gui);\n        } catch (destroyErr) {\n          console.warn(\n            '[waveform-playlist] Error destroying WAM GUI: ' +\n              (destroyErr instanceof Error ? destroyErr.message : String(destroyErr))\n          );\n        }\n      }\n    };\n  }, [plugin]);\n\n  return <div ref={hostRef} className={className ?? 'wam-effect-gui'} />;\n};\n"],"mappings":"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","names":["useRef","useState","useCallback","useRef","useEffect","connect","_a","connect","connect","Analyser","connect","useState","useRef","useCallback","connect","useEffect","Analyser","useState","useCallback","useRef","useEffect","isNativeGlobalContext","getGlobalAudioContext","connect","useState","useRef","useCallback","connect","isNativeGlobalContext","getGlobalAudioContext","useEffect","useState","useCallback","getGlobalAudioContext","getContext","isNativeGlobalContext","isNativeGlobalContext","getContext","useState","useCallback","getGlobalAudioContext","Volume","Gain","useState","useCallback","useRef","useEffect","createTrack","createClipFromSeconds","getGlobalAudioContext","arrayBuffer","audioBuffer","useState","useRef","useEffect","useCallback","getGlobalAudioContext","createTrack","createClipFromSeconds","useEffect","useState","useRef","useCallback","useState","useRef","useCallback","useEffect","_a","useState","useEffect","useRef","useCallback","useMemo","jsx","useEffect","useRef","jsx","useRef","useEffect"]}