@signalsandsorcery/plugin-sdk 2.25.1 → 2.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +235 -6
- package/dist/index.d.ts +235 -6
- package/dist/index.js +702 -185
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +695 -185
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/types/plugin-sdk.types.ts","../src/types/fx-toggle.types.ts","../src/components/TrackRow.tsx","../src/components/TrackDrawer.tsx","../src/constants/fx-presets.ts","../src/components/FxToggleBar.tsx","../src/components/PianoRollEditor.tsx","../src/components/ConfirmDialog.tsx","../src/components/Modal.tsx","../src/components/LevelMeter.tsx","../src/hooks/useTrackLevels.ts","../src/components/TrackMeterStrip.tsx","../src/components/VolumeSlider.tsx","../src/utils/volume-conversion.ts","../src/components/PanSlider.tsx","../src/components/SorceryProgressBar.tsx","../src/components/CrossfadeTrackRow.tsx","../src/crossfade-meta.ts","../src/crossfade-inpaint.ts","../src/components/ImportTrackModal.tsx","../src/components/CrossfadeModal.tsx","../src/components/DownloadPackButton.tsx","../src/components/SamplePackCTACard.tsx","../src/components/WaveformView.tsx","../src/components/waveform.ts","../src/components/ScrollingWaveform.tsx","../src/components/OffsetScrubber.tsx","../src/components/wavPeakAnalyzer.ts","../src/components/synthesizeCuePoints.ts","../src/hooks/useSceneState.ts","../src/hooks/useAnySolo.ts","../src/hooks/useSoundHistory.ts","../src/hooks/useTrackReorder.ts","../src/constants/sdk-version.ts","../src/utils/format-concurrent-tracks.ts","../src/utils/semantic-match.ts"],"sourcesContent":["/**\n * @sas/plugin-sdk — Public API\n *\n * Everything an external plugin author needs to build a generator plugin\n * for Signals & Sorcery.\n */\n\n// ============================================================================\n// Types — Core plugin contract\n// ============================================================================\n\nexport type {\n GeneratorType,\n InstrumentDescriptor,\n GeneratorPlugin,\n PluginUIProps,\n PluginHost,\n ExportedPluginData,\n CreateTrackOptions,\n PluginTrackHandle,\n PluginTrackInfo,\n ImportCandidateTrack,\n ImportCandidateScene,\n SceneFamilyTrack,\n TrackSoundSnapshot,\n ListImportableTracksOptions,\n PluginSynthInfo,\n PluginTrackRuntimeState,\n TrackStateChangeListener,\n PluginFxCategoryDetailState,\n PluginTrackFxDetailState,\n MidiClipData,\n PluginMidiNote,\n MidiWriteResult,\n ReadMidiClip,\n ReadMidiResult,\n ExportMidiBundleOptions,\n ExportMidiBundleResult,\n PostProcessOptions,\n MusicalContext,\n PluginChordTiming,\n PluginGenerationContext,\n PluginConcurrentTrackInfo,\n PluginChordSegment,\n TransportEvent,\n DeckBoundaryEvent,\n PluginTransportState,\n PluginTrackLevel,\n PluginSceneInfo,\n PluginSceneContext,\n BulkAddPlaceholderTrack,\n TransportEventListener,\n DeckBoundaryListener,\n SceneChangeListener,\n UnsubscribeFn,\n LLMGenerationRequest,\n LLMGenerationResult,\n // Tool-use LLM types — agentic plugins (chat panel, etc.) use these via\n // `host.generateWithLLMTools` to drive a Claude-Code-style loop. SDK 2.4.0+.\n LLMPart,\n LLMContent,\n LLMFunctionDeclaration,\n LLMTool,\n LLMGenerationConfig,\n LLMSystemInstruction,\n LLMToolUseRequest,\n LLMUsageMetadata,\n LLMCandidate,\n LLMToolUseResponse,\n PluginPresetData,\n ShufflePresetResult,\n SoundHistoryEntry,\n PluginSettingsSchema,\n SettingDefinition,\n PluginSettingsStore,\n // AI skill surface — lets plugins declare LLM-callable actions\n // registered as namespaced tools (plugin:<id>:<skill>). Required for\n // plugins that expose a `chat` or similar agent-delegation skill.\n PluginSkill,\n PluginSkillInputSchema,\n PluginErrorCode,\n PluginManifest,\n PluginCapabilities,\n PluginFileDialogOptions,\n PluginDownloadOptions,\n PluginHttpRequestOptions,\n PluginHttpResponse,\n PluginSampleFilter,\n PluginSampleInfo,\n PluginSampleImportResult,\n PluginSampleTrackInfo,\n PluginAudioTextureRequest,\n PluginAudioTextureResult,\n PluginCuePoints,\n PluginTrimWindow,\n ComposeSceneOptions,\n ComposeSceneResult,\n ComposeProgressListener,\n ComposeProgressEvent,\n PluginPresetInfo,\n SavePluginPresetOptions,\n PluginAppTool,\n PluginAppToolInputSchema,\n PluginAppToolResult,\n PluginStatus,\n PluginRegistration,\n StemType,\n PluginStemSplitResult,\n PluginStemTrackInfo,\n // Audio recording (since SDK 2.1.0)\n AudioInputDevice,\n RecordingTargetInfo,\n RecordingChunkFinalizedEvent,\n // Drum sampler (since SDK 1.2.0)\n DrumKit,\n // Pitched instrument sampler (since SDK 1.3.0)\n InstrumentZone,\n InstrumentSampler,\n ListAudioFilesOptions,\n} from './types/plugin-sdk.types';\n\nexport { PluginError } from './types/plugin-sdk.types';\n\n// ============================================================================\n// Types — FX toggle system\n// ============================================================================\n\nexport type {\n FxCategory,\n FxPreset,\n MixInterpolation,\n FxPresetConfig,\n FxCategoryDetailState,\n TrackFxDetailState,\n TrackFxState,\n FxPresetDataEntry,\n FxPresetData,\n} from './types/fx-toggle.types';\n\nexport {\n FX_CATEGORIES,\n FX_CHAIN_ORDER,\n FX_ENGINE_PLUGIN_NAMES,\n FX_DISPLAY_LABELS,\n EMPTY_FX_STATE,\n DEFAULT_FX_DRY_WET,\n DEFAULT_FX_CATEGORY_DETAIL,\n EMPTY_FX_DETAIL_STATE,\n} from './types/fx-toggle.types';\n\n// ============================================================================\n// Components\n// ============================================================================\n\nexport { TrackRow, type SDKTrackRowProps } from './components/TrackRow';\nexport {\n CrossfadeTrackRow,\n type CrossfadeTrackRowProps,\n type CrossfadeLayer,\n} from './components/CrossfadeTrackRow';\nexport {\n EQUAL_POWER_GAIN,\n parseCrossfadePairs,\n asCrossfadeMeta,\n buildCrossfadeVolumeCurves,\n type CrossfadeSlot,\n type CrossfadeMeta,\n type CrossfadePairMeta,\n type VolumeAutomationPoint,\n type CrossfadeVolumeCurves,\n} from './crossfade-meta';\nexport { buildCrossfadeInpaintPrompt, type CrossfadeInpaintInput } from './crossfade-inpaint';\nexport { ImportTrackModal, type ImportTrackModalProps } from './components/ImportTrackModal';\nexport {\n CrossfadeModal,\n type CrossfadeModalProps,\n type CrossfadeSelection,\n} from './components/CrossfadeModal';\nexport { ConfirmDialog, type ConfirmDialogProps } from './components/ConfirmDialog';\nexport { Modal, type ModalProps } from './components/Modal';\nexport {\n TrackDrawer,\n type TrackDrawerProps,\n type DrawerTab,\n // Backwards-compatible aliases — the drawer was `InstrumentDrawer` before it\n // grew an FX tab + Import tab and became the unified per-track drawer.\n InstrumentDrawer,\n type TrackDrawerProps as InstrumentDrawerProps,\n} from './components/TrackDrawer';\nexport {\n PianoRollEditor,\n type PianoRollEditorProps,\n PX_PER_BEAT,\n ROW_HEIGHT,\n GUTTER_W,\n DRAG_DEAD_ZONE,\n RESIZE_HANDLE_PX,\n pxToCell,\n cellToPx,\n resizeNoteDuration,\n centerScrollTop,\n transposeNotes,\n pitchToName,\n} from './components/PianoRollEditor';\nexport { VolumeSlider } from './components/VolumeSlider';\nexport { PanSlider } from './components/PanSlider';\nexport { FxToggleBar, type FxToggleBarProps } from './components/FxToggleBar';\nexport { SorceryProgressBar, calculateTimeBasedTarget } from './components/SorceryProgressBar';\nexport { DownloadPackButton, type DownloadPackButtonProps, type DownloadPackButtonVariant } from './components/DownloadPackButton';\nexport {\n SamplePackCTACard,\n type SamplePackCTACardProps,\n type SamplePackCTACardStatus,\n type SamplePackCardInfo,\n} from './components/SamplePackCTACard';\n\n// Waveform / audio-clip UI toolkit — shared by audio-oriented plugins (stems,\n// recorder). Promoted from the app's src/plugins/shared (W9 — so extracted\n// plugins reach it through the SDK, not a relative app path). Since 2.10.0.\nexport { WaveformView, type WaveformViewProps } from './components/WaveformView';\nexport { LevelMeter, type LevelMeterProps } from './components/LevelMeter';\nexport { TrackMeterStrip, type TrackMeterStripProps } from './components/TrackMeterStrip';\nexport { ScrollingWaveform, type ScrollingWaveformProps } from './components/ScrollingWaveform';\nexport { OffsetScrubber, type OffsetScrubberProps } from './components/OffsetScrubber';\nexport { computePeaks, drawWaveform, type WaveformPeaks } from './components/waveform';\nexport { analyzeWavPeak, type PeakAnalysis } from './components/wavPeakAnalyzer';\nexport { synthesizeCuePoints, type SynthesizeCuePointsOptions } from './components/synthesizeCuePoints';\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\nexport { useSceneState } from './hooks/useSceneState';\nexport { useAnySolo } from './hooks/useAnySolo';\nexport {\n useSoundHistory,\n type UseSoundHistoryResult,\n type UseSoundHistoryOptions,\n type TrackSoundHistory,\n} from './hooks/useSoundHistory';\nexport {\n useTrackReorder,\n moveItem,\n type UseTrackReorderOptions,\n type UseTrackReorderResult,\n type TrackRowDragProps,\n} from './hooks/useTrackReorder';\nexport {\n useTrackLevels,\n useTrackLevel,\n useTrackMeter,\n useTransportPlaying,\n type TrackLevelsHandle,\n type TrackMeterView,\n} from './hooks/useTrackLevels';\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n// VALID_INSTRUMENT_ROLES (SDK 1.x) removed in 2.0.0 — external plugins now\n// call `host.getValidRoles()` on PluginHost at runtime. The canonical list\n// lives in the assistant (src/music-engine/constants/instrument-classification.ts)\n// and is exposed via that accessor.\nexport { PLUGIN_SDK_VERSION } from './constants/sdk-version';\nexport { FX_PRESET_CONFIGS } from './constants/fx-presets';\n\n// ============================================================================\n// Utils\n// ============================================================================\n\nexport { sliderToDb, dbToSlider, SLIDER_UNITY, DB_MAX, DB_MIN } from './utils/volume-conversion';\nexport { formatConcurrentTracks } from './utils/format-concurrent-tracks';\n\n// Semantic sample matching — pick the closest sample to a text intent by\n// scoring against each sample's StableAudio prompt, with variety-preserving\n// top-k weighted selection. Shared by the drum + instrument resolvers. Since 2.11.0.\nexport {\n tokenizePrompt,\n scorePromptMatch,\n pickTopKWeighted,\n type ScoredCandidate,\n type PickTopKOptions,\n} from './utils/semantic-match';\n","/**\n * Plugin SDK Type Definitions\n *\n * Complete type system for the generator plugin architecture.\n * Plugins implement GeneratorPlugin and interact with the host via PluginHost.\n * All plugin output flows through TracktionEngine (MIDI or audio clips).\n */\n\nimport type { ComponentType, ReactNode } from 'react';\n\n// ============================================================================\n// Core Plugin Interface\n// ============================================================================\n\n/** What kind of Tracktion content this plugin creates */\nexport type GeneratorType = 'midi' | 'audio' | 'sample' | 'hybrid';\n\n/**\n * Drum-kit configuration for `host.setTrackDrumKit`. Prototype shape carries\n * a single sample; future multi-slot kits will extend this with a `notes`\n * map (`Record<midiNote, samplePath>`) for GM-style drum maps.\n */\nexport interface DrumKit {\n /** Absolute path to the sample (WAV, AIFF, FLAC). Triggered on every note-on. */\n samplePath: string;\n}\n\n/**\n * One key-mapped sample zone in a pitched, polyphonic instrument.\n * Used by `host.setTrackInstrumentSampler`.\n *\n * Zones in an InstrumentSampler MUST be disjoint and ordered low to\n * high by rootKey — the engine rejects overlap because Tracktion would\n * otherwise double-trigger every matching sound on each note-on.\n */\nexport interface InstrumentZone {\n /** Absolute path to the zone's sample (WAV, FLAC, AIFF). */\n samplePath: string;\n /** MIDI note this sample sounds at unshifted (0-127). */\n rootKey: number;\n /** Inclusive low end of the key range that triggers this zone (0-127). */\n minKey: number;\n /** Inclusive high end of the key range that triggers this zone (0-127). */\n maxKey: number;\n /**\n * If true, the sampler plays the sample for the duration the note is\n * held and stops on note-off (good for sustaining pads, organs, etc.,\n * whose source has been pre-trimmed to a steady-state region).\n * If false, the sampler plays the sample through to its end ignoring\n * note-off (good for plucks, mallets, percussion).\n */\n openEnded: boolean;\n}\n\n/**\n * Pitched instrument configuration for `host.setTrackInstrumentSampler`.\n * Parallel to `DrumKit` but multi-zone and pitch-aware. A manifest\n * authored by the pitched-sample pipeline reduces to one of these.\n *\n * NOTE: This is distinct from `host.setTrackInstrument(trackId, pluginId)`\n * which loads a VST3/AU synth plugin. `setTrackInstrumentSampler` loads\n * the built-in Tracktion sampler with N pre-rendered zones.\n */\nexport interface InstrumentSampler {\n /** Display name (e.g. \"Bright Warm Pluck\"). Used for diagnostics. */\n name: string;\n /** Disjoint zones, ordered low->high by rootKey. At least one required. */\n zones: ReadonlyArray<InstrumentZone>;\n}\n\n/** Options for `host.listAudioFiles`. */\nexport interface ListAudioFilesOptions {\n /**\n * File extensions to include (dot-prefixed, lowercase). Defaults to\n * `['.wav']`. Other audio formats (`.aif`, `.flac`, `.mp3`) are passed\n * through verbatim; the host does not transcode.\n */\n extensions?: string[];\n /** Walk subdirectories. Defaults to `false`. */\n recursive?: boolean;\n}\n\n/** Describes an available instrument plugin (VST3/AU synth) on the system. */\nexport interface InstrumentDescriptor {\n /** Stable plugin identifier for loading (VST3 TUID or AU component ID) */\n pluginId: string;\n /** Display name */\n name: string;\n /** Plugin manufacturer */\n manufacturer: string;\n /** Plugin format */\n type: 'vst3' | 'au' | 'vst' | 'internal';\n /** Plugin category (from scan) */\n category: string;\n /** Whether this plugin is currently installed/available */\n missing?: boolean;\n}\n\n/** Every generator plugin must implement this interface. */\nexport interface GeneratorPlugin {\n /** Unique ID, npm-style scope: '@sas/synth-generator', '@user/my-plugin' */\n readonly id: string;\n /** Human-readable name shown in accordion header */\n readonly displayName: string;\n /** Semver version string */\n readonly version: string;\n /** Short description for settings/marketplace */\n readonly description: string;\n /** 24x24 icon — data URL, relative path from plugin dir, or undefined */\n readonly icon?: string;\n /** What kind of Tracktion content this plugin creates */\n readonly generatorType: GeneratorType;\n /** Minimum host SDK version this plugin requires */\n readonly minHostVersion?: string;\n\n /**\n * Called once when plugin is loaded. Receives the PluginHost API.\n * If this throws, plugin is marked as failed and not rendered.\n */\n activate(host: PluginHost): Promise<void>;\n\n /**\n * Called when plugin is being unloaded (disable, uninstall, app quit).\n * Must complete within 5 seconds or host force-kills.\n */\n deactivate(): Promise<void>;\n\n /**\n * Return the React component rendered inside the accordion section.\n * Component receives PluginUIProps from the host.\n */\n getUIComponent(): ComponentType<PluginUIProps>;\n\n /**\n * Return JSON Schema for plugin-specific settings.\n * Host auto-renders a settings form. Return null if no settings.\n */\n getSettingsSchema(): PluginSettingsSchema | null;\n\n /**\n * Optional: Called when the active scene changes.\n */\n onSceneChanged?(sceneId: string | null): Promise<void>;\n\n /**\n * Optional: Called when the generation context changes\n * (chords updated, tracks added/removed, BPM changed).\n */\n onContextChanged?(context: MusicalContext): void;\n\n /**\n * Optional: Declare LLM-callable skills this plugin provides.\n * Skills are registered as namespaced tools (plugin:<pluginId>:<skillId>)\n * and become available to AI agents for orchestration.\n *\n * Example: the chat-panel plugin declares a `chat` skill so external\n * agents (Claude Code, OpenClaw) can delegate scene-scoped natural\n * language work to the in-app agent via a single call.\n */\n getSkills?(): PluginSkill[];\n}\n\n// ============================================================================\n// Plugin Skills (AI Harness)\n// ============================================================================\n\n/** An LLM-callable action declared by a plugin. */\nexport interface PluginSkill {\n /** Unique skill id within this plugin (e.g., 'chat', 'generate_bassline') */\n id: string;\n /** Human-readable description — drives LLM tool selection */\n description: string;\n /** JSON Schema for the skill's input parameters */\n inputSchema: PluginSkillInputSchema;\n /** Whether this skill only reads state (no mutations). Default: false */\n isReadOnly?: boolean;\n}\n\n/** JSON Schema shape for skill input parameters. */\nexport interface PluginSkillInputSchema {\n type: 'object';\n properties?: Record<string, unknown>;\n required?: string[];\n}\n\n// ============================================================================\n// Plugin UI Props\n// ============================================================================\n\n/** Props passed to every plugin's React component by the host */\nexport interface PluginUIProps {\n /** The scoped PluginHost API instance for this plugin */\n host: PluginHost;\n /** Currently active scene ID (null if none selected) */\n activeSceneId: string | null;\n /** Whether the user is authenticated (for LLM access) */\n isAuthenticated: boolean;\n /** Whether all systems are connected (engine, gateway) */\n isConnected: boolean;\n /** Which workstation deck this is rendered in */\n deckId?: 'left' | 'right';\n /** Plugin calls this to set/clear header buttons. Pass null to clear. */\n onHeaderContent?: (content: ReactNode | null) => void;\n /** Plugin calls this to show/hide the loading spinner in the header. */\n onLoading?: (loading: boolean) => void;\n /** Scene-level context: contract state, chords, BPM, etc. Null if no scene. */\n sceneContext?: PluginSceneContext | null;\n /** Callback to open the scene selector (Scenes accordion section). */\n onSelectScene?: (() => void) | null;\n /** Callback to open the contract/chords section (for \"Generate a Contract\" CTA). */\n onOpenContract?: (() => void) | null;\n /** Callback to expand this plugin's own accordion section. */\n onExpandSelf?: (() => void) | null;\n /**\n * Whether the host's accordion section for this plugin is currently expanded.\n * Plugin UIs can watch transitions to take focus, refresh data, etc. The host\n * keeps the plugin mounted across collapse/expand to preserve state, so this\n * prop (not mount/unmount) is the signal that the user is actively viewing.\n */\n isExpanded?: boolean;\n}\n\n// ============================================================================\n// PluginHost API\n// ============================================================================\n\n/**\n * Canonical display metadata for a distributable sample pack, sourced from the\n * HOST's pack registry (the same source it uses to download + version-check the\n * bundle). Returned by `host.getSamplePackInfo` so a plugin's download CTA can\n * show the live name / description / size instead of a hardcoded copy that\n * drifts when a new pack version ships. Structurally compatible with\n * `SamplePackCardInfo` (the CTA card prop).\n *\n * @since SDK 2.12.0\n */\nexport interface SamplePackPublicInfo {\n /** Stable pack identifier, e.g. `'sas-instrument-pack'`. */\n packId: string;\n /** Human-readable pack name for the CTA headline. */\n displayName: string;\n /** One-line description of the pack's contents. */\n description: string;\n /** Size in bytes of the default download variant. */\n sizeBytes: number;\n}\n\n/** Scoped API surface that plugins interact with. Plugins NEVER get direct TracktionEngine access. */\nexport interface PluginHost {\n // --- Track Management (ownership-scoped) ---\n\n /** Create a new track in the active scene. Host enforces ownership and scene routing. */\n createTrack(options: CreateTrackOptions): Promise<PluginTrackHandle>;\n\n /** Delete a track previously created by THIS plugin. */\n deleteTrack(trackId: string): Promise<void>;\n\n /** Get all tracks this plugin owns in the active scene. */\n getPluginTracks(): Promise<PluginTrackHandle[]>;\n\n /** Adopt unowned tracks in the active scene matching this plugin's generator type. */\n adoptSceneTracks(): Promise<PluginTrackHandle[]>;\n\n /** Get info about a specific owned track. */\n getTrackInfo(trackId: string): Promise<PluginTrackInfo>;\n\n /** Set track mute state. Only works on owned tracks. */\n setTrackMute(trackId: string, muted: boolean): Promise<void>;\n\n /** Set track volume (linear 0.0 - 1.0). Only works on owned tracks. */\n setTrackVolume(trackId: string, volume: number): Promise<void>;\n /**\n * Set/replace a time-based volume automation curve (a fade envelope) on a track,\n * or clear it with an empty array. Points are {time: seconds, db}; linear between\n * points. Used by crossfade tracks to fade origin↔target across the looped\n * transition. Optional — callers MUST null-check. @since SDK 2.25.0\n */\n setTrackVolumeAutomation?(trackId: string, points: Array<{ time: number; db: number }>): Promise<void>;\n\n /** Set track pan (-1.0 left to 1.0 right). Only works on owned tracks. */\n setTrackPan(trackId: string, pan: number): Promise<void>;\n\n /** Set track solo state. Only works on owned tracks. */\n setTrackSolo(trackId: string, solo: boolean): Promise<void>;\n\n /** Whether ANY track in the project is currently soloed (across all panels).\n * Lets a panel dim its non-soloed rows (the engine silences them via the\n * effective-mute model). Read-only; not ownership-scoped. */\n isAnySoloActive(): Promise<boolean>;\n\n /** Rename a track. Only works on owned tracks. */\n setTrackName(trackId: string, name: string): Promise<void>;\n\n /**\n * Persist a track's musical role to the `tracks.role` column. Call this\n * after an LLM generation classifies the track (e.g. `'bass'`, `'lead'`,\n * `'pad'`, `'fx'`, `'kicks'`) so downstream features — especially the v1\n * transition generator's layer classifier — can see the role.\n *\n * Canonical values understood by the transition classifier include\n * `bass`, `drums`, `lead`, `chords`, `pad`, `arp`, `fx`, `kicks`,\n * `snares`, `hats`, `clap`, `perc`, `riser`, `impact`. Anything else is\n * stored verbatim but won't match the neutral-role set.\n *\n * Only works on owned tracks.\n */\n setTrackRole(trackId: string, role: string): Promise<void>;\n\n /** Shuffle preset: keep MIDI, apply a random preset from the same category. Only works on owned tracks. */\n /**\n * Shuffle preset: keep MIDI, apply a random preset from the same category.\n * `excludeNames` (since SDK 1.5.0) filters preset names out of the random\n * pool; the current preset is always implicitly excluded. Use this to\n * implement a \"no-repeat until full cycle\" shuffle: the panel accumulates\n * the history and resets when shufflePreset throws \"no presets available\".\n */\n shufflePreset(trackId: string, excludeNames?: readonly string[]): Promise<ShufflePresetResult>;\n\n /** Duplicate track: copy MIDI + role to a new track with a different preset. Only works on owned tracks. */\n duplicateTrack(trackId: string): Promise<PluginTrackHandle>;\n\n /**\n * Persist this plugin's track row order for the active scene. Pass the stable\n * track dbIds ({@link PluginTrackHandle.dbId}) in the desired top-to-bottom\n * order. Reload-safe — {@link getPluginTracks} returns tracks in this order\n * across scene switches and project reopen.\n *\n * Per-panel and decoupled from the engine-synced global track order, so\n * reordering one panel never disturbs other plugins' tracks. Tracks omitted\n * from the list (e.g. newly added or duplicated) keep their natural order at\n * the end. Pairs with the {@link useTrackReorder} hook, which drives the\n * drag-and-drop UI and calls this on drop.\n *\n * @since SDK 2.16.0\n */\n reorderTracks(orderedTrackIds: readonly string[]): Promise<void>;\n\n /**\n * Return the canonical list of valid role tokens that the host's\n * classifier and UI understand. Plugins should use this list when\n * building LLM prompts or validating role values before calling\n * {@link setTrackRole}.\n *\n * The assistant owns the canonical taxonomy — plugins MUST NOT ship\n * their own hardcoded list, which would drift from the host. Pair with\n * {@link setTrackRole} to persist a classified role.\n *\n * @since SDK 2.0.0\n */\n getValidRoles(): readonly string[];\n\n // --- FX Operations (ownership-scoped) ---\n\n /** Get detailed FX state for a track (enabled, preset, dry/wet per category). */\n getTrackFxState(trackId: string): Promise<PluginTrackFxDetailState>;\n\n /** Toggle an FX category on/off for a track. */\n toggleTrackFx(trackId: string, category: string, enabled: boolean): Promise<void>;\n\n /** Set FX preset for a track. Returns the new dry/wet value if applicable. */\n setTrackFxPreset(trackId: string, category: string, presetIndex: number): Promise<{ dryWet?: number }>;\n\n /** Set FX dry/wet level for a track. */\n setTrackFxDryWet(trackId: string, category: string, value: number): Promise<void>;\n\n // --- Real-time Track State ---\n\n /** Subscribe to real-time track state changes (mute, solo, volume, pan). Returns unsubscribe fn. */\n onTrackStateChange(listener: TrackStateChangeListener): UnsubscribeFn;\n\n // --- MIDI Operations ---\n\n /** Write MIDI notes to a track this plugin owns. Replaces existing MIDI. */\n writeMidiClip(trackId: string, clip: MidiClipData): Promise<MidiWriteResult>;\n\n /** Clear all MIDI from a track this plugin owns. */\n clearMidi(trackId: string): Promise<void>;\n\n /**\n * Export all tracks owned by this plugin in the active scene as a ZIP bundle\n * of Standard MIDI Files (one .mid per track, named after each track with\n * collision-avoidance suffixes). Prompts the user for a save location.\n *\n * Tracks with no MIDI data are skipped. Returns the path written, or\n * `{ canceled: true }` if the user dismissed the save dialog.\n *\n * @since SDK 1.1.0\n */\n exportTracksAsMidiBundle(\n options?: ExportMidiBundleOptions\n ): Promise<ExportMidiBundleResult>;\n\n /**\n * Run the host's MIDI post-processing pipeline on raw notes.\n * Wraps MidiProcessor: quantize -> swing -> scale -> register -> overlaps -> humanize.\n */\n postProcessMidi(notes: PluginMidiNote[], options: PostProcessOptions): Promise<PluginMidiNote[]>;\n\n /**\n * Read a track's current MIDI notes for in-place editing (e.g. a piano\n * roll). Returns the track's clips with beat-based notes; an empty `clips`\n * array means the track has no MIDI. Reads LIVE engine state (NOT the DB),\n * so it reflects unsaved generator output too and needs no project_id\n * scoping — do not \"fix\" this into a DB query.\n *\n * Ownership-gated like {@link writeMidiClip}. Optional so a plugin built\n * against this SDK still loads on an older host — callers MUST null-check.\n * @since SDK 2.15.0\n */\n readMidiNotes?(trackId: string): Promise<ReadMidiResult>;\n\n // --- Audio Operations ---\n\n /** Place an audio file on a track this plugin owns. */\n writeAudioClip(trackId: string, filePath: string, position?: number): Promise<void>;\n\n /**\n * Render a single track to a temporary WAV file and return its path.\n * Only works on owned tracks. For MIDI/synth tracks the host mutes siblings\n * and renders the scene. For single-clip audio tracks the host MAY take a\n * copy-source fast path.\n * @since SDK 1.2.0\n */\n exportTrackAudio?(trackId: string): Promise<ExportTrackAudioResult>;\n\n /**\n * Run a chain of audio operations on an input WAV via the bundled\n * sas-audio-processor binary. Unsupported ops throw NOT_IMPLEMENTED.\n * @since SDK 1.2.0\n */\n processAudio?(\n inputPath: string,\n operations: AudioProcessingOp[]\n ): Promise<ProcessAudioResult>;\n\n /**\n * Replace a track's audio content. For audio tracks, clears clips and\n * adds the new audio. For MIDI/synth tracks, the original row is stashed\n * in plugin_data and a new audio_tracks row is inserted (MIDI is lost).\n * @since SDK 1.2.0\n */\n replaceTrackAudio?(trackId: string, audioPath: string): Promise<void>;\n\n // --- Plugin/Synth Operations ---\n\n /** Load a VST3/AU plugin onto a track this plugin owns. */\n loadSynthPlugin(trackId: string, pluginName: string): Promise<number>;\n\n /** Set plugin state (base64-encoded preset data). */\n setPluginState(trackId: string, pluginIndex: number, stateBase64: string): Promise<void>;\n\n /** Get current plugin state (base64-encoded). */\n getPluginState(trackId: string, pluginIndex: number): Promise<string>;\n\n /**\n * Set a plugin's RAW VST3/AU state — the plugin's own getStateInformation\n * format, bypassing Tracktion's ValueTree wrapper. Use for third-party\n * instruments (u-he Diva, Serum, …) whose patches the ValueTree round-trip\n * does not faithfully preserve. Default Surge XT presets use setPluginState.\n * @since SDK 2.15.0\n */\n setRawPluginState(trackId: string, pluginIndex: number, stateBase64: string): Promise<void>;\n\n /** Get a plugin's RAW VST3/AU state (see setRawPluginState). @since SDK 2.15.0 */\n getRawPluginState(trackId: string, pluginIndex: number): Promise<string>;\n\n /** List plugins currently loaded on a track. */\n getTrackPlugins(trackId: string): Promise<PluginSynthInfo[]>;\n\n /** Remove a plugin from a track. */\n removePlugin(trackId: string, pluginIndex: number): Promise<void>;\n\n /** Check if a specific VST/AU plugin is available on the system. */\n isPluginAvailable(pluginName: string): Promise<boolean>;\n\n // --- Instrument Plugin Selection ---\n\n /** Get available instrument plugins (VST3/AU synths) scanned by the engine. */\n getAvailableInstruments(): Promise<InstrumentDescriptor[]>;\n\n /** Get the instrument plugin currently loaded on a track. Null = default (Surge XT). */\n getTrackInstrument(trackId: string): Promise<InstrumentDescriptor | null>;\n\n /** Change the instrument plugin on a track. Preserves MIDI data. */\n setTrackInstrument(trackId: string, pluginId: string): Promise<void>;\n\n /** Open the instrument plugin's native editor GUI as a floating window. */\n showInstrumentEditor(trackId: string): Promise<void>;\n\n /** Close the instrument plugin's editor window. */\n hideInstrumentEditor(trackId: string): Promise<void>;\n\n // --- Drum Sampler ---\n\n /**\n * Load the engine's built-in sampler on the track (if not already\n * present) and configure it with a single one-shot sound. Every MIDI\n * note triggers the loaded sample regardless of pitch — used by the\n * drum-generator plugin where the LLM's emitted pitch is advisory.\n *\n * Idempotent: calling repeatedly on the same track swaps the loaded\n * sample without stacking more sampler instances. The sampler counts\n * as the track's instrument; mixing it with `setTrackInstrument` on\n * the same track is undefined behaviour for now.\n *\n * @since SDK 1.2.0\n */\n setTrackDrumKit(trackId: string, kit: DrumKit): Promise<void>;\n\n /**\n * Load the engine's built-in sampler on the track (if not already\n * present) and configure it with a pitched, polyphonic, multi-zone\n * instrument. Each MIDI note triggers the zone whose [minKey,maxKey]\n * range contains it; the zone is played back pitch-shifted relative\n * to its rootKey. Polyphony is handled by the Tracktion sampler's\n * voice allocator.\n *\n * Used by the instrument-generator plugin to load a pre-rendered\n * pitched-sample manifest. Mutually exclusive with `setTrackDrumKit`\n * on the same track (both occupy the sampler slot) and with\n * `setTrackInstrument(pluginId)` (which loads a VST synth instead).\n *\n * Idempotent: calling repeatedly on the same track swaps the loaded\n * zones without stacking sampler instances.\n *\n * @since SDK 1.3.0\n */\n setTrackInstrumentSampler(trackId: string, instrument: InstrumentSampler): Promise<void>;\n\n // --- Filesystem (sample library scanning) ---\n\n /**\n * List audio files (by default `.wav`) under `rootPath`. Returns\n * absolute file paths. `recursive` defaults to false; pass `true` to\n * walk subdirectories. The drum-generator plugin uses this to\n * lazily discover available samples without round-tripping each\n * folder through `getSamples`.\n *\n * Plugins MUST NOT use this to read paths outside their declared\n * sample roots — the host may add path validation in a later release.\n *\n * @since SDK 1.2.0\n */\n listAudioFiles(rootPath: string, options?: ListAudioFilesOptions): Promise<string[]>;\n\n /**\n * Read a text file's contents from the host filesystem (UTF-8). Returns\n * `null` on any read error (missing file, permission, etc.) — the\n * caller does not need to wrap the call in try/catch.\n *\n * Intended for plugin sample-library metadata: instrument manifest\n * JSON (`<instrument-id>/manifest.json`) and prompt-sibling text\n * (`<id>.txt`). Plugins parse the returned string themselves so the\n * host stays content-agnostic.\n *\n * Plugins MUST NOT use this to read paths outside their declared\n * sample roots — the host may add path validation in a later release.\n *\n * @since SDK 1.4.0\n */\n readTextFile(absolutePath: string): Promise<string | null>;\n\n // --- Scene Context (read-only) ---\n\n /** Get the FULL generation context for the active scene. */\n getGenerationContext(excludeTrackId?: string): Promise<PluginGenerationContext>;\n\n /** Get lightweight musical context (no concurrent track MIDI data). */\n getMusicalContext(): Promise<MusicalContext>;\n\n /** Get the active scene ID. Null if no scene is active. */\n getActiveSceneId(): string | null;\n\n /**\n * Get the bound project's DB id. Null when no project is bound.\n * Optional — older hosts and the renderer-side host proxy may omit it;\n * callers MUST feature-check. Used e.g. to detect project switches for\n * per-project conversation persistence.\n * @since SDK 2.18.0\n */\n getProjectId?(): string | null;\n\n /** Get list of all scenes in the project. */\n getSceneList(): Promise<PluginSceneInfo[]>;\n\n /**\n * Enumerate importable track candidates from OTHER scenes, scoped to this\n * plugin's track type (derived from the plugin id). Each candidate is\n * annotated with `importable` + `disabledReason` — the host computes the\n * harmonic/length/tempo gate so the UI only renders it. By default the active\n * scene is excluded; pass `includeSameScene` to also surface the active\n * scene's MIDI tracks owned by OTHER panels (the cross-panel re-sound source).\n * Scenes with no candidate of this type are omitted.\n *\n * Optional so a plugin built against this SDK still loads on an older host —\n * callers MUST null-check and hide the affordance when absent.\n * @since SDK 2.13.0\n */\n listImportableTracks?(opts?: ListImportableTracksOptions): Promise<ImportCandidateScene[]>;\n\n /**\n * Import a source track (from another scene) into the active scene as a\n * faithful, independent copy, delegating to the `import_track_from_scene`\n * tool. Returns the new track's handle so the panel can append a row.\n * Throws on a gate violation — call only for candidates with `importable`.\n * Optional — callers MUST null-check (see `listImportableTracks`).\n * @since SDK 2.13.0\n */\n importTrack?(opts: { sourceSceneId: string; sourceTrackId: string }): Promise<PluginTrackHandle>;\n\n /**\n * Read a source track's CURRENT sound — sample path (drums), sampler zones\n * (instruments), or Surge preset state (synths) — so a panel can copy just\n * the sound onto another track, IGNORING the contract gate that `importTrack`\n * enforces (\"different contract, same preset\"). Read-only: applies nothing.\n * The selector is the source track's DB row id (`ImportCandidateTrack.dbId`).\n * Returns null when the track has no stored sound. Optional — callers MUST\n * null-check (see `listImportableTracks`).\n * @since SDK 2.14.0\n */\n getTrackSound?(sourceTrackDbId: string): Promise<TrackSoundSnapshot | null>;\n\n /**\n * Read a source track's persisted MIDI by its DB row id — the cross-panel\n * READ half of \"re-sound a part on a different instrument\". Unlike\n * `readMidiNotes` (engine-read, ownership-gated), this reads the DB and is\n * NOT ownership-gated, so a panel can pull a part out of a track owned by a\n * DIFFERENT panel in the same scene (the selector is\n * `ImportCandidateTrack.dbId`, e.g. a `sameScene` candidate). Notes are\n * beat-based, identical shape to `readMidiNotes`; the loop span comes from the\n * source scene. Returns `{ clips: [] }` when the track has no MIDI. Optional —\n * callers MUST null-check (see `listImportableTracks`).\n * @since SDK 2.20.0\n */\n readImportableTrackMidi?(sourceTrackDbId: string): Promise<ReadMidiResult>;\n\n /**\n * List THIS panel's family tracks in a specific scene (by DB id), WITHOUT the\n * import key/length/tempo gate that `listImportableTracks` applies. Powers the\n * crossfade picker: the origin (from) and target (to) scenes of a transition\n * deliberately differ in key, so gating would wrongly hide valid candidates.\n * Project-scoped, read-only. Returns [] for an unknown/empty scene. Optional —\n * callers MUST null-check (see `listImportableTracks`).\n * @since SDK 2.22.0\n */\n listSceneFamilyTracks?(sceneDbId: string): Promise<SceneFamilyTrack[]>;\n\n /**\n * Read a specific scene's musical key (tonic + mode) by db id. Labels the\n * SOURCE keys of a crossfade's origin/target patterns — the active-scene\n * musical context only carries the transition scene's key. Optional — callers\n * MUST null-check. @since SDK 2.24.0\n */\n getSceneKey?(sceneDbId: string): Promise<{ key: string; mode: string } | null>;\n\n // --- Transport & Playback Events ---\n\n /** Subscribe to transport state changes. Returns unsubscribe function. */\n onTransportEvent(listener: TransportEventListener): UnsubscribeFn;\n\n /** Subscribe to deck boundary events. Returns unsubscribe function. */\n onDeckBoundary(listener: DeckBoundaryListener): UnsubscribeFn;\n\n /** Subscribe to scene change events. Returns unsubscribe function. */\n onSceneChange(listener: SceneChangeListener): UnsubscribeFn;\n\n /** Get current transport state (one-shot). */\n getTransportState(): Promise<PluginTransportState>;\n\n /**\n * One-shot mono peak level for every track this plugin owns. Drives the\n * cosmetic per-track strip meters; poll at ~30Hz while the transport is\n * playing. The host scopes the result to this plugin's tracks and coalesces\n * the underlying engine read, so a busy engine yields a STALE meter rather\n * than a backlog (playback always wins over the GUI). Optional: guard with\n * `typeof host.getTrackLevels === 'function'` for older hosts.\n * @since SDK 2.21.0\n */\n getTrackLevels?(): Promise<PluginTrackLevel[]>;\n\n // --- LLM Access (metered, authenticated) ---\n\n /** Generate text/JSON via the host's authenticated LLM service. */\n generateWithLLM(request: LLMGenerationRequest): Promise<LLMGenerationResult>;\n\n /**\n * Generate with native tool-use (function calling). Used by agentic plugins\n * (chat panel, etc.) to drive an iterative loop where the model calls tools,\n * observes results, and decides next steps — same loop class as Claude Code\n * or VS Code agent mode.\n *\n * Shape mirrors Gemini's `generateContent` REST surface; the host forwards\n * verbatim to the gateway's Gemini-native passthrough endpoint, which adds\n * the central Google API key. Plugins never see provider credentials.\n *\n * Available since SDK 2.4.0.\n */\n generateWithLLMTools(request: LLMToolUseRequest): Promise<LLMToolUseResponse>;\n\n /**\n * Resolve absolute paths for spawning the bundled `sas` CLI as a subprocess.\n * Used by agentic plugins that drive the CLI as their tool surface (chat\n * panel, etc.). Returns `null` when called from a renderer-side host or\n * when the CLI isn't accessible.\n *\n * Available since SDK 2.4.0.\n */\n getCliPaths(): { appExe: string; cliEntry: string } | null;\n\n /**\n * Resolve the absolute path to a bundled resource directory shipped with\n * the app via `extraResources` (e.g. `'drum-samples'`,\n * `'tracktion-presets'`). In dev, resolves to\n * `<projectRoot>/resources/<name>`. In packaged builds, resolves to\n * `<process.resourcesPath>/<name>`.\n *\n * Returns `null` if the host cannot resolve paths in this context\n * (e.g. Electron mocked out in unit tests). Plugins MUST null-check and\n * either degrade gracefully or fall back to a known dev path.\n *\n * Async by design: the renderer-side host proxy round-trips through IPC.\n *\n * @since SDK 2.7.0\n */\n getBundledResourcePath(name: string): Promise<string | null>;\n\n /** Check if LLM access is available (user authenticated + gateway reachable). */\n isLLMAvailable(): Promise<boolean>;\n\n // --- App Tool Bridge ---\n\n /**\n * List the host's registered app tools. Used by plugins (e.g. the chat\n * panel) that want to expose the same surface external AI agents have.\n *\n * `opts.scope` filters by scope tag — scene-scoped consumers pass\n * `'scene'` to hide project-level tools they shouldn't call. When omitted,\n * every tool regardless of scope is returned.\n *\n * `opts.includeDeferred` (since SDK 2.18.0) opts in to tools flagged with\n * `deferLoading` (progressive disclosure). Default `false` mirrors\n * `/api/v1/actions` — the curated core surface. Used by curation layers\n * that promote specific deferred/project tools onto an agent's default\n * declaration set.\n *\n * @since SDK 1.2.0\n */\n listAppTools(opts?: {\n scope?: 'scene' | 'project';\n includeDeferred?: boolean;\n }): Promise<PluginAppTool[]>;\n\n /**\n * Execute a host app tool by name. Delegates to the in-process\n * ToolRegistry — every call (including this one) broadcasts to the\n * UI's `mutations:tool-executed` channel so renderer state stays\n * fresh whether the call mutates or is read-only. Read-only callers\n * pay zero extra cost since the renderer debounces and skips\n * redundant reloads.\n *\n * For scene-scoped tools tagged with `autoBindSceneId`, the host\n * overrides the caller's `sceneId` param with the currently-active\n * scene. That keeps a scene-bound caller from accidentally targeting\n * another scene.\n *\n * `opts.provenance` (since SDK 2.18.0) stamps the originating actor onto\n * every domain event this call emits — pass `'agent'` from autonomous\n * agent loops so the UI orchestrator can gate auto-navigation, `'user'`\n * when proxying a direct user gesture. Omitted = `'system'`.\n *\n * @since SDK 1.2.0\n */\n executeAppTool(\n name: string,\n params: Record<string, unknown>,\n opts?: { provenance?: 'agent' | 'user' }\n ): Promise<PluginAppToolResult>;\n\n /**\n * Monotonic counter that increments on every state mutation\n * (`broadcastMutation('tool-executed', ...)`). Use as a cache key for\n * derived state that depends on the project: when the counter changes,\n * something mutated; when it doesn't, your cache is still valid.\n *\n * Mostly aimed at performance-sensitive callers like ambient-context\n * builders that want to skip re-querying state when nothing has\n * changed. The counter is process-local — it resets on app restart\n * and is not durable across sessions.\n *\n * Implementation detail: the counter is bumped by `mutation-broadcaster`\n * before the broadcaster fires, so a synchronous `getMutationSeq()`\n * call from inside a mutation listener will see the post-bump value.\n *\n * @since SDK 2.6.0\n */\n getMutationSeq(): number;\n\n // --- Preset System ---\n\n /** Get available preset categories for a synth plugin. */\n getPresetCategories(pluginName: string): Promise<string[]>;\n\n /** Get a random preset from a category. */\n getRandomPreset(category: string): Promise<PluginPresetData | null>;\n\n /** Get a specific preset by name from a category. */\n getPresetByName(category: string, name: string): Promise<PluginPresetData | null>;\n\n /** Use LLM to classify a text description into a preset category. */\n classifyPresetCategory(description: string): Promise<string>;\n\n // --- Storage & Settings ---\n\n /** Get absolute path to this plugin's isolated data directory. */\n getDataDirectory(): string;\n\n /** Persisted key-value settings store. */\n settings: PluginSettingsStore;\n\n // --- Sample Pack Distribution ---\n\n /**\n * Return the absolute path to an installed sample pack's root directory,\n * or `null` if the pack is missing OR its installed version doesn't match\n * what the current app build expects.\n *\n * Plugins should treat `null` as \"show the download CTA\"; do NOT fall back\n * to a hardcoded path. The host owns where samples live (currently\n * `<userData>/samples/<installSubdir>/`).\n *\n * Stable packIds: `'sas-drum-pack'`, `'sas-instrument-pack'`. Both packs\n * are downloaded on demand via the host's pack-download flow; see\n * `host.isSamplePackCurrent` and the renderer-side `DownloadPackButton`.\n *\n * @since SDK 2.7.0\n */\n getSamplePackRoot(packId: string): Promise<string | null>;\n\n /**\n * True if the installed version of `packId` matches the version this app\n * build expects. False if the pack is missing OR the installed version\n * differs (older or newer).\n *\n * Plugins call this on activate to decide between rendering their normal\n * UI vs the \"Sample library not installed / Update available\" CTA.\n *\n * @since SDK 2.7.0\n */\n isSamplePackCurrent(packId: string): Promise<boolean>;\n\n /**\n * Return the currently-installed version string for `packId` (e.g. `'1'`,\n * `'2'`), or `null` if the pack is not installed at all. Reads the\n * `_pack-version.json` marker inside the pack's install dir.\n *\n * Useful for distinguishing the \"missing\" CTA from the \"stale, update\n * available\" CTA — plugins can call this when `isSamplePackCurrent`\n * returns false to pick the right empty-state message.\n *\n * @since SDK 2.7.0\n */\n getSamplePackInstalledVersion(packId: string): Promise<string | null>;\n\n /**\n * Trigger a download + install of `packId` via the host's pack system (the\n * same flow `getSamplePackRoot` / `isSamplePackCurrent` report on). Resolves\n * when the install completes or fails. Plugins call this from a \"download\n * library\" CTA instead of reaching into the app's IPC (`window.electronAPI`)\n * directly.\n *\n * @since SDK 2.8.0\n */\n startSamplePackDownload(\n packId: string\n ): Promise<{ success: boolean; error?: string }>;\n\n /**\n * Subscribe to download/install progress for `packId`. Returns an unsubscribe\n * fn. `status` mirrors the host's pack-download states (e.g. `'downloading' |\n * 'extracting' | 'installing' | 'complete' | 'error'`); `progress` is 0-100.\n *\n * @since SDK 2.8.0\n */\n onSamplePackProgress(\n packId: string,\n listener: (progress: {\n packId?: string;\n status: string;\n progress: number;\n message?: string;\n }) => void\n ): UnsubscribeFn;\n\n /**\n * Return the canonical display metadata (`displayName`, `description`,\n * `sizeBytes`) for `packId` from the host's pack registry — the SAME source\n * the host uses to download + version-check the pack. A plugin's download CTA\n * should prefer this over a hardcoded copy so the size/description stay in\n * sync with whatever bundle the host actually ships (no per-version drift).\n * Resolves `null` for an unknown packId.\n *\n * Optional so a plugin built against this SDK still runs on an older host:\n * callers should fall back to their own static copy when it is absent or\n * returns `null`.\n *\n * @since SDK 2.12.0\n */\n getSamplePackInfo?(packId: string): Promise<SamplePackPublicInfo | null>;\n\n /**\n * Per-pack roots of the USER's imported sample packs for `kind`. Each root\n * is laid out exactly like the corresponding stock pack (drums:\n * `<root>/<role>/<file>.wav` + `.txt` sidecars; instruments:\n * `<root>/<category>/<id>/manifest.json`), so resolvers scan them as\n * additional roots alongside `getSamplePackRoot`. `[]` when nothing is\n * imported. User content lives under `<userData>/user-samples/` — strictly\n * separate on disk; stock pack installs never touch it.\n *\n * Optional for older-host compat: feature-check\n * (`host.getUserSampleRoots?.(...)`) and treat absence as `[]`.\n *\n * @since SDK 2.20.0\n */\n getUserSampleRoots?(kind: 'drums' | 'instruments'): Promise<string[]>;\n\n /**\n * Ask the host app to open its sample-import wizard targeting `kind`.\n * Fire-and-forget; renderer-hosted plugins only (the wizard is an app-level\n * modal — the main-process host no-ops). Library changes land as\n * `onSamplePackProgress` events with packId `user:<kind>` and\n * `status: 'complete'`, so subscribe to that to refresh.\n *\n * @since SDK 2.20.0\n */\n openSampleImportWizard?(kind: 'drums' | 'instruments'): void;\n\n // --- Deck playback ---\n //\n // The two playback decks: `'loop-a'` (composition / cue, headphones) and\n // `'loop-b'` (performance / main). These route through the SAME host path\n // the workstation UI uses, so the deck mutual-exclusivity rules\n // (PlaybackRuleEngine) are enforced identically — a plugin cannot bypass\n // them. Used by playback-driven plugins (e.g. the recorder, which starts\n // loop-a so a take has a backing loop). Available on renderer-hosted plugins.\n\n /**\n * Start a deck playing the given scene/transition. Mirrors the workstation's\n * transport play. `contentType` defaults to `'scene'`.\n *\n * @since SDK 2.9.0\n */\n deckPlay(\n deckId: string,\n contentId?: string,\n contentType?: 'scene' | 'transition'\n ): Promise<{ success: boolean; error?: string; code?: string }>;\n\n /**\n * Stop a deck.\n *\n * @since SDK 2.9.0\n */\n deckStop(deckId: string): Promise<{ success: boolean; error?: string }>;\n\n /**\n * Subscribe to per-deck state changes. Each event carries the `deckId`, the\n * `property` that changed (e.g. `'playing'`), and its new `value`. Returns an\n * unsubscribe fn.\n *\n * @since SDK 2.9.0\n */\n onDeckStateChanged(\n listener: (event: { deckId: string; property: string; value: unknown }) => void\n ): UnsubscribeFn;\n\n /**\n * Subscribe to the \"all decks stopped\" engine event (e.g. global transport\n * stop). Returns an unsubscribe fn.\n *\n * @since SDK 2.9.0\n */\n onAllDecksStopped(listener: () => void): UnsubscribeFn;\n\n // --- Scoped Data API ---\n\n /** Get a value from scene-scoped plugin data. */\n getSceneData<T = unknown>(sceneId: string, key: string): Promise<T | null>;\n\n /** Set a value in scene-scoped plugin data. */\n setSceneData(sceneId: string, key: string, value: unknown): Promise<void>;\n\n /** Get all key-value pairs for a scene. */\n getAllSceneData(sceneId: string): Promise<Record<string, unknown>>;\n\n /** Delete a key from scene-scoped plugin data. */\n deleteSceneData(sceneId: string, key: string): Promise<void>;\n\n /** Get the full project-scoped state object. */\n getProjectData<T = unknown>(key: string): Promise<T | null>;\n\n /** Set a project-scoped data value. */\n setProjectData(key: string, value: unknown): Promise<void>;\n\n // --- Notifications & Progress ---\n\n /** Show a toast notification to the user. */\n showToast(type: 'info' | 'success' | 'warning' | 'error', title: string, message?: string): void;\n\n /** Set progress indicator on a specific track. -1 to hide. */\n setProgress(trackId: string, progress: number): void;\n\n /** Set a global status message in the plugin's accordion header. */\n setStatusMessage(message: string | null): void;\n\n /** Request user confirmation via a modal dialog. */\n confirmAction(title: string, message: string): Promise<boolean>;\n\n // --- File System (Phase 2) ---\n\n /** Show a native file open dialog. Requires 'fileDialog' capability. */\n showOpenDialog(options: PluginFileDialogOptions): Promise<string[] | null>;\n\n /** Show a native file save dialog. Requires 'fileDialog' capability. */\n showSaveDialog(options: PluginFileDialogOptions): Promise<string | null>;\n\n /** Download a file to the plugin's data directory. */\n downloadFile(url: string, filename: string, options?: PluginDownloadOptions): Promise<string>;\n\n /** Copy a file into the plugin's data directory. */\n importFile(sourcePath: string, destFilename: string): Promise<string>;\n\n // --- Network (Phase 2, capability-gated) ---\n\n /** Make an HTTP request. Requires 'network' capability with allowedHosts. */\n httpRequest(options: PluginHttpRequestOptions): Promise<PluginHttpResponse>;\n\n // --- Secure Storage (Phase 2) ---\n\n /** Store a secret in the OS keychain (plugin-scoped). */\n storeSecret(key: string, value: string): Promise<void>;\n\n /** Retrieve a secret from the OS keychain (plugin-scoped). */\n getSecret(key: string): Promise<string | null>;\n\n /** Delete a secret from the OS keychain (plugin-scoped). */\n deleteSecret(key: string): Promise<void>;\n\n // --- Sample Library (Phase 2) ---\n\n /** Query the sample library with optional filters. */\n getSamples(filter?: PluginSampleFilter): Promise<PluginSampleInfo[]>;\n\n /** Get a single sample by ID. */\n getSampleById(id: string): Promise<PluginSampleInfo | null>;\n\n /** Import audio files into the sample library. */\n importSamples(filePaths: string[]): Promise<PluginSampleImportResult>;\n\n /** Create a sample track in the active scene. */\n createSampleTrack(sampleId: string, options?: { name?: string }): Promise<PluginTrackHandle>;\n\n /** Delete a sample track. */\n deleteSampleTrack(trackId: string): Promise<void>;\n\n /** Get all sample tracks in the active scene. Re-establishes ownership. */\n getPluginSampleTracks(): Promise<PluginSampleTrackInfo[]>;\n\n /** Time-stretch a sample to a target BPM. Returns the new sample info. */\n timeStretchSample(sampleId: string, targetBpm: number): Promise<PluginSampleInfo>;\n\n /**\n * Fit a sample to the active scene's `(bpm, length_bars)`. Composes:\n * 1. Time-stretch to scene BPM (no-op if already matching).\n * 2. Chop / loop-stitch / passthrough so the resulting clip's duration\n * equals exactly `length_bars × 4 × (60 / bpm)` seconds.\n *\n * Required because the deck loops the clip at the scene's bar boundary —\n * a 4-bar sample dropped into a 2-bar scene used to over-run; a 4-bar\n * sample dropped into an 8-bar scene used to leave 4 bars of silence.\n *\n * The fitted sample is cached in the library by content hash, so\n * subsequent calls for the same `(sample, bpm, bars)` return instantly.\n */\n fitSampleToScene(sampleId: string): Promise<PluginSampleInfo>;\n\n /**\n * Lightweight one-shot sample audition through the cue (headphone) output.\n *\n * Plays the file via a dedicated SimpleLoopPlayer instance in the audio\n * engine — no Tracktion track or clip is created, no BPM matching, no\n * sync. Calling previewSample again with a different file replaces the\n * current preview cleanly. Independent of loop-b: starting/stopping a\n * preview never affects the performance deck and vice versa.\n */\n previewSample(filePath: string): Promise<void>;\n\n /**\n * Stop any in-flight sample preview started by previewSample(). Safe to\n * call when no preview is active — never throws.\n */\n stopPreview(): Promise<void>;\n\n // --- Audio Generation (Phase 2) ---\n\n /** Invoke the host's audio texture generation pipeline. */\n generateAudioTexture(request: PluginAudioTextureRequest): Promise<PluginAudioTextureResult>;\n\n // --- Audio Cue Points + Offset (Migration 060) ---\n\n /**\n * Persist cue points (detected beat positions) for an audio track.\n * Called once after `writeAudioClip` to remember the trim metadata so the\n * UI can later draw beat ticks and snap-to-beat the manual offset.\n *\n * Pass `null` to clear cue points. Throws OWNERSHIP_VIOLATION if the\n * track wasn't created by this plugin.\n */\n setCuePoints(trackId: string, cues: PluginCuePoints | null): Promise<void>;\n\n /** Read cue points previously written by `setCuePoints`. Returns null when none stored. */\n getCuePoints(trackId: string): Promise<PluginCuePoints | null>;\n\n /**\n * Set the manual sample-offset applied to the track's audio clip during\n * playback. Positive shifts later, negative shifts earlier with head\n * silence. Throws OWNERSHIP_VIOLATION if not owned by this plugin.\n */\n setAudioOffsetSamples(trackId: string, offsetSamples: number): Promise<void>;\n\n /** Read the current manual offset (0 if never set). */\n getAudioOffsetSamples(trackId: string): Promise<number>;\n\n // --- Raw / pre-trim audio metadata (stems trim editor) ---\n\n /**\n * Read raw bytes of an audio file written by the host. The path may be\n * `~app/`-relative or project-relative — the host resolves it using the\n * same logic as `writeAudioClip`. Throws FILE_NOT_FOUND if the path\n * can't be resolved or doesn't exist on disk.\n */\n getAudioFileBytes(filePath: string): Promise<ArrayBuffer>;\n\n /** Persist the original (raw, un-trimmed) audio file path for a track. */\n setRawAudioFilePath(trackId: string, filePath: string | null): Promise<void>;\n\n /** Read the raw audio file path persisted via `setRawAudioFilePath`. */\n getRawAudioFilePath(trackId: string): Promise<string | null>;\n\n /**\n * Persist the cue-points detected in the raw (un-trimmed) audio file.\n * Sample positions are in input-file coordinates.\n */\n setRawCuePoints(trackId: string, cues: PluginCuePoints | null): Promise<void>;\n\n /** Read raw-domain cue points persisted via `setRawCuePoints`. */\n getRawCuePoints(trackId: string): Promise<PluginCuePoints | null>;\n\n /** Persist the current trim window inside the raw audio file. */\n setTrimWindow(trackId: string, window: PluginTrimWindow | null): Promise<void>;\n\n /** Read the current trim window persisted via `setTrimWindow`. */\n getTrimWindow(trackId: string): Promise<PluginTrimWindow | null>;\n\n /**\n * Re-trim the raw audio file at the given sample offset and replace the\n * track's audio clip with the new slice. Persists updated trimmed-domain\n * cue points and the new trim window.\n */\n commitTrimWindow(\n trackId: string,\n startSample: number,\n durationSamples: number,\n ): Promise<{ filePath: string; cuePoints: PluginCuePoints | null }>;\n\n // --- Scene Composition ---\n\n /** Trigger bulk composition for the active scene (LLM plans arrangement, creates tracks, generates MIDI). */\n composeScene(options: ComposeSceneOptions): Promise<ComposeSceneResult>;\n\n /** Subscribe to composition progress events (planning, generating, complete, error). */\n onComposeProgress(listener: ComposeProgressListener): UnsubscribeFn;\n\n /** Subscribe to engine ready events (fires when the engine finishes loading tracks after a scene change). */\n onEngineReady(listener: () => void): UnsubscribeFn;\n\n /**\n * Subscribe to external state mutations (CLI, MCP, or HTTP-API tool calls\n * that bypass plugin-host methods). Fires after such a tool finishes,\n * signalling that scene/track DB state may have changed underneath the\n * plugin's local cache. Use it to refresh state that the plugin doesn't\n * own — e.g. re-running adoptSceneTracks() so AI-created tracks become\n * visible without requiring the user to switch scenes.\n *\n * Optional: only the renderer-side host implements this. Main-side\n * plugins should subscribe to the typed domain-event bus instead.\n */\n onAfterAgentMutation?(listener: () => void): UnsubscribeFn;\n\n // --- MIDI Extensions (Phase 2) ---\n\n /** Audition a single note on a track (fire-and-forget preview). */\n auditionNote(trackId: string, pitch: number, velocity: number, durationMs: number): Promise<void>;\n\n // --- Plugin Presets (Phase 2) ---\n\n /** Get presets for this plugin, optionally filtered by category. */\n getPluginPresets(category?: string): Promise<PluginPresetInfo[]>;\n\n /** Save a new preset for this plugin. */\n savePluginPreset(options: SavePluginPresetOptions): Promise<PluginPresetInfo>;\n\n /** Delete a plugin preset by ID. */\n deletePluginPreset(id: string): Promise<void>;\n\n // --- Performance / Logging (Phase 2) ---\n\n /** Log a performance metric. */\n logMetric(name: string, durationMs: number, metadata?: Record<string, unknown>): void;\n\n /** Start a timer. Returns a stop function that logs the duration. */\n startTimer(name: string): () => void;\n\n // --- Stem Splitting ---\n\n /** Split an audio track into stems (vocals, drums, bass, other). Creates new muted tracks. */\n splitStems(trackId: string): Promise<PluginStemSplitResult>;\n\n /** Check if the stem splitter binary is available. */\n isStemSplitterAvailable(): Promise<boolean>;\n\n // --- Audio Recording (capability-gated, since SDK 2.1.0) ---\n\n /**\n * Enumerate audio input devices visible to the engine. Empty list means\n * no input device is available (or the OS denied permission). Requires\n * `audioCapture` capability.\n * @since SDK 2.1.0\n */\n getAudioInputDevices(): Promise<AudioInputDevice[]>;\n\n /**\n * Snapshot of engine state needed to start a recording session. Reads\n * the engine sample rate, the active scene id, the transition-render\n * lock state, and current BPM/bars. Requires `audioCapture`.\n * @since SDK 2.1.0\n */\n getRecordingTargetInfo(): Promise<RecordingTargetInfo>;\n\n /**\n * Begin a recording session. Engine writes integer-PCM WAV chunks to\n * disk; one chunk per call to `markRecordingChunkBoundary`. Each\n * finalized chunk fires a `RecordingChunkFinalizedEvent` to\n * subscribers of `onRecordingChunkFinalized`. Throws\n * AUDIO_CAPTURE_DENIED on permission failure or if no device is\n * available.\n *\n * Pass `deviceId` to override the platform-configured input (rare —\n * only useful for tests or workflows that need a specific device).\n * Omit it to use the platform's selected input from\n * `AudioRoutingConfig.inputDeviceId` — this is the recommended path\n * for plugins post-SDK-2.2.0.\n *\n * @since SDK 2.1.0 (deviceId required) — 2.2.0 made it optional.\n */\n startTrackRecording(deviceId?: string): Promise<void>;\n\n /**\n * Mark the boundary between two recording chunks. The engine closes the\n * currently-open WAV writer and opens a new one; the closed file fires\n * a `RecordingChunkFinalizedEvent` once flush completes. No-op if no\n * recording session is active.\n *\n * Pass `boundaryHostTimeNs` from `DeckBoundaryEvent.boundaryHostTimeNs`\n * for sample-perfect take alignment (Path 2). The engine then splits\n * the chunk at the EXACT recorder-sample that corresponds to that\n * host-time, eliminating the ~5–50 ms of jitter introduced by the\n * legacy \"split wherever the writer is\" path. Required for any\n * workflow that overlays multiple takes (vocalist comping, layered\n * dubs); optional for single-take captures.\n *\n * @since SDK 2.1.0 — 2.4.0 added optional boundaryHostTimeNs.\n */\n markRecordingChunkBoundary(boundaryHostTimeNs?: number): Promise<void>;\n\n /**\n * Stop the active recording session. The final chunk is closed and\n * finalized; its `RecordingChunkFinalizedEvent` fires before this\n * promise resolves. Returns the path of the final chunk (also delivered\n * via the event for symmetry).\n * @since SDK 2.1.0\n */\n stopTrackRecording(): Promise<{ finalChunkPath: string; durationMs: number }>;\n\n /**\n * Subscribe to chunk-finalized events for this plugin's active recording\n * session. Auto-unsubscribed on `deactivate`. Returns unsubscribe fn.\n * @since SDK 2.1.0\n */\n onRecordingChunkFinalized(\n listener: (event: RecordingChunkFinalizedEvent) => void\n ): UnsubscribeFn;\n\n /**\n * Get the platform-configured audio input device, or null when no\n * device is set. Read-only; configured via the assistant's\n * AudioRoutingPanel. Plugins use this to display the current input\n * to the user without exposing their own picker.\n *\n * @since SDK 2.2.0\n */\n getCurrentInputDevice(): Promise<AudioInputDevice | null>;\n\n /**\n * Subscribe to input-device changes (user picks a new mic in the\n * Audio Routing panel). Listeners should refetch via\n * `getCurrentInputDevice()`. Returns an unsubscribe fn.\n *\n * @since SDK 2.4.0\n */\n onInputDeviceChange(listener: () => void): UnsubscribeFn;\n\n /**\n * Get the platform's mic-to-output round-trip latency offset in\n * samples. 0 = uncalibrated. Plugins recording audio apply this via\n * `setAudioOffsetSamples` so takes line up with the source loop.\n *\n * @since SDK 2.2.0\n */\n getRecordingLatencyOffsetSamples(): Promise<number>;\n\n /**\n * Snapshot of the input level for the most recent audio block.\n * Renderer polls at ~30Hz to drive a level meter / scrolling\n * waveform without an event-channel subscription.\n *\n * @since SDK 2.3.0\n */\n getRecordingInputLevel(): Promise<{\n peakDb: number;\n peakLinear: number;\n clipped: boolean;\n active: boolean;\n }>;\n\n /**\n * Reset the latched clip indicator. Safe regardless of whether\n * monitoring or recording is active.\n *\n * @since SDK 2.3.0\n */\n clearRecordingInputClipIndicator(): Promise<void>;\n}\n\n// ============================================================================\n// Stem Splitting Types\n// ============================================================================\n\n/** Stem type identifiers */\nexport type StemType = 'vocals' | 'drums' | 'bass' | 'other';\n\n/** Result of splitting an audio track into stems */\nexport interface PluginStemSplitResult {\n /** Created stem tracks with audio loaded (all auto-muted) */\n stems: PluginStemTrackInfo[];\n}\n\n/** Information about a single stem track created by stem splitting */\nexport interface PluginStemTrackInfo {\n /** The stem type (vocals, drums, bass, other) */\n stemType: StemType;\n /** Track handle for the new stem track */\n track: PluginTrackHandle;\n}\n\n// ============================================================================\n// Exported Plugin Data Types (for .sasproj portability)\n// ============================================================================\n\nexport interface ExportedPluginData {\n pluginId: string;\n scope: 'project' | 'scene' | 'global';\n scopeId: string | null;\n key: string;\n value: string; // JSON-serialized\n}\n\n// ============================================================================\n// Track Types\n// ============================================================================\n\nexport interface CreateTrackOptions {\n /** Display name for the track. Auto-generated if omitted. */\n name?: string;\n /** Musical role hint: 'bass', 'drums', 'lead', 'chords', 'pad', 'arp', 'fx' */\n role?: string;\n /** Load a synth plugin immediately (default: false) */\n loadSynth?: boolean;\n /** Which synth to load (default: 'Surge XT'). Ignored if loadSynth=false. */\n synthName?: string;\n /**\n * Stable plugin identifier for a custom instrument (VST3 TUID or AU component ID).\n * If provided with loadSynth=true, loads this plugin instead of synthName.\n * Null/undefined = use default (Surge XT).\n */\n instrumentPluginId?: string | null;\n /** Metadata stored in DB. Plugins can use this for plugin-specific data. */\n metadata?: Record<string, unknown>;\n}\n\nexport interface PluginTrackHandle {\n /** Tracktion engine track ID (stable, GUID-based) */\n id: string;\n /** Display name */\n name: string;\n /** Database row ID */\n dbId: string;\n /** Musical role (if set) */\n role?: string;\n /** Prompt from tracks table (fallback when plugin_data not yet populated) */\n prompt?: string;\n /** Custom instrument plugin ID (null = default Surge XT) */\n instrumentPluginId?: string | null;\n /** Custom instrument display name (null = Surge XT) */\n instrumentName?: string | null;\n}\n\n/**\n * One source track offered by `listImportableTracks`, already filtered to the\n * calling panel's type. The host computes the gate; the UI only renders it.\n * @since SDK 2.13.0\n */\nexport interface ImportCandidateTrack {\n /** Source track's engine track id (the selector passed back to importTrack). */\n trackId: string;\n /** Source track's DB row id (globally unique; good React key). */\n dbId: string;\n /** Display name shown in the modal row. */\n name: string;\n /** Musical role if set (drives the row icon). */\n role?: string;\n /** True when this track can be copied into the active scene as-is. */\n importable: boolean;\n /** Why the track is disabled (shown as a tooltip). Present iff `!importable`. */\n disabledReason?: string;\n}\n\n/**\n * One track in a specific scene, returned by `host.listSceneFamilyTracks`,\n * already narrowed to the calling panel's family. Unlike `ImportCandidateTrack`\n * it carries NO import gate — the crossfade picker lists every same-family track\n * in the origin/target scene regardless of key/length. @since SDK 2.22.0\n */\nexport interface SceneFamilyTrack {\n /** Track's DB row id — the selector for getTrackSound + crossfade metadata. */\n dbId: string;\n /** Display name shown in the picker. */\n name: string;\n /** Musical role if set — used to enforce same-role crossfade pairing. */\n role?: string;\n}\n\n/**\n * One OTHER scene and its candidate tracks (already type-filtered). Scenes with\n * zero candidates of the panel's type are omitted by the host.\n * @since SDK 2.13.0\n */\nexport interface ImportCandidateScene {\n /** Source scene's engine scene id. */\n sceneId: string;\n /** Source scene's display name. */\n sceneName: string;\n /** Candidate tracks of this panel's type (may include disabled ones). */\n tracks: ImportCandidateTrack[];\n /**\n * True for the synthetic \"this scene — other panels\" entry: the ACTIVE\n * scene's MIDI tracks owned by OTHER panels. Importing one re-sounds the part\n * on the calling panel's instrument (via `readImportableTrackMidi` +\n * `writeMidiClip`) rather than faithfully copying it. Absent/false for\n * ordinary cross-scene entries. @since SDK 2.20.0\n */\n sameScene?: boolean;\n}\n\n/**\n * A source track's current sound, as returned by `host.getTrackSound`. The\n * discriminant matches the panel that reads it: drums → 'sample', instruments →\n * 'instrument', synths → 'preset'. `label` is the human name for the History row.\n * @since SDK 2.14.0\n */\n/**\n * How a synth `state` blob is serialized. `valuetree` is Tracktion's wrapped\n * format (default Surge XT presets); `raw` is the plugin's own\n * getStateInformation format (third-party instruments). Absent ⇒ `valuetree`,\n * for backward compatibility with history recorded before SDK 2.15.0.\n * @since SDK 2.15.0\n */\nexport type SynthStateType = 'raw' | 'valuetree';\n\nexport type TrackSoundSnapshot =\n | { kind: 'sample'; samplePath: string; label: string }\n | { kind: 'instrument'; displayName: string; instrumentId: string | null; zones: InstrumentZone[]; label: string }\n | { kind: 'preset'; state: string; label: string; stateType?: SynthStateType };\n\n/** Options for `PluginHost.listImportableTracks`. @since SDK 2.13.0 */\nexport interface ListImportableTracksOptions {\n /**\n * Coarse content family. 'midi' = synth/drum/instrument, 'audio' = stems,\n * 'sample' = loops. Defaults are derived from the calling plugin id, so\n * panels normally pass nothing.\n */\n family?: 'midi' | 'audio' | 'sample';\n /**\n * When true, prepend the active scene's MIDI tracks owned by OTHER panels as a\n * `sameScene` entry (the cross-panel re-sound source). Off by default so the\n * plain cross-scene import is unchanged. MIDI panels only. @since SDK 2.20.0\n */\n includeSameScene?: boolean;\n}\n\nexport interface PluginTrackInfo extends PluginTrackHandle {\n /** Is track muted? */\n muted: boolean;\n /** Is track soloed? */\n soloed: boolean;\n /** Volume (linear 0-1) */\n volume: number;\n /** Pan (-1 to 1) */\n pan: number;\n /** Loaded plugins on this track */\n plugins: PluginSynthInfo[];\n /** Has MIDI clips? */\n hasMidi: boolean;\n /** Has audio clips? */\n hasAudio: boolean;\n}\n\nexport interface PluginSynthInfo {\n index: number;\n name: string;\n type: string; // 'VST3' | 'AudioUnit' | 'Internal'\n enabled: boolean;\n}\n\n// ============================================================================\n// Real-time Track State Types\n// ============================================================================\n\n/** Real-time runtime state of a track (pushed from engine) */\nexport interface PluginTrackRuntimeState {\n id: string;\n muted: boolean;\n solo: boolean;\n volume: number;\n pan: number;\n}\n\n/** Listener for real-time track state changes */\nexport type TrackStateChangeListener = (trackId: string, state: PluginTrackRuntimeState) => void;\n\n// ============================================================================\n// FX Detail Types (SDK-friendly re-export)\n// ============================================================================\n\n/** Per-category FX detail state */\nexport interface PluginFxCategoryDetailState {\n enabled: boolean;\n presetIndex: number; // 0-4\n dryWet: number; // 0.0-1.0\n}\n\n/** Full FX detail state for a track — one entry per FX category */\nexport type PluginTrackFxDetailState = Record<string, PluginFxCategoryDetailState>;\n\n// ============================================================================\n// MIDI Types\n// ============================================================================\n\nexport interface MidiClipData {\n /** Start time in seconds */\n startTime: number;\n /** End time in seconds */\n endTime: number;\n /** BPM for beat<->time conversion */\n tempo: number;\n /** MIDI notes */\n notes: PluginMidiNote[];\n}\n\nexport interface PluginMidiNote {\n /** MIDI pitch 0-127 */\n pitch: number;\n /** Start position in quarter-note beats (0 = beginning of clip) */\n startBeat: number;\n /** Duration in quarter-note beats */\n durationBeats: number;\n /** Velocity 1-127 */\n velocity: number;\n /** MIDI channel 0-15 (default: 0) */\n channel?: number;\n}\n\nexport interface MidiWriteResult {\n /** Number of notes written */\n notesInserted: number;\n /** Actual bars covered */\n bars: number;\n}\n\n/**\n * One clip returned by {@link PluginHost.readMidiNotes}. `endTime - startTime`\n * (seconds) is the clip's loop span; round-trip it back into\n * {@link MidiClipData} on save so an edit never changes the clip length.\n * @since SDK 2.15.0\n */\nexport interface ReadMidiClip {\n /** Clip start in seconds (engine timeline). */\n startTime: number;\n /** Clip end in seconds. Loop span = endTime - startTime. */\n endTime: number;\n /** Beat-based notes, identical shape to {@link MidiClipData.notes}. */\n notes: PluginMidiNote[];\n}\n\n/**\n * Result of {@link PluginHost.readMidiNotes}: every clip on the track. Drum /\n * instrument / synth tracks are single-clip, so callers normally use\n * `clips[0]`; the array form mirrors the engine and is future-proof.\n * @since SDK 2.15.0\n */\nexport interface ReadMidiResult {\n clips: ReadMidiClip[];\n}\n\n/**\n * Options for {@link PluginHost.exportTracksAsMidiBundle}.\n * @since SDK 1.1.0\n */\nexport interface ExportMidiBundleOptions {\n /** Default ZIP filename suggested in the save dialog (without extension). */\n defaultName?: string;\n}\n\n/**\n * Result of {@link PluginHost.exportTracksAsMidiBundle}.\n * @since SDK 1.1.0\n */\nexport type ExportMidiBundleResult =\n | { success: true; filePath: string; trackCount: number; skippedCount: number }\n | { success: false; canceled: true }\n | { success: false; canceled?: false; error: string };\n\n// ============================================================================\n// Audio Processing Bridge (SDK 1.2.0 — see ai-orchestration-design.md §16)\n// ============================================================================\n\n/** @since SDK 1.2.0 */\nexport interface ExportTrackAudioResult {\n path: string;\n bpm: number;\n durationMs: number;\n fromCopyFastPath?: boolean;\n}\n\n/** @since SDK 1.2.0 */\nexport interface ProcessAudioResult {\n outputPath: string;\n}\n\n/** @since SDK 1.2.0 */\nexport type AudioProcessingOp =\n | { tool: 'normalize' }\n | { tool: 'compress'; params?: { threshold?: number; ratio?: number } }\n | { tool: 'eq'; params?: { low_gain?: number; mid_gain?: number; high_gain?: number } }\n | { tool: 'reverb'; params?: { room_size?: number; dry_wet?: number } }\n | { tool: 'pitch-shift'; params: { semitones: number } }\n | { tool: 'time-stretch'; params: { target_bpm: number } }\n | { tool: 'filter'; params: { type: 'lowpass' | 'highpass'; cutoff: number } }\n | { tool: 'gain'; params: { db: number } }\n | { tool: 'limit' }\n | { tool: 'trim'; params?: { start?: number; end?: number } };\n\nexport interface PostProcessOptions {\n /** Snap notes to grid (default: true) */\n quantize?: boolean;\n /** Grid size: '1/4', '1/8', '1/16', '1/32', '1/8T', '1/16T' (default: '1/16') */\n quantizeGrid?: string;\n /** Quantize strength 0-100 (default: 75) */\n quantizeStrength?: number;\n /** Swing amount 0-100 (default: 0) */\n swing?: number;\n /** Humanize timing/velocity variation 0-100 (default: 0) */\n humanize?: number;\n /** Enforce diatonic scale (default: false). Uses scene key/mode. */\n enforceScale?: boolean;\n /** Clamp notes to pitch range [low, high] */\n clampRegister?: [number, number];\n /** Remove overlapping notes on same pitch/channel (default: true) */\n removeOverlaps?: boolean;\n}\n\n// ============================================================================\n// Context Types\n// ============================================================================\n\nexport interface MusicalContext {\n key: string; // 'C', 'D', 'Eb', 'F#', etc.\n mode: string; // 'major', 'minor', 'dorian', 'mixolydian', etc.\n bpm: number; // 20-960\n bars: number; // Scene length in bars\n genre: string | null; // 'Drum & Bass', 'Lo-fi Hip Hop', etc.\n timeSignature: string; // '4/4', '3/4', '6/8'\n chordProgression: PluginChordTiming[];\n /**\n * The scene's natural-language contract prompt (e.g. \"dark psytrance,\n * driving 130 BPM, claustrophobic\"). Null when the scene has no\n * contract set yet. Auto-prefixed to the LLM by `host.generateWithLLM`\n * so every per-track generation sees the scene-level intent without\n * each plugin having to plumb it through manually.\n * @since SDK 1.2.0\n */\n contractPrompt: string | null;\n}\n\nexport interface PluginChordTiming {\n /** Chord symbol: 'Cm7', 'G', 'Fmaj7', etc. */\n symbol: string;\n /** Start position in quarter notes */\n startQn: number;\n /** End position in quarter notes */\n endQn: number;\n}\n\n/** Full generation context — includes concurrent track MIDI data */\nexport interface PluginGenerationContext {\n chordProgression: {\n key: { tonic: string; mode: string };\n chordsWithTiming: PluginChordTiming[];\n genre: string | null;\n };\n concurrentTracks: PluginConcurrentTrackInfo[];\n /**\n * Count of tracks the host had to drop entirely from `concurrentTracks`\n * because their notes pushed the running total past the cross-track\n * budget. Panels should disclose this to the LLM (e.g. \"… N additional\n * tracks omitted to fit token budget\") so the model knows it is\n * working with partial context.\n * @since SDK 1.2.0\n */\n truncatedTrackCount?: number;\n}\n\nexport interface PluginConcurrentTrackInfo {\n trackId: string;\n role: string | undefined;\n presetCategory: string | null;\n /** Notes organized by which chord they fall under */\n notesByChord: PluginChordSegment[];\n /**\n * The user-typed prompt that produced this track's MIDI (from\n * `tracks.prompt`). Lets the LLM see *intent* alongside the notes —\n * \"punchy 909 kick\" carries more meaning than the kick MIDI alone.\n * @since SDK 1.2.0\n */\n prompt?: string;\n /**\n * True when the host capped this track's notes (per-track budget).\n * The `notesByChord` payload is a prefix of the real content; the\n * total dropped count is `originalNoteCount - sum(notesByChord.notes.length)`.\n * @since SDK 1.2.0\n */\n truncated?: boolean;\n /** The track's full note count before per-track truncation. */\n originalNoteCount?: number;\n}\n\nexport interface PluginChordSegment {\n chord: string;\n chordRangeQn: [number, number];\n notes: PluginMidiNote[];\n}\n\n// ============================================================================\n// Transport Types\n// ============================================================================\n\nexport interface TransportEvent {\n type: 'play' | 'stop' | 'pause' | 'bpmChange' | 'positionChange';\n bpm?: number;\n position?: number; // in seconds\n isPlaying?: boolean;\n}\n\nexport interface DeckBoundaryEvent {\n deckId: string; // 'loop-a', 'loop-b'\n bar: number; // Current bar number (1-based)\n beat: number; // Current beat within bar (1-based)\n loopCount: number; // How many loops completed\n /**\n * Stream-time sample index at which the loop wrap was detected in the\n * audio thread (engine's AudioBoundaryProbe). Undefined when the\n * audio-thread anchor was unavailable. @since SDK 2.4.0\n */\n boundaryAudioSamplePosition?: number;\n /**\n * Monotonic host-time (nanoseconds) at the audio block in which the\n * loop wrap was detected. Same clock as\n * `juce::AudioIODeviceCallbackContext::hostTimeNs`. Pair with\n * `markRecordingChunkBoundary(boundaryHostTimeNs)` for sample-perfect\n * take alignment. @since SDK 2.4.0\n */\n boundaryHostTimeNs?: number;\n}\n\nexport interface PluginTransportState {\n isPlaying: boolean;\n isPaused: boolean;\n bpm: number;\n position: number; // in seconds\n timeSignature: string;\n}\n\n/**\n * Mono peak level for a single track, as reported by `getTrackLevels()`.\n * Drives the cosmetic per-track strip meters. `peakDb` is the max of the\n * L/R channels, floored at -120 (the \"no signal\" sentinel).\n * @since SDK 2.21.0\n */\nexport interface PluginTrackLevel {\n /** Tracktion engine track id — matches `PluginTrackHandle.id`. */\n trackId: string;\n /** Mono peak in dBFS (max of L/R), floored at -120. */\n peakDb: number;\n /** Latched overload since the last poll. */\n clipped: boolean;\n}\n\nexport interface PluginSceneInfo {\n id: string;\n name: string;\n isMuted: boolean;\n}\n\n/** Scene-level contract/context state passed to plugin UIs as a prop */\nexport interface PluginSceneContext {\n /** Whether a contract has been generated (genre or contractPrompt exists AND chords exist) */\n hasContract: boolean;\n /** Original user prompt text (e.g., \"dark psytrance\"). Null if none. */\n contractPrompt: string | null;\n /** Extracted genre. Null if none. */\n genre: string | null;\n /** Musical key. Null if no chord progression. */\n key: { tonic: string; mode: string } | null;\n /** Chord symbols (e.g., [\"Cm\", \"Fm\", \"G\"]). Empty if no chords. */\n chords: string[];\n /** BPM from project tempo */\n bpm: number;\n /** Scene length in bars */\n bars: number;\n /** Whether any synth tracks exist in this scene */\n hasTracks: boolean;\n /** Whether bulk generation is currently in progress */\n isBulkGenerating: boolean;\n /**\n * Scene kind. A 'transition' scene bridges two other scenes (the\n * transition-as-scene feature) and unlocks the crossfade-track UI in the\n * instrument panels; ordinary scenes are 'scene'. Absent on older hosts.\n * @since SDK 2.22.0\n */\n sceneType?: 'scene' | 'transition';\n /** For a transition scene, the DB id of the scene it bridges FROM (origin). Null otherwise. @since SDK 2.22.0 */\n transitionFromSceneId?: string | null;\n /** For a transition scene, the DB id of the scene it bridges TO (target). Null otherwise. @since SDK 2.22.0 */\n transitionToSceneId?: string | null;\n}\n\n/** Placeholder track state for the progressive bulk-add UX */\nexport interface BulkAddPlaceholderTrack {\n id: string;\n planIndex: number;\n role: string;\n description: string;\n status: 'planned' | 'creating' | 'completed' | 'failed';\n error?: string;\n}\n\nexport type TransportEventListener = (event: TransportEvent) => void;\nexport type DeckBoundaryListener = (event: DeckBoundaryEvent) => void;\nexport type SceneChangeListener = (sceneId: string | null) => void;\nexport type UnsubscribeFn = () => void;\n\n// ============================================================================\n// LLM Types\n// ============================================================================\n\nexport interface LLMGenerationRequest {\n /** System prompt (instructions, role, output format) */\n system: string;\n /** User prompt (the actual request) */\n user: string;\n /** Max tokens for response (host may cap this) */\n maxTokens?: number;\n /** Expected response format hint */\n responseFormat?: 'text' | 'json';\n /**\n * If true, the host will NOT auto-prefix the user prompt with musical\n * context (key, BPM, chords, genre, etc.). Default: false (context IS\n * prefixed automatically).\n */\n skipContextPrefix?: boolean;\n}\n\nexport interface LLMGenerationResult {\n /** Raw response text */\n content: string;\n /** Tokens consumed */\n tokensUsed: number;\n /** Model that generated the response */\n model: string;\n}\n\n// ----------------------------------------------------------------------------\n// Tool-use LLM types (Gemini-native shape, since SDK 2.4.0)\n// ----------------------------------------------------------------------------\n//\n// Plugins that want a Claude-Code / VS-Code-agent-mode loop call\n// `host.generateWithLLMTools(...)` with these shapes. The host forwards to\n// the gateway's Gemini-native passthrough endpoint, where Google's API key\n// is added centrally — plugins never see the raw key. Token usage is\n// tracked by the gateway just like `generateWithLLM`.\n//\n// Shapes mirror Gemini's REST `generateContent` surface deliberately. We do\n// not pull in `@google/genai` as a dependency: with the gateway as a\n// passthrough and the host owning auth, an SDK adds no value over typed\n// JSON, and we keep tighter control of breaking changes.\n\n/** A single part of a Gemini-style content block. */\nexport interface LLMPart {\n /** Plain text. Mutually exclusive with functionCall / functionResponse. */\n text?: string;\n /** A tool/function the model is asking the host to invoke. */\n functionCall?: {\n name: string;\n args: Record<string, unknown>;\n /**\n * Opaque signature returned by Gemini 3+ tool-use models. Must be echoed\n * verbatim when the assistant turn is replayed on a later iteration, or\n * the API rejects the request with a 400 (\"Function call is missing a\n * thought_signature in functionCall parts.\"). Pre-Gemini-3 models leave\n * this undefined; preserving it round-trip is safe across families.\n */\n thoughtSignature?: string;\n };\n /** The result of a tool call, fed back into the loop on the next turn. */\n functionResponse?: {\n name: string;\n response: Record<string, unknown>;\n };\n}\n\nexport interface LLMContent {\n /** 'user' = user/tool-result; 'model' = assistant. */\n role: 'user' | 'model';\n parts: LLMPart[];\n}\n\nexport interface LLMFunctionDeclaration {\n name: string;\n description: string;\n /** JSON Schema. Use `type: 'object'` with `properties` for any tool. */\n parameters: {\n type: 'object';\n properties?: Record<string, unknown>;\n required?: string[];\n };\n}\n\nexport interface LLMTool {\n functionDeclarations: LLMFunctionDeclaration[];\n}\n\nexport interface LLMGenerationConfig {\n temperature?: number;\n topP?: number;\n topK?: number;\n maxOutputTokens?: number;\n}\n\nexport interface LLMSystemInstruction {\n parts: { text: string }[];\n}\n\nexport interface LLMToolUseRequest {\n /** Gemini model id (e.g. 'gemini-2.5-flash'). */\n model: string;\n /** Conversation so far, including any tool-result turns. */\n contents: LLMContent[];\n /** System prompt as Gemini-native systemInstruction. */\n systemInstruction?: LLMSystemInstruction;\n /** Tool declarations the model may call. */\n tools?: LLMTool[];\n /** Optional tool-call mode override. */\n toolConfig?: {\n functionCallingConfig?: {\n mode?: 'AUTO' | 'ANY' | 'NONE';\n allowedFunctionNames?: string[];\n };\n };\n generationConfig?: LLMGenerationConfig;\n}\n\nexport interface LLMUsageMetadata {\n promptTokenCount: number;\n candidatesTokenCount: number;\n totalTokenCount: number;\n}\n\nexport interface LLMCandidate {\n content: LLMContent;\n finishReason?: string;\n index?: number;\n}\n\nexport interface LLMToolUseResponse {\n candidates: LLMCandidate[];\n usageMetadata?: LLMUsageMetadata;\n}\n\n// ============================================================================\n// Preset Types\n// ============================================================================\n\nexport interface PluginPresetData {\n name: string;\n category: string;\n /** Base64-encoded plugin state — pass to setPluginState() */\n state: string;\n}\n\n/** Result of shufflePreset() — the new preset that was applied */\nexport interface ShufflePresetResult {\n presetName: string;\n presetCategory: string;\n}\n\n/**\n * One entry in a track's in-session \"sound history\" — the data behind the\n * TrackRow ↩ back-arrow and the drawer \"History\" tab (see `useSoundHistory`).\n *\n * `descriptor` is opaque to the SDK: each generator plugin defines its own shape\n * (a drum sample path string, an instrument `{ displayName, zones }`, a synth\n * `{ pluginIndex, stateBase64 }`) and is the value handed back to the plugin's\n * `applySound` callback to re-apply the sound.\n */\nexport interface SoundHistoryEntry {\n /** Human-readable label shown in the History list (filename, preset/instrument name). */\n label: string;\n /** Opaque, plugin-defined value used to re-apply this sound. */\n descriptor: unknown;\n /** User-starred. Favorited entries are never auto-evicted by the history cap. */\n favorite?: boolean;\n}\n\n// ============================================================================\n// Settings Types\n// ============================================================================\n\nexport interface PluginSettingsSchema {\n type: 'object';\n properties: Record<string, SettingDefinition>;\n}\n\nexport interface SettingDefinition {\n type: 'string' | 'number' | 'boolean' | 'select';\n label: string;\n description?: string;\n default?: unknown;\n /** For 'select' type */\n options?: Array<{ label: string; value: string }>;\n /** For 'number' type */\n min?: number;\n max?: number;\n}\n\nexport interface PluginSettingsStore {\n get<T>(key: string, defaultValue: T): T;\n set(key: string, value: unknown): void;\n getAll(): Record<string, unknown>;\n /** Subscribe to settings changes. Returns unsubscribe fn. */\n onChange(listener: (key: string, value: unknown) => void): UnsubscribeFn;\n}\n\n// ============================================================================\n// Error Types\n// ============================================================================\n\nexport type PluginErrorCode =\n | 'NOT_OWNED' // Tried to modify a track not owned by this plugin\n | 'TRACK_NOT_FOUND' // Track ID doesn't exist in engine\n | 'TRACK_LIMIT_EXCEEDED' // Plugin has too many tracks\n | 'NO_ACTIVE_SCENE' // No scene selected\n | 'ENGINE_ERROR' // Tracktion engine call failed\n | 'INVALID_MIDI' // Malformed MIDI data\n | 'FILE_NOT_FOUND' // Audio file doesn't exist\n | 'INVALID_FORMAT' // Unsupported audio format\n | 'PLUGIN_NOT_FOUND' // VST/AU plugin not installed\n | 'LLM_BUDGET_EXCEEDED' // Over token limit\n | 'LLM_UNAVAILABLE' // Gateway unreachable\n | 'NOT_AUTHENTICATED' // User not logged in\n | 'TIMEOUT' // Operation timed out\n | 'CANCELLED' // User cancelled the operation\n | 'INCOMPATIBLE' // Plugin requires newer SDK version\n | 'CAPABILITY_DENIED' // Plugin lacks required capability\n | 'SECRET_NOT_FOUND' // Secret key doesn't exist\n | 'VALIDATION_ERROR' // Inputs failed schema/format validation\n | 'AUDIO_CAPTURE_DENIED'; // OS-level mic permission denied or input device unavailable\n\nexport class PluginError extends Error {\n public readonly code: PluginErrorCode;\n public readonly details?: Record<string, unknown>;\n\n constructor(\n code: PluginErrorCode,\n message: string,\n details?: Record<string, unknown>\n ) {\n super(message);\n this.name = 'PluginError';\n this.code = code;\n this.details = details;\n }\n}\n\n// ============================================================================\n// Plugin Manifest (on-disk plugin.json)\n// ============================================================================\n\nexport interface PluginManifest {\n id: string;\n displayName: string;\n version: string;\n description: string;\n generatorType: GeneratorType;\n main: string; // e.g., 'dist/index.js'\n renderer?: string; // e.g., 'dist/ui.bundle.js' (UMD bundle for renderer)\n icon?: string; // e.g., 'assets/icon.svg'\n author?: string;\n license?: string;\n minHostVersion?: string;\n capabilities?: PluginCapabilities;\n settings?: Record<string, SettingDefinition>;\n builtIn?: boolean;\n repository?: string; // e.g., 'https://github.com/user/my-plugin'\n}\n\nexport interface PluginCapabilities {\n requiresLLM?: boolean;\n requiresSurgeXT?: boolean;\n requiresNetwork?: boolean;\n /** Allowed network hosts for httpRequest (e.g., ['api.splice.com']) */\n network?: { allowedHosts?: string[] };\n /** Plugin needs native file dialog access */\n fileDialog?: boolean;\n /**\n * Plugin needs microphone / line-in capture. Gates the recording host\n * methods (getAudioInputDevices, startTrackRecording, etc).\n * @since SDK 2.1.0\n */\n audioCapture?: boolean;\n}\n\n// ============================================================================\n// Audio Recording (since SDK 2.1.0)\n// ============================================================================\n\n/**\n * Audio input device exposed by the audio engine. The `deviceId` is the\n * stable identifier returned by JUCE's AudioDeviceManager and accepted as\n * the device argument to `startTrackRecording`.\n * @since SDK 2.1.0\n */\nexport interface AudioInputDevice {\n /** Stable device identifier — passed back to startTrackRecording. */\n deviceId: string;\n /** Human-readable device name (e.g., \"MacBook Pro Microphone\", \"USB Mic\"). */\n label: string;\n /** True if this is the system default input device. */\n isDefault: boolean;\n /** Number of input channels the device supports (1 = mono, 2 = stereo). */\n channelCount: number;\n}\n\n/**\n * Engine state snapshot that an audio-recording plugin needs before\n * starting a session.\n * @since SDK 2.1.0\n */\nexport interface RecordingTargetInfo {\n /** Engine device sample rate, e.g. 44100 or 48000. */\n engineSampleRate: number;\n /** Active scene id, or null when no scene is selected. */\n sceneId: string | null;\n /** True when a transition render lock is held — recorder must refuse. */\n isRenderLocked: boolean;\n /** Current project BPM. */\n bpm: number;\n /** Active scene length in bars (4/4 assumed), or null when no scene. */\n bars: number | null;\n /**\n * Sample-perfect-recording compatibility (Path 2 gate). When false,\n * the recorder must refuse to start a session and surface\n * `recordingCompatibilityReason` to the user — input + output\n * devices cannot be sample-aligned.\n * @since SDK 2.4.0\n */\n canRecordSamplePerfect?: boolean;\n recordingCompatibilityReason?: string;\n}\n\n/**\n * Event payload fired when the engine finalizes a recording chunk WAV\n * file (either at a boundary mark or at session stop).\n * @since SDK 2.1.0\n */\nexport interface RecordingChunkFinalizedEvent {\n /** Absolute path to the finalized WAV file on disk. */\n filePath: string;\n /** Zero-based chunk index within the active session. */\n chunkIndex: number;\n /** Duration of this chunk in milliseconds. */\n durationMs: number;\n /** WAV sample rate. */\n sampleRate: number;\n /** WAV channel count. */\n channels: number;\n /**\n * Sample-perfect-recording metadata (Path 2). When the chunk was\n * closed via a host-time-anchored `markRecordingChunkBoundary` call,\n * carries recorder-local sample positions plus the host-time at\n * which the boundary fired. Undefined / -1 means the boundary\n * lacked a host-time anchor (legacy or stop-driven finalize).\n * @since SDK 2.4.0\n */\n recorderSampleStart?: number;\n recorderSampleEnd?: number;\n boundaryHostTimeNs?: number;\n}\n\n// ============================================================================\n// Phase 2: File System Types\n// ============================================================================\n\nexport interface PluginFileDialogOptions {\n title?: string;\n defaultPath?: string;\n filters?: Array<{ name: string; extensions: string[] }>;\n /** For open dialog: allow selecting multiple files */\n multiSelections?: boolean;\n /** For open dialog: allow selecting directories */\n directories?: boolean;\n}\n\nexport interface PluginDownloadOptions {\n /** HTTP headers to include */\n headers?: Record<string, string>;\n /** Overwrite if file exists (default: false) */\n overwrite?: boolean;\n}\n\n// ============================================================================\n// Phase 2: Network Types\n// ============================================================================\n\nexport interface PluginHttpRequestOptions {\n url: string;\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n headers?: Record<string, string>;\n body?: string | Record<string, unknown>;\n /** Timeout in milliseconds (default: 30000) */\n timeoutMs?: number;\n}\n\nexport interface PluginHttpResponse {\n status: number;\n statusText: string;\n headers: Record<string, string>;\n body: string;\n}\n\n// ============================================================================\n// Phase 2: Sample Library Types\n// ============================================================================\n\nexport interface PluginSampleFilter {\n bpm?: number;\n key?: { tonic: string; mode?: string };\n category?: string;\n searchQuery?: string;\n}\n\nexport interface PluginSampleInfo {\n id: string;\n filename: string;\n filePath: string;\n category: string | null;\n bpm: number | null;\n keyTonic: string | null;\n keyMode: string | null;\n durationSeconds: number | null;\n fileSizeBytes: number | null;\n tags: string[] | null;\n}\n\nexport interface PluginSampleImportResult {\n imported: number;\n skipped: number;\n errors: string[];\n}\n\n/** Sample track with associated sample metadata (returned by getPluginSampleTracks) */\nexport interface PluginSampleTrackInfo {\n track: PluginTrackHandle;\n sample: PluginSampleInfo;\n volume: number;\n pan: number;\n}\n\n// ============================================================================\n// Phase 2: Audio Generation Types\n// ============================================================================\n\nexport interface PluginAudioTextureRequest {\n /** Text prompt describing the audio texture */\n prompt: string;\n /** Duration in seconds (default: scene length) */\n durationSeconds?: number;\n /** Target BPM (default: project BPM) */\n bpm?: number;\n}\n\nexport interface PluginAudioTextureResult {\n /** Path to the generated audio file */\n filePath: string;\n /** Duration of the generated audio in seconds */\n durationSeconds: number;\n /**\n * Beat positions inside the generated audio file plus the detected BPM.\n * Sample positions are relative to the file at `filePath`. Null when the\n * audio-processor did not surface detection data (older binary, fallback\n * path, or processing failed). Persist via `host.setCuePoints` after the\n * clip is written so the OffsetScrubber UI can read them later.\n */\n cuePoints: PluginCuePoints | null;\n /**\n * Path to the un-trimmed (raw) Lyria output. Used by the stems\n * trim editor to draw the full waveform. Persist via\n * `host.setRawAudioFilePath`. Null when no raw file is available.\n */\n rawFilePath?: string | null;\n /** Same beats as `cuePoints` in raw-file sample coordinates. */\n rawCuePoints?: PluginCuePoints | null;\n /**\n * Auto-detected start of the trim window inside the raw file (sample\n * offset). Null when detection was skipped.\n */\n inputStartSample?: number | null;\n}\n\n/**\n * Cue-points sidecar surfaced by the audio-processor `trim` command —\n * sample positions for each detected beat inside the generated WAV.\n * Mirrors the canonical `CuePoints` shape from the assistant; duplicated\n * here so external plugins don't reach into sas-app internals.\n */\nexport interface PluginCuePoints {\n /** Schema version (currently 1). */\n schema: 1;\n /** Sample rate the beat positions are expressed in. */\n sample_rate: number;\n /** Detected BPM (may differ from project BPM). Null when detection failed. */\n detected_bpm: number | null;\n /** Sample position of bar 1 / beat 1 inside the clip. */\n downbeat_sample: number;\n /** Monotone-increasing array of beat positions in samples. */\n beats: number[];\n /** ISO-8601 timestamp of when detection ran. */\n detected_at: string;\n}\n\n/**\n * A trim window inside a raw (un-trimmed) audio file. `start_sample` is\n * the offset from the start of the raw file; `duration_samples` is the\n * length of the trimmed slice. Both are in raw-file sample coordinates.\n */\nexport interface PluginTrimWindow {\n start_sample: number;\n duration_samples: number;\n}\n\n// ============================================================================\n// Scene Composition Types\n// ============================================================================\n\n/** Options for composing a full scene arrangement via LLM. */\nexport interface ComposeSceneOptions {\n /** The contract prompt / musical direction for the arrangement. */\n contractPrompt: string;\n /** Genre hint (e.g. 'techno', 'jazz'). Optional. */\n genre?: string | null;\n}\n\n/** Result from a scene composition. */\nexport interface ComposeSceneResult {\n /** Whether the composition completed successfully. */\n success: boolean;\n /** Number of tracks created. */\n tracksCreated: number;\n /** Error message if not successful. */\n error?: string;\n}\n\n/** Listener for composition progress events. */\nexport type ComposeProgressListener = (event: ComposeProgressEvent) => void;\n\n/** Progress event emitted during scene composition. */\nexport interface ComposeProgressEvent {\n /** Current phase: 'planning' (LLM deciding tracks), 'generating' (creating MIDI), 'complete', 'error'. */\n phase: 'planning' | 'generating' | 'complete' | 'error';\n /** Per-track placeholder state (available once planning is done). */\n placeholders?: BulkAddPlaceholderTrack[];\n /** Error message when phase is 'error'. */\n error?: string;\n /** Scene ID this compose event belongs to (for scene-keyed UI state). */\n sceneId?: string;\n}\n\n// ============================================================================\n// Phase 2: Plugin Preset Types\n// ============================================================================\n\nexport interface PluginPresetInfo {\n id: string;\n name: string;\n category: string | null;\n isBuiltIn: boolean;\n data: Record<string, unknown>;\n}\n\nexport interface SavePluginPresetOptions {\n name: string;\n category?: string;\n data: Record<string, unknown>;\n}\n\n// ============================================================================\n// App Tool Bridge (since SDK 1.2.0)\n// ============================================================================\n\n/** JSON Schema shape for a tool's input params. */\nexport interface PluginAppToolInputSchema {\n type: 'object';\n properties?: Record<string, unknown>;\n required?: string[];\n}\n\n/** Lightweight descriptor returned by `PluginHost.listAppTools`. */\nexport interface PluginAppTool {\n name: string;\n description: string;\n inputSchema: PluginAppToolInputSchema;\n /** `'scene'` = safe for scene-scoped callers. `'project'` = cross-scene. */\n scope?: 'scene' | 'project';\n /**\n * `true` = the operation cannot be undone via the host's checkpoint/undo\n * system (project delete, disk overwrite, external export, …). The host\n * gates such calls behind a user-approval flow when invoked with agent\n * provenance; agent UIs may also surface the flag (e.g. ⚠ in a tool list).\n * @since SDK 2.18.0\n */\n irreversible?: boolean;\n}\n\n/** Result shape returned by `PluginHost.executeAppTool`. */\nexport interface PluginAppToolResult {\n success: boolean;\n action: string;\n message?: string;\n error?: string;\n /**\n * Tool-specific payload. Concrete shape depends on the tool — callers\n * should treat this as opaque unless they know the tool.\n */\n data?: unknown;\n}\n\n// ============================================================================\n// Plugin Registry Types (used by host internals)\n// ============================================================================\n\nexport type PluginStatus = 'pending' | 'active' | 'failed' | 'disabled' | 'incompatible';\n\nexport interface PluginRegistration {\n /** The loaded plugin instance */\n plugin: GeneratorPlugin;\n /** Current status */\n status: PluginStatus;\n /** Resolved manifest from disk */\n manifest: PluginManifest;\n /** The scoped PluginHost instance for this plugin */\n host: PluginHost | null;\n /** Sort order for accordion display */\n sortOrder: number;\n /** Whether the plugin is enabled */\n enabled: boolean;\n /** Error message if status is 'failed' */\n error?: string;\n}\n","/**\n * FX Toggle Types\n *\n * Types and constants for per-track FX toggle buttons.\n * Each track can enable/disable 6 FX categories independently.\n * The engine is the source of truth — no database persistence needed.\n */\n\n/** Available FX categories in signal chain order */\nexport type FxCategory = 'eq' | 'compressor' | 'chorus' | 'phaser' | 'delay' | 'reverb';\n\n/** All FX categories in signal chain order */\nexport const FX_CATEGORIES: readonly FxCategory[] = [\n 'eq',\n 'compressor',\n 'chorus',\n 'phaser',\n 'delay',\n 'reverb',\n] as const;\n\n/** Position in the signal chain (lower = earlier) */\nexport const FX_CHAIN_ORDER: Record<FxCategory, number> = {\n eq: 0,\n compressor: 1,\n chorus: 2,\n phaser: 3,\n delay: 4,\n reverb: 5,\n};\n\n/** Map from FxCategory to Tracktion Engine built-in plugin xmlTypeName */\nexport const FX_ENGINE_PLUGIN_NAMES: Record<FxCategory, string> = {\n eq: '4bandEq',\n compressor: 'compressor',\n chorus: 'chorus',\n phaser: 'phaser',\n delay: 'delay',\n reverb: 'reverb',\n};\n\n/** Display labels for UI buttons */\nexport const FX_DISPLAY_LABELS: Record<FxCategory, string> = {\n eq: 'EQ',\n compressor: 'Comp',\n chorus: 'Chorus',\n phaser: 'Phaser',\n delay: 'Delay',\n reverb: 'Reverb',\n};\n\n/** Per-track FX state: which categories are active */\nexport interface TrackFxState {\n eq: boolean;\n compressor: boolean;\n chorus: boolean;\n phaser: boolean;\n delay: boolean;\n reverb: boolean;\n}\n\n/** Default state: all FX disabled */\nexport const EMPTY_FX_STATE: TrackFxState = {\n eq: false,\n compressor: false,\n chorus: false,\n phaser: false,\n delay: false,\n reverb: false,\n};\n\n// ============================================================================\n// Preset Types\n// ============================================================================\n\n/** A single FX preset definition */\nexport interface FxPreset {\n /** Display name (e.g. \"Room\", \"Hall\") */\n name: string;\n /** Short label for button (e.g. \"RM\", \"HL\") */\n shortLabel: string;\n /** Map from automatable parameter name -> value (set via setPluginParameter) */\n params: Record<string, number>;\n /** CachedValue params set via XML state (getPluginState/setPluginState) */\n xmlStateParams?: Record<string, number>;\n /** BPM-relative delay time multiplier (1.0 = quarter note). When set, Delay Time is computed at apply time. */\n noteMultiplier?: number;\n /** Fixed delay time in ms (non-BPM-synced). Mutually exclusive with noteMultiplier. */\n fixedLengthMs?: number;\n}\n\n/** How dry/wet is applied to the plugin */\nexport type MixInterpolation = 'direct' | 'gain-scale' | 'ratio-scale';\n\n/** Preset configuration for an FX category */\nexport interface FxPresetConfig {\n /** Exactly 5 presets */\n presets: [FxPreset, FxPreset, FxPreset, FxPreset, FxPreset];\n /** Name of the native mix/wet parameter, or null if no native dry/wet */\n mixParamName: string | null;\n /** XML attribute name for dry/wet control (for plugins with no automatable mix param, e.g. chorus/phaser) */\n mixXmlAttr?: string;\n /** How to apply dry/wet (defaults to 'direct') */\n mixInterpolation: MixInterpolation;\n}\n\n/** Per-category detail state for a single FX on a track */\nexport interface FxCategoryDetailState {\n enabled: boolean;\n presetIndex: number; // 0-4\n dryWet: number; // 0.0-1.0\n}\n\n/** Extended FX state per track with preset and dry/wet info */\nexport type TrackFxDetailState = Record<FxCategory, FxCategoryDetailState>;\n\n/** Default dry/wet mix level (33% — musically useful for most effects) */\nexport const DEFAULT_FX_DRY_WET = 0.33;\n\n/** Default detail state for a single category */\nexport const DEFAULT_FX_CATEGORY_DETAIL: FxCategoryDetailState = {\n enabled: false,\n presetIndex: 0,\n dryWet: DEFAULT_FX_DRY_WET,\n};\n\n/** Default detail state: all FX disabled, preset 0, full wet */\nexport const EMPTY_FX_DETAIL_STATE: TrackFxDetailState = {\n eq: { ...DEFAULT_FX_CATEGORY_DETAIL },\n compressor: { ...DEFAULT_FX_CATEGORY_DETAIL },\n chorus: { ...DEFAULT_FX_CATEGORY_DETAIL },\n phaser: { ...DEFAULT_FX_CATEGORY_DETAIL },\n delay: { ...DEFAULT_FX_CATEGORY_DETAIL },\n reverb: { ...DEFAULT_FX_CATEGORY_DETAIL },\n};\n\n/** Persisted FX data for a single category (stored as JSON in database) */\nexport interface FxPresetDataEntry {\n presetIndex: number;\n dryWet: number;\n enabled: boolean;\n}\n\n/** Persisted FX data format (stored as JSON in database) */\nexport type FxPresetData = Partial<Record<FxCategory, FxPresetDataEntry>>;\n","/**\n * SDK TrackRow — Reusable track row component for generator plugins.\n *\n * Renders a complete track UI with prompt input, generation controls,\n * shuffle/copy, volume/pan, mute/solo, FX drawer, and visual states\n * (amber pulse for \"needs generation\", progress overlay, error indicator).\n *\n * Layout matches TrackInput (main branch) for visual parity.\n *\n * Depends only on PluginHost types + existing shared renderer components.\n */\n\nimport React from 'react';\nimport { AlertCircle, ChevronDown, GripVertical } from 'lucide-react';\nimport { TrackDrawer, type DrawerTab } from './TrackDrawer';\nimport { ConfirmDialog } from './ConfirmDialog';\nimport { TrackMeterStrip } from './TrackMeterStrip';\nimport type { TrackLevelsHandle } from '../hooks/useTrackLevels';\nimport type { InstrumentDescriptor, SoundHistoryEntry, PluginMidiNote } from '../types/plugin-sdk.types';\nimport type { TrackRowDragProps } from '../hooks/useTrackReorder';\nimport { VolumeSlider } from './VolumeSlider';\nimport { PanSlider } from './PanSlider';\nimport { SorceryProgressBar } from './SorceryProgressBar';\nimport type { TrackFxDetailState, FxCategory } from '../types/fx-toggle.types';\n\n// ============================================================================\n// Props\n// ============================================================================\n\nexport interface SDKTrackRowProps {\n /** Track identity */\n track: { id: string; name: string; role?: string };\n /** Current prompt text (optional — omit when using contentSlot) */\n prompt?: string;\n /** Playback state */\n runtimeState: { muted: boolean; solo: boolean; volume: number; pan: number };\n /** True when ANOTHER track is soloed, so this (non-soloed) track is currently\n * silenced. Renders the row dimmed while leaving its Mute button UNLIT — the\n * engine's effective-mute model silences it without touching user-mute. Purely\n * visual; does not change mute/solo state. */\n soloedOut?: boolean;\n /** FX category states */\n fxDetailState: TrackFxDetailState;\n /** Whether the unified track drawer is open. */\n drawerOpen: boolean;\n /** Which tab the drawer is showing. */\n drawerTab: DrawerTab;\n /** Switch the active drawer tab (tab-strip clicks). Omit for single-tab panels (e.g. loops = FX only). */\n onTabChange?: (tab: DrawerTab) => void;\n /** Generation in progress */\n isGenerating?: boolean;\n /** Auth state */\n isAuthenticated?: boolean;\n /** Error from last generation */\n error?: string | null;\n /** Enables shuffle/copy buttons */\n hasMidi?: boolean;\n /** Progress % (for persistence across scene switches) */\n generationProgress?: number;\n /** For progress bar pacing */\n estimatedGenerationMs?: number;\n /** Prompt edit (optional — omit to hide prompt input) */\n onPromptChange?: (prompt: string) => void;\n /** \"Create\" button / Enter key (optional — omit to hide Create button) */\n onGenerate?: () => void;\n /** Shuffle preset (optional — omit to hide Shuffle button) */\n onShuffle?: () => void;\n /** Duplicate track (optional — omit to hide Copy button) */\n onCopy?: () => void;\n /** Delete track. Optional — omit to hide the delete button (e.g. a composite\n * like CrossfadeTrackRow owns a single delete for the whole pair). */\n onDelete?: () => void;\n /** Custom content replacing the prompt input (e.g., sample info display) */\n contentSlot?: React.ReactNode;\n /** Toggle mute */\n onMuteToggle: () => void;\n /** Toggle solo */\n onSoloToggle: () => void;\n /** Volume slider */\n onVolumeChange: (vol: number) => void;\n /** Pan slider */\n onPanChange: (pan: number) => void;\n /** FX category toggle (optional — omit to hide FX button) */\n onFxToggle?: (cat: FxCategory, enabled: boolean) => void;\n /** FX preset select */\n onFxPresetChange?: (cat: FxCategory, idx: number) => void;\n /** FX dry/wet */\n onFxDryWetChange?: (cat: FxCategory, val: number) => void;\n /** Open/close FX (optional — omit to hide FX button) */\n onToggleFxDrawer?: () => void;\n /** Progress persistence callback */\n onProgressChange?: (pct: number) => void;\n /** Left border accent color */\n accentColor?: string;\n // --- Instrument Plugin Selection ---\n /** Current instrument display name (null/undefined = Surge XT default) */\n instrumentName?: string | null;\n /** Whether the current instrument plugin is missing from the system */\n instrumentMissing?: boolean;\n /** Open/close the drawer to a non-FX tab (the ▾ button). Omit to hide it. */\n onToggleDrawer?: () => void;\n /** Available instrument plugins for the drawer */\n availableInstruments?: InstrumentDescriptor[];\n /** Currently loaded instrument plugin ID */\n currentInstrumentPluginId?: string | null;\n /** Called when user selects an instrument from the drawer */\n onInstrumentSelect?: (pluginId: string) => void;\n /** Whether instrument scan is loading */\n instrumentsLoading?: boolean;\n /** Re-scan for instruments */\n onRefreshInstruments?: () => void;\n // --- Instrument Editor (Stage 2) ---\n /** Pick-tab sub-view: native plugin editor instead of the instrument grid. */\n editorStage?: boolean;\n /** Called when user clicks \"Open Editor\" */\n onShowEditor?: () => void;\n /** Called when user wants to go back from editor view */\n onBackToInstruments?: () => void;\n // --- Sound History (drawer \"History\" tab) ---\n /** Ordered list of sounds this track has had this session. */\n soundHistory?: readonly SoundHistoryEntry[];\n /** Index into soundHistory of the currently-applied sound. */\n soundHistoryCursor?: number;\n /** Restore a sound from the History tab by index. */\n onRestoreSound?: (index: number) => void;\n /** Toggle the favorite (⭐) flag on a history entry. */\n onToggleFavorite?: (index: number) => void;\n /** Open the drawer's sound-import picker; omit to hide the button. */\n onImportSound?: () => void;\n /** Sound-import button label (\"Import Sample\" / \"Import Preset\"). */\n importSoundLabel?: string;\n // --- Edit tab (piano-roll MIDI editor) ---\n /** Current MIDI notes for the piano-roll editor (the 'edit' tab). */\n editNotes?: readonly PluginMidiNote[];\n /** Persist edited notes; PRESENCE of this callback enables the Edit tab. */\n onNotesChange?: (notes: PluginMidiNote[]) => void;\n /** Scene length in bars (piano-roll grid width). */\n editBars?: number;\n /** Scene BPM (piano-roll audition timing). */\n editBpm?: number;\n /** Snap step in quarter notes for the piano roll (default 0.25). */\n editSnap?: number;\n /** Optional single-note preview when the user adds a note. */\n onAuditionNote?: (pitch: number, velocity: number, durationMs: number) => void;\n // --- Drag-to-reorder ---\n /** Drag props from {@link useTrackReorder}. When present, renders the grip\n * handle and makes the row a drop target. Omit for non-reorderable lists. */\n drag?: TrackRowDragProps;\n // --- Per-track peak meter (cosmetic) ---\n /** Shared meter handle from `useTrackLevels(host, isPlaying)`. When present,\n * a thin peak meter welds to the bottom of the row. Omit to hide it. */\n levels?: TrackLevelsHandle;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function TrackRow({\n track,\n prompt,\n runtimeState,\n soloedOut = false,\n fxDetailState,\n drawerOpen,\n drawerTab,\n onTabChange,\n isGenerating = false,\n isAuthenticated = false,\n error,\n hasMidi = false,\n generationProgress = 0,\n estimatedGenerationMs = 15000,\n onPromptChange,\n onGenerate,\n onShuffle,\n onCopy,\n onDelete,\n contentSlot,\n onMuteToggle,\n onSoloToggle,\n onVolumeChange,\n onPanChange,\n onFxToggle,\n onFxPresetChange,\n onFxDryWetChange,\n onToggleFxDrawer,\n onProgressChange,\n accentColor = '#A78BFA',\n instrumentName,\n instrumentMissing,\n onToggleDrawer,\n availableInstruments,\n currentInstrumentPluginId,\n onInstrumentSelect,\n instrumentsLoading,\n onRefreshInstruments,\n editorStage,\n onShowEditor,\n onBackToInstruments,\n soundHistory,\n soundHistoryCursor,\n onRestoreSound,\n onToggleFavorite,\n onImportSound,\n importSoundLabel,\n editNotes,\n onNotesChange,\n editBars,\n editBpm,\n editSnap,\n onAuditionNote,\n drag,\n levels,\n}: SDKTrackRowProps): React.ReactElement {\n const { muted: isMuted, solo: isSoloed, volume: currentVolume, pan: currentPan } = runtimeState;\n\n // Guard the (irreversible) delete behind a confirmation modal — the bare \"x\"\n // was one stray click away from losing a track's MIDI + sound.\n const [confirmDelete, setConfirmDelete] = React.useState(false);\n\n // \"Needs generation\" = has prompt, no MIDI yet, not currently generating\n const needsGeneration = !!(prompt?.trim() && !hasMidi && !isGenerating);\n\n const hasFxActive = Object.values(fxDetailState).some(\n (d: { enabled: boolean }) => d.enabled\n );\n\n // The two row buttons open the SAME unified drawer to different tabs:\n // FX → the 'fx' tab; ▾ → a non-FX tab (History/Pick/Import).\n const fxTabOpen = drawerOpen && drawerTab === 'fx';\n const soundTabOpen = drawerOpen && drawerTab !== 'fx';\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {\n if (e.key === 'Enter' && !e.shiftKey && onGenerate) {\n e.preventDefault();\n onGenerate();\n }\n };\n\n // Amber pulse class for \"needs generation\" state\n const borderColorStyle = needsGeneration\n ? undefined // handled by className animation\n : accentColor;\n\n const borderClass = needsGeneration\n ? 'border-amber-400 animate-pulse'\n : 'border-sas-border';\n\n return (\n <div data-testid=\"sdk-track-row-wrapper\" className=\"w-full\" {...(drag?.rowProps ?? {})}>\n <div\n data-testid=\"sdk-track-row\"\n className={`relative flex items-stretch gap-1 p-2 ${levels ? 'rounded-t-sm' : 'rounded-sm'} border w-full overflow-hidden ${borderClass} bg-sas-panel-alt ${drag?.isDragging ? 'opacity-40' : ''} ${drag?.isDragTarget ? 'ring-2 ring-sas-accent ring-inset' : ''}`}\n style={{\n borderLeftColor: needsGeneration ? '#f59e0b' : borderColorStyle,\n borderLeftWidth: '3px',\n }}\n >\n {/* Drag-to-reorder grip — only when reorder is enabled. z-30 keeps it\n above the generating overlay; only the grip is draggable, so the\n row's inputs and sliders stay interactive. */}\n {drag && (\n <div\n data-testid=\"sdk-drag-handle\"\n {...drag.handleProps}\n className=\"flex-shrink-0 self-stretch flex items-center -ml-0.5 pr-0.5 text-sas-muted/40 hover:text-sas-muted cursor-grab active:cursor-grabbing relative z-30\"\n title=\"Drag to reorder\"\n aria-label=\"Drag to reorder track\"\n >\n <GripVertical className=\"w-3.5 h-3.5\" strokeWidth={2} />\n </div>\n )}\n\n {/* Generating progress overlay - stops before buttons (right-44) */}\n {isGenerating && (\n <div className=\"absolute left-0 top-0 bottom-0 right-44 z-20\">\n <SorceryProgressBar\n isLoading={true}\n statusText=\"CONJURING MIDI...\"\n heightClass=\"h-full\"\n initialProgress={generationProgress}\n onProgressChange={onProgressChange}\n estimatedDurationMs={estimatedGenerationMs}\n />\n </div>\n )}\n\n {/* Left: Content area (prompt input or custom content slot) with track name, volume, and pan underneath.\n Dimmed when soloed-out (silenced by another track's solo); the Mute/Solo\n buttons below stay full-opacity and interactive so the user can un-solo. */}\n <div\n data-testid=\"sdk-track-content\"\n className={`flex flex-col flex-1 min-w-0 relative z-10 transition-opacity ${soloedOut ? 'opacity-40' : ''}`}\n title={soloedOut ? 'Silenced — another track is soloed' : undefined}\n >\n {contentSlot ? contentSlot : onPromptChange ? (\n <input\n type=\"text\"\n data-testid=\"sdk-prompt-input\"\n value={prompt ?? ''}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => onPromptChange(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"Describe your part...\"\n disabled={isGenerating}\n className=\"sas-input w-full px-2 py-1 text-xs disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n ) : null}\n {/* Track name, volume slider, and pan slider in horizontal row */}\n <div className=\"flex items-center gap-2 mt-1\">\n {track.name && (\n <span className=\"text-[10px] text-sas-muted/60 truncate pl-2 flex-shrink-0 max-w-[80px]\" title={track.name}>\n {track.name}\n </span>\n )}\n <span className=\"text-[9px] text-sas-muted/50 flex-shrink-0\">vol:</span>\n <VolumeSlider\n value={currentVolume}\n onChange={onVolumeChange}\n disabled={isGenerating}\n className=\"flex-1 min-w-[40px]\"\n />\n <span className=\"text-[9px] text-sas-muted/50 flex-shrink-0\">pan:</span>\n <PanSlider\n value={currentPan}\n onChange={onPanChange}\n disabled={isGenerating}\n className=\"w-10 flex-shrink-0\"\n />\n </div>\n </div>\n\n {/* Error indicator - shows when generation failed */}\n {error && (\n <div\n data-testid=\"sdk-error-indicator\"\n className=\"flex-shrink-0 relative z-10 self-stretch flex items-center px-1 group cursor-help\"\n title={error}\n >\n <div className=\"relative\">\n <AlertCircle\n className=\"w-5 h-5 text-red-500 animate-pulse\"\n strokeWidth={2.5}\n />\n {/* Tooltip - appears on hover */}\n <div className=\"absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-red-900/95 text-red-100 text-xs rounded shadow-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 max-w-[200px] truncate\">\n {error}\n </div>\n </div>\n </div>\n )}\n\n {/* Right: Button grid (2 rows) - z-30 to stay above generating overlay */}\n <div className=\"flex flex-col gap-0.5 flex-shrink-0 relative z-30 justify-center\">\n {/* Top row: [Create] [Copy] M x — Create/Copy only shown when handlers provided */}\n <div className=\"flex gap-1 items-center\">\n {onGenerate && (\n <button\n data-testid=\"sdk-generate-button\"\n onClick={onGenerate}\n disabled={!isAuthenticated || isGenerating || !prompt?.trim()}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n !isAuthenticated || isGenerating\n ? 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n : needsGeneration\n ? 'bg-amber-500/30 border-amber-500 text-amber-400 hover:bg-amber-500 hover:text-sas-bg animate-pulse'\n : prompt?.trim()\n ? 'bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg'\n : 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n }`}\n title={!isAuthenticated ? 'Please log in' : isGenerating ? 'Generating...' : 'Generate MIDI'}\n >\n Create\n </button>\n )}\n {onCopy && (\n <button\n data-testid=\"sdk-copy-button\"\n onClick={onCopy}\n disabled={!hasMidi || isGenerating}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n !hasMidi || isGenerating\n ? 'bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed'\n : 'bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={hasMidi ? 'Duplicate track with different preset' : 'Generate MIDI first'}\n >\n Copy\n </button>\n )}\n <button\n data-testid=\"sdk-mute-button\"\n onClick={onMuteToggle}\n disabled={isGenerating}\n className={`px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${\n isGenerating\n ? 'bg-sas-panel text-sas-muted/50 cursor-not-allowed'\n : isMuted\n ? 'bg-red-600 text-white'\n : 'bg-sas-panel-alt text-sas-muted hover:bg-sas-border'\n }`}\n title={isMuted ? 'Unmute track' : 'Mute track'}\n >\n M\n </button>\n {onDelete && (\n <button\n data-testid=\"sdk-delete-button\"\n onClick={() => setConfirmDelete(true)}\n className=\"text-sas-danger/70 hover:text-sas-danger px-1 py-0.5 transition-colors text-sm\"\n title=\"Delete track\"\n >\n x\n </button>\n )}\n </div>\n {/* Bottom row: [Shuffle] [FX] Solo [▾] */}\n <div className=\"flex gap-1 items-center\">\n {onShuffle && (\n <button\n data-testid=\"sdk-shuffle-button\"\n onClick={onShuffle}\n disabled={!hasMidi || isGenerating || !!currentInstrumentPluginId}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n !hasMidi || isGenerating || !!currentInstrumentPluginId\n ? 'bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed'\n : 'bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={\n currentInstrumentPluginId\n ? 'Shuffle only works with default Surge XT'\n : hasMidi\n ? 'Re-roll sound (keep MIDI)'\n : 'Generate MIDI first'\n }\n >\n Shuffle\n </button>\n )}\n {onToggleFxDrawer && (\n <button\n data-testid=\"sdk-fx-button\"\n onClick={onToggleFxDrawer}\n disabled={isGenerating}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n isGenerating\n ? 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n : fxTabOpen\n ? 'bg-sas-accent border-sas-accent text-sas-bg'\n : hasFxActive\n ? 'bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg'\n : 'bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={fxTabOpen ? 'Hide FX controls' : 'Show FX controls'}\n >\n FX\n </button>\n )}\n <button\n data-testid=\"sdk-solo-button\"\n onClick={onSoloToggle}\n disabled={isGenerating}\n className={`px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${\n isGenerating\n ? 'bg-sas-panel text-sas-muted/50 cursor-not-allowed'\n : isSoloed\n ? 'bg-yellow-500 text-black'\n : 'bg-sas-panel-alt text-sas-muted hover:bg-sas-border'\n }`}\n title={isSoloed ? 'Unsolo track' : 'Solo track'}\n >\n S\n </button>\n {onToggleDrawer && (\n <button\n data-testid=\"sdk-plugin-button\"\n onClick={onToggleDrawer}\n disabled={isGenerating}\n className={`px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${\n isGenerating\n ? 'bg-sas-panel text-sas-muted/50 cursor-not-allowed'\n : soundTabOpen\n ? 'bg-sas-accent border-sas-accent text-sas-bg'\n : instrumentMissing\n ? 'bg-amber-500/20 text-amber-400 hover:bg-amber-500/40'\n : 'bg-sas-panel-alt text-sas-muted hover:bg-sas-border'\n }`}\n title={`Sound — presets & history${instrumentMissing ? ' (instrument missing)' : ''}`}\n >\n <ChevronDown className=\"w-3 h-3\" strokeWidth={2.5} />\n </button>\n )}\n </div>\n </div>\n </div>\n\n {/* Thin per-track peak meter, welded to the bottom of the row (cosmetic).\n Isolated in TrackMeterStrip so its ~30Hz updates re-render only the\n strip, never this whole row. Squared bottom when a drawer welds below. */}\n {levels && (\n <TrackMeterStrip levels={levels} trackId={track.id} roundBottom={!drawerOpen} />\n )}\n\n {/* Unified track drawer — one drawer, contextual tabs (FX / Pick / History / Import).\n The FX button opens it to 'fx'; the ▾ button to a non-FX tab. Which tabs\n appear is computed inside TrackDrawer from the callbacks provided. */}\n {drawerOpen && (\n <div\n data-testid=\"sdk-track-drawer\"\n className=\"border border-t-0 border-sas-border bg-sas-bg rounded-b-sm px-3 py-2 max-h-[260px] overflow-y-auto\"\n >\n <TrackDrawer\n activeTab={drawerTab}\n onTabChange={onTabChange}\n trackId={track.id}\n fxState={fxDetailState}\n onFxToggle={onFxToggle}\n onFxPresetChange={onFxPresetChange}\n onFxDryWetChange={onFxDryWetChange}\n fxDisabled={isGenerating}\n instruments={availableInstruments}\n currentPluginId={currentInstrumentPluginId ?? null}\n isLoading={instrumentsLoading ?? false}\n onSelect={onInstrumentSelect}\n onRefresh={onRefreshInstruments}\n editorStage={editorStage}\n onShowEditor={onShowEditor}\n onBackToInstruments={onBackToInstruments}\n selectedInstrumentName={instrumentName}\n soundHistory={soundHistory}\n soundHistoryCursor={soundHistoryCursor}\n onRestoreSound={onRestoreSound}\n onToggleFavorite={onToggleFavorite}\n onImportSound={onImportSound}\n importSoundLabel={importSoundLabel}\n editNotes={editNotes}\n onNotesChange={onNotesChange}\n editBars={editBars}\n editBpm={editBpm}\n editSnap={editSnap}\n onAuditionNote={onAuditionNote}\n />\n </div>\n )}\n\n <ConfirmDialog\n open={confirmDelete}\n title=\"Delete track?\"\n message={\n <>\n <span className=\"text-sas-text\">{track.name?.trim() || 'This track'}</span> will be\n permanently removed from this scene. This cannot be undone.\n </>\n }\n confirmLabel=\"Delete\"\n onConfirm={() => {\n setConfirmDelete(false);\n onDelete?.();\n }}\n onCancel={() => setConfirmDelete(false)}\n testIdPrefix=\"track-delete-confirm\"\n />\n </div>\n );\n}\n\nexport default TrackRow;\n","/**\n * TrackDrawer — the unified per-track drawer body.\n *\n * ONE drawer with a flat contextual tab strip. Which tabs appear is computed\n * from which callbacks the host panel provides:\n * - FX (onFxToggle) — the 6-category FX toggle bar\n * - Pick (onSelect) — instrument-plugin picker (+ native editor stage)\n * - History (onRestoreSound) — sounds this track has had (restore / favorite)\n * - Import (onImportSound) — copy a sound from a matching track in another scene\n *\n * The active tab is CONTROLLED by the host (activeTab / onTabChange) so the\n * track row's FX button and ▾ button can open the SAME drawer to a chosen tab.\n * When only one tab is enabled (e.g. loops = FX only) the strip is hidden and\n * that single view renders directly.\n *\n * (Was `InstrumentDrawer` — renamed once it grew an FX tab + Import tab. A\n * `TrackDrawer as InstrumentDrawer` alias is exported from the barrel for\n * backwards compatibility.)\n */\n\nimport React, { useState, useMemo } from 'react';\nimport type { InstrumentDescriptor, SoundHistoryEntry, PluginMidiNote } from '../types/plugin-sdk.types';\nimport type { FxCategory, TrackFxDetailState } from '../types/fx-toggle.types';\nimport { FxToggleBar } from './FxToggleBar';\nimport { PianoRollEditor } from './PianoRollEditor';\n\n// ============================================================================\n// Tabs\n// ============================================================================\n\n/** The contextual tabs a track drawer can show, in display order. */\nexport type DrawerTab = 'fx' | 'pick' | 'history' | 'import' | 'edit';\n\nconst TAB_LABELS: Record<DrawerTab, string> = {\n fx: 'FX',\n pick: 'Pick',\n history: 'History',\n import: 'Import',\n edit: 'Edit',\n};\n\n// ============================================================================\n// Props\n// ============================================================================\n\nexport interface TrackDrawerProps {\n /** Which tab is active (controlled by the host TrackRow). */\n activeTab: DrawerTab;\n /** Switch tabs (strip clicks). */\n onTabChange?: (tab: DrawerTab) => void;\n\n // --- FX tab (enabled when onFxToggle is provided) ---\n trackId: string;\n fxState: TrackFxDetailState;\n onFxToggle?: (category: FxCategory, enabled: boolean) => void;\n onFxPresetChange?: (category: FxCategory, presetIndex: number) => void;\n onFxDryWetChange?: (category: FxCategory, value: number) => void;\n /** Disable FX controls (e.g. while the track is generating). */\n fxDisabled?: boolean;\n\n // --- Pick tab (enabled when onSelect is provided) ---\n /** Available instrument plugins from engine scan. */\n instruments?: InstrumentDescriptor[];\n /** Currently loaded instrument plugin ID (null = default Surge XT). */\n currentPluginId?: string | null;\n /** Whether the instrument scan is still in progress. */\n isLoading?: boolean;\n /** Called when user selects an instrument (presence enables the Pick tab). */\n onSelect?: (pluginId: string) => void;\n /** Re-scan plugins. */\n onRefresh?: () => void;\n /** Pick-tab sub-view: show the native plugin editor instead of the grid. */\n editorStage?: boolean;\n /** Called when user clicks \"Open Plugin Editor\". */\n onShowEditor?: () => void;\n /** Called when user goes back from the editor to the instrument grid. */\n onBackToInstruments?: () => void;\n /** Name of the selected instrument (shown in the editor header). */\n selectedInstrumentName?: string | null;\n\n // --- History tab (enabled when onRestoreSound is provided) ---\n soundHistory?: readonly SoundHistoryEntry[];\n soundHistoryCursor?: number;\n /** Restore a sound by index; presence enables the History tab. */\n onRestoreSound?: (index: number) => void;\n /** Toggle the favorite (⭐) flag on a history entry; omit to hide the star. */\n onToggleFavorite?: (index: number) => void;\n\n // --- Import tab (enabled when onImportSound is provided) ---\n /** Open the sound-import picker; presence enables the Import tab. */\n onImportSound?: () => void;\n /** Button label, e.g. \"Import Sample\" (drums/instruments) or \"Import Preset\" (synths). */\n importSoundLabel?: string;\n\n // --- Edit tab (enabled when onNotesChange is provided) ---\n /** Current MIDI notes for the piano-roll editor. */\n editNotes?: readonly PluginMidiNote[];\n /** Persist edited notes; PRESENCE of this callback enables the Edit tab. */\n onNotesChange?: (notes: PluginMidiNote[]) => void;\n /** Scene length in bars (piano-roll grid width). Default 4. */\n editBars?: number;\n /** Scene BPM (piano-roll audition timing). Default 120. */\n editBpm?: number;\n /** Snap step in quarter notes for the piano roll (default 0.25). */\n editSnap?: number;\n /** Optional single-note preview when the user adds a note. */\n onAuditionNote?: (pitch: number, velocity: number, durationMs: number) => void;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function TrackDrawer({\n activeTab,\n onTabChange,\n trackId,\n fxState,\n onFxToggle,\n onFxPresetChange,\n onFxDryWetChange,\n fxDisabled = false,\n instruments = [],\n currentPluginId = null,\n isLoading = false,\n onSelect,\n onRefresh,\n editorStage = false,\n onShowEditor,\n onBackToInstruments,\n selectedInstrumentName,\n soundHistory,\n soundHistoryCursor = -1,\n onRestoreSound,\n onToggleFavorite,\n onImportSound,\n importSoundLabel,\n editNotes,\n onNotesChange,\n editBars,\n editBpm,\n editSnap,\n onAuditionNote,\n}: TrackDrawerProps): React.ReactElement {\n // --- Hooks (MUST stay above every early return) ---\n const [search, setSearch] = useState('');\n\n const fxEnabled = !!onFxToggle;\n const pickEnabled = !!onSelect;\n const historyEnabled = !!onRestoreSound;\n const importEnabled = !!onImportSound;\n const editEnabled = !!onNotesChange;\n\n const enabledTabs = useMemo((): DrawerTab[] => {\n const tabs: DrawerTab[] = [];\n if (fxEnabled) tabs.push('fx');\n if (pickEnabled) tabs.push('pick');\n if (historyEnabled) tabs.push('history');\n if (importEnabled) tabs.push('import');\n if (editEnabled) tabs.push('edit');\n return tabs;\n }, [fxEnabled, pickEnabled, historyEnabled, importEnabled, editEnabled]);\n\n /** Sentinel pluginId for the default Surge XT entry */\n const SURGE_XT_DEFAULT_ID = 'Surge XT';\n\n // Filter instruments by search query, with selected instrument always first.\n // Computed unconditionally so the hook order is stable across tab switches.\n const filtered = useMemo((): InstrumentDescriptor[] => {\n let all = instruments.filter((i: InstrumentDescriptor) => i.name !== 'Surge XT');\n if (search.trim()) {\n const q = search.toLowerCase();\n all = all.filter(\n (i: InstrumentDescriptor) =>\n i.name.toLowerCase().includes(q) || i.manufacturer.toLowerCase().includes(q),\n );\n }\n if (currentPluginId) {\n const selectedIdx = all.findIndex((i: InstrumentDescriptor) => i.pluginId === currentPluginId);\n if (selectedIdx > 0) {\n const [selected] = all.splice(selectedIdx, 1);\n all.unshift(selected);\n }\n }\n return all;\n }, [instruments, search, currentPluginId]);\n\n // --- Derived (non-hook) values ---\n const history = soundHistory ?? [];\n const effectiveTab: DrawerTab = enabledTabs.includes(activeTab)\n ? activeTab\n : enabledTabs[0] ?? 'fx';\n\n const tabClass = (active: boolean): string =>\n `px-2 py-0.5 text-xs rounded-sm transition-colors ${\n active ? 'bg-sas-accent/20 text-sas-accent font-medium' : 'text-sas-muted hover:text-sas-accent'\n }`;\n\n // The tab strip replaces the old \"Sound\" title. Hidden when only one tab is\n // enabled (e.g. loops = FX only) — that single view renders directly.\n const strip =\n enabledTabs.length > 1 ? (\n <div\n className=\"flex items-center gap-1 border-b border-sas-border pb-1\"\n data-testid=\"sdk-drawer-tabs\"\n >\n {enabledTabs.map((tab: DrawerTab) => (\n <button\n key={tab}\n type=\"button\"\n data-testid={`sdk-drawer-tab-${tab}`}\n onClick={() => onTabChange?.(tab)}\n className={tabClass(effectiveTab === tab)}\n >\n {tab === 'history' && history.length > 0\n ? `History (${history.length})`\n : TAB_LABELS[tab]}\n </button>\n ))}\n </div>\n ) : null;\n\n // Subtle current-sound hint (the \"Sound\" title was removed in favour of tabs).\n const currentSound =\n soundHistoryCursor >= 0 && soundHistoryCursor < history.length\n ? history[soundHistoryCursor].label\n : null;\n\n const header =\n strip || currentSound ? (\n <div className=\"flex flex-col gap-1\" data-testid=\"sdk-drawer-header\">\n {strip}\n {currentSound && (\n <span\n className=\"text-[10px] text-sas-muted/60 truncate px-0.5\"\n title={currentSound}\n >\n {currentSound}\n </span>\n )}\n </div>\n ) : null;\n\n // ---- Edit tab (piano-roll MIDI editor) ----\n if (effectiveTab === 'edit') {\n return (\n <div className=\"flex flex-col gap-2\" data-testid=\"sdk-drawer-edit\">\n {header}\n <PianoRollEditor\n notes={editNotes ?? []}\n onChange={onNotesChange ?? ((): void => {})}\n bars={editBars ?? 4}\n bpm={editBpm ?? 120}\n snap={editSnap}\n onAuditionNote={onAuditionNote}\n />\n </div>\n );\n }\n\n // ---- FX tab ----\n if (effectiveTab === 'fx') {\n return (\n <div className=\"flex flex-col gap-2\" data-testid=\"sdk-drawer-fx\">\n {header}\n <FxToggleBar\n trackId={trackId}\n fxState={fxState}\n onToggle={(_t: string, category: FxCategory, enabled: boolean) =>\n onFxToggle?.(category, enabled)\n }\n onPresetChange={(_t: string, category: FxCategory, presetIndex: number) =>\n onFxPresetChange?.(category, presetIndex)\n }\n onDryWetChange={(_t: string, category: FxCategory, value: number) =>\n onFxDryWetChange?.(category, value)\n }\n disabled={fxDisabled}\n />\n </div>\n );\n }\n\n // ---- Import tab ----\n if (effectiveTab === 'import') {\n const soundNoun = /preset/i.test(importSoundLabel ?? '')\n ? 'preset'\n : /sample/i.test(importSoundLabel ?? '')\n ? 'sample'\n : 'sound';\n return (\n <div className=\"flex flex-col gap-2\" data-testid=\"sdk-drawer-import\">\n {header}\n <p className=\"text-[11px] text-sas-muted/70 leading-snug\">\n Copy the sound from a matching track in another scene — your MIDI stays, only the{' '}\n {soundNoun} changes.\n </p>\n <button\n type=\"button\"\n data-testid=\"sdk-drawer-import-sound\"\n onClick={onImportSound}\n className=\"w-full px-2 py-1.5 text-[11px] rounded-sm border border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent transition-colors\"\n title=\"Copy a sound from a track in another scene (ignores contract)\"\n >\n ⇪ {importSoundLabel ?? 'Import Sound'}\n </button>\n </div>\n );\n }\n\n // ---- History tab ----\n if (effectiveTab === 'history') {\n const order = history.map((_, i) => i).reverse(); // newest first\n return (\n <div className=\"flex flex-col gap-2\">\n {header}\n {history.length === 0 ? (\n <div\n className=\"text-xs text-sas-muted/60 text-center py-3\"\n data-testid=\"sdk-history-empty\"\n >\n No sounds yet — shuffle to build history.\n </div>\n ) : (\n <ul\n className=\"flex flex-col gap-1 max-h-[160px] overflow-y-auto\"\n data-testid=\"sdk-history-list\"\n >\n {order.map((i) => {\n const entry = history[i];\n const isCurrent = i === soundHistoryCursor;\n return (\n <li key={i} className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n data-testid=\"sdk-history-entry\"\n disabled={isCurrent}\n onClick={() => onRestoreSound?.(i)}\n className={`flex-1 min-w-0 flex items-center justify-between px-2 py-1.5 rounded-sm border text-left text-xs transition-colors ${\n isCurrent\n ? 'border-sas-accent bg-sas-accent/20 text-sas-accent cursor-default'\n : 'border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={isCurrent ? 'Current sound' : `Restore: ${entry.label}`}\n >\n <span className=\"truncate\">{entry.label}</span>\n <span className=\"text-[10px] text-sas-muted/60 flex-shrink-0 ml-2\">\n {isCurrent ? '● current' : 'restore'}\n </span>\n </button>\n {onToggleFavorite && (\n <button\n type=\"button\"\n data-testid=\"sdk-history-favorite\"\n onClick={() => onToggleFavorite(i)}\n className={`flex-shrink-0 px-1 py-0.5 text-sm leading-none transition-colors ${\n entry.favorite\n ? 'text-yellow-400'\n : 'text-sas-muted/40 hover:text-yellow-400'\n }`}\n title={entry.favorite ? 'Unfavorite' : 'Favorite (keeps it from being evicted)'}\n >\n {entry.favorite ? '★' : '☆'}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n }\n\n // ---- Pick tab: native editor stage ----\n if (effectiveTab === 'pick' && editorStage) {\n return (\n <div className=\"flex flex-col gap-2\">\n {header}\n <div className=\"flex items-center gap-2\">\n <button\n onClick={() => onBackToInstruments?.()}\n className=\"px-2 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors\"\n >\n ← Back\n </button>\n <span className=\"text-xs text-sas-muted font-medium truncate flex-1\">\n {selectedInstrumentName ?? 'Plugin'}\n </span>\n </div>\n <button\n onClick={() => onShowEditor?.()}\n className=\"w-full py-2 text-xs font-medium rounded-sm border border-sas-accent bg-sas-accent/20 text-sas-accent hover:bg-sas-accent/40 transition-colors\"\n >\n Open Plugin Editor\n </button>\n </div>\n );\n }\n\n // ---- Pick tab: instrument grid (default) ----\n const isDefaultSelected = currentPluginId === null;\n const isSelected = (pluginId: string): boolean => pluginId === currentPluginId;\n\n return (\n <div className=\"flex flex-col gap-2\">\n {header}\n {/* Search + Refresh row */}\n <div className=\"flex items-center gap-2\">\n <input\n type=\"text\"\n value={search}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.target.value)}\n placeholder=\"Search instruments...\"\n className=\"sas-input flex-1 px-2 py-1 text-xs\"\n />\n <button\n onClick={() => onRefresh?.()}\n disabled={isLoading}\n className=\"px-2 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors disabled:opacity-50\"\n title=\"Re-scan plugins\"\n >\n {isLoading ? '...' : 'Refresh'}\n </button>\n </div>\n\n {/* Instrument grid */}\n {isLoading && instruments.length === 0 ? (\n <div className=\"text-xs text-sas-muted/60 text-center py-3\">Scanning plugins...</div>\n ) : (\n <div className=\"grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto\">\n {/* Permanent \"Surge XT (Default)\" entry — always available */}\n <button\n key=\"__surge-xt-default__\"\n onClick={() => onSelect?.(SURGE_XT_DEFAULT_ID)}\n className={`flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors ${\n isDefaultSelected\n ? 'border-sas-accent bg-sas-accent/20 text-sas-accent'\n : 'border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title=\"Surge XT — Default instrument\"\n >\n <span className=\"text-xs font-medium truncate w-full\">\n {isDefaultSelected && '✓ '}Surge XT\n </span>\n <span className=\"text-[9px] text-sas-muted/50 truncate w-full\">Default</span>\n </button>\n {/* Scanned instruments */}\n {filtered.map((inst: InstrumentDescriptor) => {\n const selected = isSelected(inst.pluginId);\n return (\n <button\n key={inst.pluginId}\n onClick={() => onSelect?.(inst.pluginId)}\n className={`flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors ${\n selected\n ? 'border-sas-accent bg-sas-accent/20 text-sas-accent'\n : inst.missing\n ? 'border-amber-500/50 bg-amber-500/10 text-amber-400 hover:border-amber-500'\n : 'border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={`${inst.name} by ${inst.manufacturer} (${inst.type.toUpperCase()})${inst.missing ? ' — MISSING' : ''}`}\n >\n <span className=\"text-xs font-medium truncate w-full\">\n {selected && '✓ '}\n {inst.name}\n </span>\n <span className=\"text-[9px] text-sas-muted/50 truncate w-full\">\n {inst.manufacturer || inst.type.toUpperCase()}\n </span>\n </button>\n );\n })}\n {filtered.length === 0 && (\n <div className=\"col-span-2 text-xs text-sas-muted/60 text-center py-2\">\n {search.trim() ? 'No matches' : 'No other plugins found'}\n </div>\n )}\n </div>\n )}\n </div>\n );\n}\n\n/** Backwards-compatible alias — the drawer was named `InstrumentDrawer` before it grew FX/Import tabs. */\nexport { TrackDrawer as InstrumentDrawer };\n\nexport default TrackDrawer;\n","/**\n * FX Preset Definitions\n *\n * 5 presets per FX category (30 total).\n *\n * Parameter names must match the Tracktion Engine's AutomatableParameter names\n * for each built-in plugin exactly (case-sensitive).\n *\n * Chorus & Phaser have ZERO automatable parameters — all values are set via\n * XML state (CachedValues on the plugin ValueTree).\n *\n * Lives in shared/ so both main process (services) and renderer (UI) can import.\n */\n\nimport type { FxCategory, FxPresetConfig } from '../types/fx-toggle.types';\n\n// ============================================================================\n// EQ (4-Band Equaliser)\n// ============================================================================\n\nconst EQ_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'The Smiley',\n shortLabel: 'SM',\n params: {\n 'Low-shelf freq': 80, 'Low-shelf gain': 4, 'Low-shelf Q': 0.5,\n 'Mid freq 1': 500, 'Mid gain 1': -3, 'Mid Q 1': 0.7,\n 'Mid freq 2': 2000, 'Mid gain 2': -2, 'Mid Q 2': 0.7,\n 'High-shelf freq': 12000, 'High-shelf gain': 4, 'High-shelf Q': 0.5,\n },\n },\n {\n name: 'Telephone',\n shortLabel: 'TP',\n params: {\n 'Low-shelf freq': 400, 'Low-shelf gain': -20, 'Low-shelf Q': 1.0,\n 'Mid freq 1': 1000, 'Mid gain 1': 5, 'Mid Q 1': 2.0,\n 'Mid freq 2': 3000, 'Mid gain 2': -5, 'Mid Q 2': 1.0,\n 'High-shelf freq': 5000, 'High-shelf gain': -20, 'High-shelf Q': 1.0,\n },\n },\n {\n name: 'Warmth',\n shortLabel: 'WM',\n params: {\n 'Low-shelf freq': 120, 'Low-shelf gain': 3, 'Low-shelf Q': 0.7,\n 'Mid freq 1': 400, 'Mid gain 1': 2, 'Mid Q 1': 1.0,\n 'Mid freq 2': 4000, 'Mid gain 2': 0, 'Mid Q 2': 0.5,\n 'High-shelf freq': 10000, 'High-shelf gain': -4, 'High-shelf Q': 0.5,\n },\n },\n {\n name: 'Vocal Air',\n shortLabel: 'VA',\n params: {\n 'Low-shelf freq': 100, 'Low-shelf gain': -6, 'Low-shelf Q': 0.7,\n 'Mid freq 1': 300, 'Mid gain 1': -2, 'Mid Q 1': 1.0,\n 'Mid freq 2': 1500, 'Mid gain 2': 0, 'Mid Q 2': 0.5,\n 'High-shelf freq': 14000, 'High-shelf gain': 6, 'High-shelf Q': 0.4,\n },\n },\n {\n name: 'De-Box',\n shortLabel: 'DB',\n params: {\n 'Low-shelf freq': 60, 'Low-shelf gain': 0, 'Low-shelf Q': 0.5,\n 'Mid freq 1': 350, 'Mid gain 1': -5, 'Mid Q 1': 2.0,\n 'Mid freq 2': 800, 'Mid gain 2': -3, 'Mid Q 2': 2.0,\n 'High-shelf freq': 10000, 'High-shelf gain': 0, 'High-shelf Q': 0.5,\n },\n },\n ],\n mixParamName: null,\n mixInterpolation: 'gain-scale',\n};\n\n// ============================================================================\n// Compressor\n// ============================================================================\n\nconst COMPRESSOR_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Vocal Leveler',\n shortLabel: 'VL',\n params: { 'Threshold': 0.251, 'Ratio': 0.5, 'Attack': 20.0, 'Release': 200.0, 'Output': 2.0 },\n },\n {\n name: 'Drum Smash',\n shortLabel: 'DS',\n params: { 'Threshold': 0.100, 'Ratio': 0.1, 'Attack': 0.5, 'Release': 100.0, 'Output': 8.0 },\n },\n {\n name: 'Bus Glue',\n shortLabel: 'BG',\n params: { 'Threshold': 0.316, 'Ratio': 0.666, 'Attack': 80.0, 'Release': 150.0, 'Output': 1.0 },\n },\n {\n name: 'Bass Anchor',\n shortLabel: 'BA',\n params: { 'Threshold': 0.177, 'Ratio': 0.25, 'Attack': 10.0, 'Release': 250.0, 'Output': 4.0 },\n },\n {\n name: 'Safety Net',\n shortLabel: 'SN',\n params: { 'Threshold': 0.891, 'Ratio': 0.0, 'Attack': 0.3, 'Release': 50.0, 'Output': 0.0 },\n },\n ],\n mixParamName: null,\n mixInterpolation: 'ratio-scale',\n};\n\n// ============================================================================\n// Chorus\n// ============================================================================\n\nconst CHORUS_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Dimension',\n shortLabel: 'DM',\n params: {},\n xmlStateParams: { depthMs: 1.5, speedHz: 0.5, width: 1.0, mixProportion: 0.5 },\n },\n {\n name: '80s Crystal',\n shortLabel: '80',\n params: {},\n xmlStateParams: { depthMs: 4.0, speedHz: 2.5, width: 0.8, mixProportion: 0.4 },\n },\n {\n name: 'Sea Sick',\n shortLabel: 'SS',\n params: {},\n xmlStateParams: { depthMs: 7.0, speedHz: 0.8, width: 0.3, mixProportion: 1.0 },\n },\n {\n name: 'Pseudo-Leslie',\n shortLabel: 'PL',\n params: {},\n xmlStateParams: { depthMs: 2.0, speedHz: 6.0, width: 0.9, mixProportion: 0.7 },\n },\n {\n name: 'Thickener',\n shortLabel: 'TK',\n params: {},\n xmlStateParams: { depthMs: 1.0, speedHz: 0.2, width: 1.0, mixProportion: 0.3 },\n },\n ],\n mixParamName: null,\n mixXmlAttr: 'mixProportion',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Phaser\n// ============================================================================\n\nconst PHASER_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Slow Burn',\n shortLabel: 'SB',\n params: {},\n xmlStateParams: { depth: 6.0, rate: 0.1, feedback: 0.3 },\n },\n {\n name: 'Funky Quack',\n shortLabel: 'FQ',\n params: {},\n xmlStateParams: { depth: 3.0, rate: 2.0, feedback: 0.8 },\n },\n {\n name: 'Jet Plane',\n shortLabel: 'JP',\n params: {},\n xmlStateParams: { depth: 8.0, rate: 0.2, feedback: 0.9 },\n },\n {\n name: 'Underwater',\n shortLabel: 'UW',\n params: {},\n xmlStateParams: { depth: 1.5, rate: 4.0, feedback: 0.1 },\n },\n {\n name: 'Static Notch',\n shortLabel: 'ST',\n params: {},\n xmlStateParams: { depth: 2.0, rate: 0.05, feedback: 0.6 },\n },\n ],\n mixParamName: null,\n mixXmlAttr: 'depth',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Delay\n// ============================================================================\n\nconst DELAY_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Vocal Slap',\n shortLabel: 'VS',\n fixedLengthMs: 110,\n params: { 'Feedback': -20.0, 'Mix proportion': 0.25 },\n },\n {\n name: 'Grand Canyon',\n shortLabel: 'GC',\n noteMultiplier: 1.0,\n params: { 'Feedback': -4.0, 'Mix proportion': 0.45 },\n },\n {\n name: 'Wide Doubler',\n shortLabel: 'WD',\n fixedLengthMs: 25,\n params: { 'Feedback': -30.0, 'Mix proportion': 0.5 },\n },\n {\n name: 'Dub Echo',\n shortLabel: 'DE',\n noteMultiplier: 0.6,\n params: { 'Feedback': -1.5, 'Mix proportion': 0.4 },\n },\n {\n name: 'Rhythmic Wash',\n shortLabel: 'RW',\n noteMultiplier: 0.75,\n params: { 'Feedback': -8.0, 'Mix proportion': 0.2 },\n },\n ],\n mixParamName: 'Mix proportion',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Reverb\n// ============================================================================\n\nconst REVERB_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Drum Room',\n shortLabel: 'DR',\n params: { 'Room Size': 0.2, 'Damping': 0.2, 'Wet Level': 0.15, 'Dry Level': 0.5, 'Width': 0.8 },\n },\n {\n name: 'Vocal Hall',\n shortLabel: 'VH',\n params: { 'Room Size': 0.8, 'Damping': 0.6, 'Wet Level': 0.25, 'Dry Level': 0.5, 'Width': 1.0 },\n },\n {\n name: 'Cathedral',\n shortLabel: 'CT',\n params: { 'Room Size': 1.0, 'Damping': 0.1, 'Wet Level': 0.333, 'Dry Level': 0.2, 'Width': 1.0 },\n },\n {\n name: 'Tile Bathroom',\n shortLabel: 'TB',\n params: { 'Room Size': 0.15, 'Damping': 0.0, 'Wet Level': 0.2, 'Dry Level': 0.5, 'Width': 0.5 },\n },\n {\n name: 'Vintage Plate',\n shortLabel: 'VP',\n params: { 'Room Size': 0.4, 'Damping': 1.0, 'Wet Level': 0.2, 'Dry Level': 0.5, 'Width': 1.0 },\n },\n ],\n mixParamName: 'Wet Level',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Export\n// ============================================================================\n\n/** All preset configs keyed by FX category */\nexport const FX_PRESET_CONFIGS: Record<FxCategory, FxPresetConfig> = {\n eq: EQ_PRESETS,\n compressor: COMPRESSOR_PRESETS,\n chorus: CHORUS_PRESETS,\n phaser: PHASER_PRESETS,\n delay: DELAY_PRESETS,\n reverb: REVERB_PRESETS,\n};\n","/**\n * FxToggleBar Component\n *\n * Per-track FX control panel with 6 rows (one per FX category).\n * Each row: [Category toggle] [Preset 1-5 buttons] [Dry/Wet slider]\n *\n * Signal chain order: EQ -> Compressor -> Chorus -> Phaser -> Delay -> Reverb\n */\n\nimport React from 'react';\nimport type { FxCategory, TrackFxDetailState, FxCategoryDetailState } from '../types/fx-toggle.types';\nimport { FX_CATEGORIES, FX_DISPLAY_LABELS } from '../types/fx-toggle.types';\nimport { FX_PRESET_CONFIGS } from '../constants/fx-presets';\n\n/** Per-category active colors */\nconst FX_COLORS: Record<FxCategory, string> = {\n eq: 'bg-blue-500',\n compressor: 'bg-orange-500',\n chorus: 'bg-teal-500',\n phaser: 'bg-purple-500',\n delay: 'bg-green-500',\n reverb: 'bg-cyan-500',\n};\n\nexport interface FxToggleBarProps {\n trackId: string;\n fxState: TrackFxDetailState;\n onToggle: (trackId: string, category: FxCategory, enabled: boolean) => void;\n onPresetChange: (trackId: string, category: FxCategory, presetIndex: number) => void;\n onDryWetChange: (trackId: string, category: FxCategory, value: number) => void;\n disabled?: boolean;\n}\n\nexport const FxToggleBar: React.FC<FxToggleBarProps> = ({\n trackId,\n fxState,\n onToggle,\n onPresetChange,\n onDryWetChange,\n disabled = false,\n}) => {\n return (\n <div className=\"flex flex-col gap-1\" data-testid=\"fx-toggle-bar\">\n {FX_CATEGORIES.map((category: FxCategory) => {\n const detail: FxCategoryDetailState = fxState[category];\n const isActive = detail.enabled;\n const label = FX_DISPLAY_LABELS[category];\n const activeColor = FX_COLORS[category];\n const config = FX_PRESET_CONFIGS[category];\n\n return (\n <div key={category} className=\"flex items-center gap-0.5\">\n {/* Category toggle button */}\n <button\n data-testid={`fx-toggle-${category}`}\n disabled={disabled}\n onClick={() => onToggle(trackId, category, !isActive)}\n className={`w-14 py-0.5 text-[10px] font-semibold rounded-sm transition-colors leading-none flex-shrink-0 text-center ${\n disabled\n ? 'bg-sas-panel text-sas-muted/30 cursor-not-allowed'\n : isActive\n ? `${activeColor} text-white`\n : 'bg-sas-panel-alt text-sas-muted/60 hover:bg-sas-border hover:text-sas-muted'\n }`}\n title={`${isActive ? 'Disable' : 'Enable'} ${category.toUpperCase()}`}\n >\n {label}\n </button>\n\n {/* Preset buttons 1-5 */}\n {config.presets.map((preset, idx: number) => (\n <button\n key={idx}\n data-testid={`fx-preset-${category}-${idx}`}\n disabled={disabled || !isActive}\n onClick={() => onPresetChange(trackId, category, idx)}\n className={`w-5 h-5 text-[9px] font-medium rounded-sm transition-colors leading-none flex-shrink-0 ${\n disabled || !isActive\n ? 'bg-sas-panel text-sas-muted/20 cursor-not-allowed'\n : detail.presetIndex === idx\n ? `${activeColor} text-white`\n : 'bg-sas-panel-alt text-sas-muted/50 hover:bg-sas-border hover:text-sas-muted'\n }`}\n title={preset.name}\n >\n {idx + 1}\n </button>\n ))}\n\n {/* Dry/Wet slider */}\n <input\n type=\"range\"\n data-testid={`fx-drywet-${category}`}\n min=\"0\"\n max=\"100\"\n value={Math.round(detail.dryWet * 100)}\n disabled={disabled || !isActive}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) =>\n onDryWetChange(trackId, category, Number(e.target.value) / 100)\n }\n className=\"flex-1 min-w-[30px] h-3 accent-sas-accent disabled:opacity-30 cursor-pointer disabled:cursor-not-allowed\"\n title={`Dry/Wet: ${Math.round(detail.dryWet * 100)}%`}\n />\n <span className=\"text-[8px] text-sas-muted/50 w-6 text-right flex-shrink-0\">\n {Math.round(detail.dryWet * 100)}%\n </span>\n </div>\n );\n })}\n </div>\n );\n};\n","/**\n * PianoRollEditor — a compact, DOM-based MIDI note editor for the track drawer.\n *\n * Controlled: `notes` in, `onChange(next)` out. Notes render as absolutely-\n * positioned divs over a beat/pitch grid (DOM, not canvas — so it themes with\n * sas-* tokens and is fully driveable by React Testing Library). Supports:\n * - add : click an empty grid cell\n * - delete : click an existing note (no drag)\n * - move : drag a note's body (snap-quantised)\n * - resize : drag a note's right-edge handle (snap-quantised, ≥ one step)\n * - octave : shift the whole clip ±12 (toolbar) — no velocity lane\n * / marquee yet.\n * On load the viewport auto-scrolls to vertically center the note cluster, so a\n * low melody isn't stranded off-screen at the bottom of the pitch range.\n *\n * Coordinate spaces:\n * pitch (0-127) ── row = hi - pitch ── top px = row * ROW_HEIGHT\n * beat (¼ notes) ─────────────────────── left px = beat * PX_PER_BEAT\n *\n * The pure helpers (`cellToPx` / `pxToCell` / `transposeNotes`) and layout\n * constants are exported so coordinate math can be unit-tested without a DOM.\n */\nimport React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport type { PluginMidiNote } from '../types/plugin-sdk.types';\n\n// ============================================================================\n// Layout constants (exported for tests)\n// ============================================================================\n\n/** Horizontal pixels per quarter-note beat. */\nexport const PX_PER_BEAT = 24;\n/** Vertical pixels per semitone row. */\nexport const ROW_HEIGHT = 12;\n/** Left keyboard-gutter width (px). */\nexport const GUTTER_W = 28;\n/** Pointer travel (px) before a press on a note becomes a drag instead of a click. */\nexport const DRAG_DEAD_ZONE = 4;\n/** Width (px) of the right-edge grab handle that resizes a note's length. */\nexport const RESIZE_HANDLE_PX = 6;\n/** Max height (px) of the vertical scroll viewport — drives load-time centering. */\nexport const SCROLL_MAX_H = 150;\n\nconst NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] as const;\nconst BLACK_KEYS = new Set([1, 3, 6, 8, 10]);\n\nconst SNAP_LABELS: Record<string, string> = {\n '2': '1/2',\n '1': '1/4',\n '0.5': '1/8',\n '0.25': '1/16',\n '0.125': '1/32',\n};\n\nfunction clamp(v: number, lo: number, hi: number): number {\n return Math.max(lo, Math.min(hi, v));\n}\n\nfunction snapLabel(s: number): string {\n return SNAP_LABELS[String(s)] ?? `${s}`;\n}\n\n// ============================================================================\n// Pure helpers (DOM-free, exported for unit tests)\n// ============================================================================\n\n/** MIDI pitch → scientific note name (60 = C4). */\nexport function pitchToName(pitch: number): string {\n const name = NOTE_NAMES[((pitch % 12) + 12) % 12];\n const octave = Math.floor(pitch / 12) - 1;\n return `${name}${octave}`;\n}\n\n/**\n * Cell (pitch, startBeat) → top-left pixel offset within the grid.\n * `hi` is the highest (top) visible pitch.\n */\nexport function cellToPx(\n pitch: number,\n startBeat: number,\n hi: number,\n): { left: number; top: number } {\n return { left: startBeat * PX_PER_BEAT, top: (hi - pitch) * ROW_HEIGHT };\n}\n\n/**\n * Grid-local pixel → snapped cell. `hi` is the highest visible pitch; the beat\n * snaps to the nearest `snap` step and clamps to `[0, totalBeats - snap]`;\n * pitch clamps to `[0, 127]`.\n */\nexport function pxToCell(\n localX: number,\n localY: number,\n hi: number,\n snap: number,\n bars: number,\n beatsPerBar: number,\n): { pitch: number; startBeat: number } {\n const totalBeats = bars * beatsPerBar;\n const pitch = clamp(hi - Math.floor(localY / ROW_HEIGHT), 0, 127);\n const rawBeat = localX / PX_PER_BEAT;\n const snapped = Math.round(rawBeat / snap) * snap;\n const startBeat = clamp(snapped, 0, Math.max(0, totalBeats - snap));\n return { pitch, startBeat };\n}\n\n/**\n * New `durationBeats` for a note whose right edge is dragged to grid-local pixel\n * `localX`. The end snaps to the nearest `snap` step, is clamped to at least one\n * step past `startBeat`, and never extends beyond the grid's right edge\n * (`bars * beatsPerBar`). `startBeat` and `pitch` are untouched.\n */\nexport function resizeNoteDuration(\n startBeat: number,\n localX: number,\n snap: number,\n bars: number,\n beatsPerBar: number,\n): number {\n const totalBeats = bars * beatsPerBar;\n const snappedEnd = Math.round(localX / PX_PER_BEAT / snap) * snap;\n const end = clamp(snappedEnd, startBeat + snap, totalBeats);\n return end - startBeat;\n}\n\n/**\n * `scrollTop` that vertically centers the bulk of the notes in a `viewportH`-px\n * window. Targets the MEDIAN pitch (robust to a stray high/low outlier — keeps\n * \"where the majority of notes are\" framed) and clamps to the valid scroll\n * range. `hi` is the top visible pitch; `rowCount` the total rows in the grid.\n * Returns 0 when there are no notes.\n */\nexport function centerScrollTop(\n pitches: readonly number[],\n hi: number,\n rowCount: number,\n viewportH: number,\n): number {\n if (pitches.length === 0) return 0;\n const sorted = [...pitches].sort((a, b) => a - b);\n const mid = Math.floor(sorted.length / 2);\n const median = sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;\n // Pixel center of the median row, then offset so it lands mid-viewport.\n const medianRowCenterPx = (hi - median) * ROW_HEIGHT + ROW_HEIGHT / 2;\n const maxScroll = Math.max(0, rowCount * ROW_HEIGHT - viewportH);\n return clamp(medianRowCenterPx - viewportH / 2, 0, maxScroll);\n}\n\n/** Transpose every note by `semitones`, clamping pitch to [0,127] (never drops a note). */\nexport function transposeNotes(\n notes: readonly PluginMidiNote[],\n semitones: number,\n): PluginMidiNote[] {\n return notes.map((n) => ({ ...n, pitch: clamp(n.pitch + semitones, 0, 127) }));\n}\n\n// ============================================================================\n// Props\n// ============================================================================\n\nexport interface PianoRollEditorProps {\n /** Controlled note list (quarter-note beats). The editor never mutates this. */\n notes: readonly PluginMidiNote[];\n /** Emitted on every edit (add / delete / move / transpose) with the full next array. */\n onChange: (next: PluginMidiNote[]) => void;\n /** Scene length in bars → grid width = bars * beatsPerBar * PX_PER_BEAT. */\n bars: number;\n /** BPM — used only for audition timing in v1. */\n bpm: number;\n /** Beats per bar (time-signature numerator). Default 4. */\n beatsPerBar?: number;\n /** Snap step in quarter notes (1 = ¼ note, 0.25 = 1/16). Default 0.25. */\n snap?: number;\n /** Snap steps the toolbar selector offers. Default [1, 0.5, 0.25]. */\n snapOptions?: number[];\n /** Notified when the user changes snap (the editor still tracks it internally). */\n onSnapChange?: (snap: number) => void;\n /** Lowest pitch always visible. Default C2 (36). */\n minPitch?: number;\n /** Highest pitch always visible. Default C6 (84). */\n maxPitch?: number;\n /** Expand the visible window to include notes outside [minPitch,maxPitch]. Default true. */\n autoFit?: boolean;\n /** Optional single-note preview, fired when a note is added. */\n onAuditionNote?: (pitch: number, velocity: number, durationMs: number) => void;\n /** Velocity for newly-added notes. Default 100. */\n defaultVelocity?: number;\n /** Disable all interaction (e.g. while the track is generating). Default false. */\n disabled?: boolean;\n /** Extra className for the outer container. */\n className?: string;\n /** Test id for the outer container. Default \"sdk-piano-roll\". */\n testId?: string;\n}\n\ninterface DragState {\n /**\n * `pending-*` is an undecided press (becomes the matching committed mode once\n * the pointer travels past {@link DRAG_DEAD_ZONE}, else resolves on pointer-up):\n * pending-note → drag (move) | no travel → delete\n * pending-resize → resize | no travel → delete\n * pending-add → (on up) add a note\n */\n mode: 'pending-note' | 'pending-resize' | 'pending-add' | 'drag' | 'resize';\n /** Index into `notes` for a note press; -1 for an empty-grid press. */\n index: number;\n startX: number;\n startY: number;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function PianoRollEditor({\n notes,\n onChange,\n bars,\n bpm,\n beatsPerBar = 4,\n snap = 0.25,\n snapOptions = [1, 0.5, 0.25],\n onSnapChange,\n minPitch = 36,\n maxPitch = 84,\n autoFit = true,\n onAuditionNote,\n defaultVelocity = 100,\n disabled = false,\n className,\n testId = 'sdk-piano-roll',\n}: PianoRollEditorProps): React.ReactElement {\n const [snapState, setSnapState] = useState(snap);\n const gridRef = useRef<HTMLDivElement | null>(null);\n const scrollRef = useRef<HTMLDivElement | null>(null);\n const dragRef = useRef<DragState | null>(null);\n // True once we've auto-centered the current note set; re-armed when the notes\n // clear or the user octave-shifts, so the view re-frames only on a fresh load.\n const didCenterRef = useRef(false);\n\n // Visible pitch window: the default [minPitch, maxPitch], expanded to include\n // any notes that fall outside (± 2 semitones of headroom). Stable + testable.\n const { lo, hi } = useMemo((): { lo: number; hi: number } => {\n if (autoFit && notes.length > 0) {\n const ps = notes.map((n) => n.pitch);\n return {\n lo: Math.max(0, Math.min(minPitch, Math.min(...ps) - 2)),\n hi: Math.min(127, Math.max(maxPitch, Math.max(...ps) + 2)),\n };\n }\n return { lo: minPitch, hi: maxPitch };\n }, [autoFit, notes, minPitch, maxPitch]);\n\n const rowCount = hi - lo + 1;\n const totalBeats = bars * beatsPerBar;\n const gridWidth = totalBeats * PX_PER_BEAT;\n const gridHeight = rowCount * ROW_HEIGHT;\n\n // Latest values for the stable pointer handlers — avoids stale closures and\n // handler re-binding (the documented render-loop hazard). Assigned during\n // render so the handlers always read current props/state.\n const stateRef = useRef({\n notes, onChange, snapState, hi, bars, beatsPerBar, defaultVelocity, bpm, onAuditionNote, disabled,\n });\n stateRef.current = {\n notes, onChange, snapState, hi, bars, beatsPerBar, defaultVelocity, bpm, onAuditionNote, disabled,\n };\n\n const localCoords = useCallback((clientX: number, clientY: number): { x: number; y: number } => {\n const rect = gridRef.current?.getBoundingClientRect();\n return { x: clientX - (rect?.left ?? 0), y: clientY - (rect?.top ?? 0) };\n }, []);\n\n const handlePointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>): void => {\n if (stateRef.current.disabled) return;\n const target = e.target as HTMLElement;\n const noteEl = target.closest('[data-testid=\"sdk-pr-note\"]') as HTMLElement | null;\n const idxAttr = noteEl?.getAttribute('data-index');\n // A press that lands on the note's right-edge handle resizes; anywhere else\n // on the note moves/deletes; empty grid adds.\n const onResizeHandle = idxAttr != null && target.closest('[data-resize-handle]') != null;\n dragRef.current = {\n mode: idxAttr == null ? 'pending-add' : onResizeHandle ? 'pending-resize' : 'pending-note',\n index: idxAttr != null ? Number(idxAttr) : -1,\n startX: e.clientX,\n startY: e.clientY,\n };\n try {\n (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);\n } catch {\n /* jsdom / unsupported — drag still works via grid-level handlers */\n }\n }, []);\n\n const handlePointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>): void => {\n const drag = dragRef.current;\n if (!drag) return;\n const dist = Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY);\n if (dist > DRAG_DEAD_ZONE) {\n if (drag.mode === 'pending-note') drag.mode = 'drag';\n else if (drag.mode === 'pending-resize') drag.mode = 'resize';\n }\n const s = stateRef.current;\n const { x, y } = localCoords(e.clientX, e.clientY);\n\n if (drag.mode === 'resize') {\n const note = s.notes[drag.index];\n if (!note) return;\n const durationBeats = resizeNoteDuration(note.startBeat, x, s.snapState, s.bars, s.beatsPerBar);\n if (durationBeats === note.durationBeats) return;\n const next = s.notes.map((n, i) => (i === drag.index ? { ...n, durationBeats } : n));\n s.onChange(next);\n return;\n }\n\n if (drag.mode !== 'drag') return;\n const { pitch, startBeat } = pxToCell(x, y, s.hi, s.snapState, s.bars, s.beatsPerBar);\n const next = s.notes.map((n, i) => (i === drag.index ? { ...n, pitch, startBeat } : n));\n s.onChange(next);\n }, [localCoords]);\n\n const handlePointerUp = useCallback((e: React.PointerEvent<HTMLDivElement>): void => {\n const drag = dragRef.current;\n dragRef.current = null;\n if (!drag) return;\n const s = stateRef.current;\n if (s.disabled) return;\n\n if (drag.mode === 'pending-note' || drag.mode === 'pending-resize') {\n // Pressed a note (body or resize handle) without dragging past the dead\n // zone → treat as a plain click → delete it.\n s.onChange(s.notes.filter((_, i) => i !== drag.index));\n return;\n }\n if (drag.mode === 'pending-add') {\n const { x, y } = localCoords(e.clientX, e.clientY);\n const { pitch, startBeat } = pxToCell(x, y, s.hi, s.snapState, s.bars, s.beatsPerBar);\n const note: PluginMidiNote = {\n pitch,\n startBeat,\n durationBeats: s.snapState,\n velocity: s.defaultVelocity,\n channel: 0,\n };\n s.onChange([...s.notes, note]);\n s.onAuditionNote?.(pitch, s.defaultVelocity, Math.max(1, s.snapState * (60 / s.bpm) * 1000));\n }\n // mode 'drag' / 'resize' already emitted their final state during pointermove.\n }, [localCoords]);\n\n const handlePointerCancel = useCallback((): void => {\n dragRef.current = null;\n }, []);\n\n const handleOctave = useCallback((delta: number): void => {\n const s = stateRef.current;\n if (s.disabled) return;\n // The whole clip jumps an octave — re-frame the view onto its new position.\n didCenterRef.current = false;\n s.onChange(transposeNotes(s.notes, delta));\n }, []);\n\n const handleSnapChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>): void => {\n const v = Number(e.target.value);\n setSnapState(v);\n onSnapChange?.(v);\n }, [onSnapChange]);\n\n // Auto-frame the notes on load: the autoFit window already contains every note\n // vertically, but the scroll viewport starts pinned to the top — so a melody\n // sitting low needs a manual scroll to find. Center the note cluster once per\n // load (re-armed on clear / octave-shift), never mid-edit or mid-drag.\n useLayoutEffect(() => {\n const el = scrollRef.current;\n if (!el) return;\n if (notes.length === 0) {\n didCenterRef.current = false;\n return;\n }\n if (didCenterRef.current || dragRef.current) return;\n didCenterRef.current = true;\n const viewportH = el.clientHeight || SCROLL_MAX_H;\n el.scrollTop = centerScrollTop(\n notes.map((n) => n.pitch),\n hi,\n rowCount,\n viewportH,\n );\n }, [notes, hi, rowCount]);\n\n // Pitch rows for the keyboard gutter, top (hi) first.\n const rows = useMemo((): number[] => {\n const out: number[] = [];\n for (let p = hi; p >= lo; p--) out.push(p);\n return out;\n }, [hi, lo]);\n\n // Beat columns + bar columns + row lines, drawn purely in CSS so the only\n // hit-testable DOM in the grid is the notes themselves.\n const gridBg = useMemo((): string => {\n const beatPx = PX_PER_BEAT;\n const barPx = PX_PER_BEAT * beatsPerBar;\n return [\n `repeating-linear-gradient(to right, transparent 0 ${beatPx - 1}px, rgba(255,255,255,0.06) ${beatPx - 1}px ${beatPx}px)`,\n `repeating-linear-gradient(to right, transparent 0 ${barPx - 1}px, rgba(255,255,255,0.16) ${barPx - 1}px ${barPx}px)`,\n `repeating-linear-gradient(to bottom, transparent 0 ${ROW_HEIGHT - 1}px, rgba(255,255,255,0.04) ${ROW_HEIGHT - 1}px ${ROW_HEIGHT}px)`,\n ].join(', ');\n }, [beatsPerBar]);\n\n const octaveDisabled = disabled || notes.length === 0;\n\n return (\n <div className={`flex flex-col gap-1 ${className ?? ''}`} data-testid={testId}>\n {/* Toolbar */}\n <div className=\"flex items-center gap-1\" data-testid=\"sdk-pr-toolbar\">\n <button\n type=\"button\"\n data-testid=\"sdk-pr-octave-down\"\n disabled={octaveDisabled}\n onClick={() => handleOctave(-12)}\n className=\"px-1.5 py-0.5 text-[10px] rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors disabled:opacity-40\"\n title=\"Octave down (−12 semitones)\"\n >\n Oct −\n </button>\n <button\n type=\"button\"\n data-testid=\"sdk-pr-octave-up\"\n disabled={octaveDisabled}\n onClick={() => handleOctave(12)}\n className=\"px-1.5 py-0.5 text-[10px] rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors disabled:opacity-40\"\n title=\"Octave up (+12 semitones)\"\n >\n Oct +\n </button>\n <label className=\"flex items-center gap-1 text-[10px] text-sas-muted/70 ml-1\">\n Snap\n <select\n data-testid=\"sdk-pr-snap\"\n value={snapState}\n disabled={disabled}\n onChange={handleSnapChange}\n className=\"sas-input px-1 py-0.5 text-[10px]\"\n >\n {snapOptions.map((s) => (\n <option key={s} value={s}>\n {snapLabel(s)}\n </option>\n ))}\n </select>\n </label>\n <span className=\"text-[10px] text-sas-muted/60 ml-auto\" data-testid=\"sdk-pr-note-count\">\n {notes.length} {notes.length === 1 ? 'note' : 'notes'}\n </span>\n </div>\n\n {/* Scroll region: keyboard gutter + note grid */}\n <div\n ref={scrollRef}\n className=\"overflow-auto border border-sas-border rounded-sm bg-sas-bg\"\n style={{ maxHeight: SCROLL_MAX_H }}\n data-testid=\"sdk-pr-scroll\"\n >\n <div className=\"flex\" style={{ width: GUTTER_W + gridWidth }}>\n {/* Keyboard gutter — pinned left during horizontal scroll */}\n <div\n data-testid=\"sdk-pr-gutter\"\n className=\"sticky left-0 z-10 flex-shrink-0 bg-sas-panel-alt\"\n style={{ width: GUTTER_W }}\n >\n {rows.map((p) => (\n <div\n key={p}\n data-testid=\"sdk-pr-key\"\n data-pitch={p}\n className={`flex items-center justify-end pr-1 text-[8px] leading-none border-b border-sas-border/30 ${\n BLACK_KEYS.has(((p % 12) + 12) % 12)\n ? 'bg-sas-bg text-sas-muted/40'\n : 'text-sas-muted/70'\n }`}\n style={{ height: ROW_HEIGHT }}\n >\n {p % 12 === 0 ? pitchToName(p) : ''}\n </div>\n ))}\n </div>\n\n {/* Note grid */}\n <div\n ref={gridRef}\n data-testid=\"sdk-pr-grid\"\n className=\"relative flex-shrink-0\"\n style={{\n width: gridWidth,\n height: gridHeight,\n backgroundImage: gridBg,\n cursor: disabled ? 'not-allowed' : 'crosshair',\n touchAction: 'none',\n }}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUp}\n onPointerCancel={handlePointerCancel}\n >\n {notes.map((n, i) => {\n const { left, top } = cellToPx(n.pitch, n.startBeat, hi);\n const width = Math.max(3, n.durationBeats * PX_PER_BEAT);\n // Handle never exceeds half the note, so even a 1-step note keeps a\n // left \"body\" zone for moving.\n const handleW = Math.min(RESIZE_HANDLE_PX, width / 2);\n return (\n <div\n key={i}\n data-testid=\"sdk-pr-note\"\n data-index={i}\n data-pitch={n.pitch}\n data-start-beat={n.startBeat}\n data-duration-beats={n.durationBeats}\n className=\"absolute rounded-[2px] bg-sas-accent/80 border border-sas-accent hover:bg-sas-accent\"\n style={{ left, top, width, height: ROW_HEIGHT }}\n title={`${pitchToName(n.pitch)} · beat ${n.startBeat} · ${n.durationBeats}♪ · vel ${n.velocity}`}\n >\n {!disabled && (\n <div\n data-resize-handle=\"\"\n data-testid=\"sdk-pr-note-resize\"\n className=\"absolute top-0 right-0 h-full rounded-r-[2px] hover:bg-sas-bg/40\"\n style={{ width: handleW, cursor: 'ew-resize' }}\n />\n )}\n </div>\n );\n })}\n {notes.length === 0 && (\n <div\n data-testid=\"sdk-pr-empty\"\n className=\"absolute inset-0 flex items-center justify-center text-[10px] text-sas-muted/50 pointer-events-none\"\n >\n No notes — click to add\n </div>\n )}\n </div>\n </div>\n </div>\n </div>\n );\n}\n\nexport default PianoRollEditor;\n","/**\n * ConfirmDialog — styled in-app confirmation modal (SDK component).\n *\n * A small, reusable \"are you sure?\" dialog matching the app's dark theme\n * (mirrors ImportTrackModal chrome: sas-panel / sas-border / shadow-xl). It\n * guards destructive actions; the first consumer is track deletion, which was\n * one stray click away from losing a track's MIDI + sound.\n *\n * Controlled component — the caller owns `open` and the confirm/cancel\n * handlers. Escape and a backdrop click both cancel, and the Cancel button is\n * auto-focused on open so a reflexive Enter dismisses rather than deletes.\n *\n * @since SDK 2.17.0\n */\n\nimport React, { useRef } from 'react';\nimport { Modal } from './Modal';\n\nexport interface ConfirmDialogProps {\n /** Controls visibility (the caller owns open/closed). */\n open: boolean;\n /** Bold heading line. */\n title: string;\n /** Body copy — a string or richer node. */\n message: React.ReactNode;\n /** Confirm button label (default \"Delete\"). */\n confirmLabel?: string;\n /** Cancel button label (default \"Cancel\"). */\n cancelLabel?: string;\n /** When true (default), the confirm button reads as a destructive (red) action. */\n destructive?: boolean;\n /** Fired when the user confirms. */\n onConfirm: () => void;\n /** Fired on Cancel, Escape, or backdrop click. */\n onCancel: () => void;\n /** data-testid prefix so each dialog is addressable in tests. */\n testIdPrefix?: string;\n}\n\nexport function ConfirmDialog({\n open,\n title,\n message,\n confirmLabel = 'Delete',\n cancelLabel = 'Cancel',\n destructive = true,\n onConfirm,\n onCancel,\n testIdPrefix = 'confirm-dialog',\n}: ConfirmDialogProps): React.ReactElement | null {\n const cancelRef = useRef<HTMLButtonElement>(null);\n\n // Escape, backdrop click, and focus-on-open are owned by the shared <Modal>.\n return (\n <Modal open={open} onClose={onCancel} testIdPrefix={testIdPrefix} initialFocusRef={cancelRef}>\n <div\n className=\"w-[360px] max-w-[90vw] flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl\"\n onClick={(e) => e.stopPropagation()}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n data-testid={`${testIdPrefix}-modal`}\n >\n {/* Header */}\n <div className=\"px-4 py-3 border-b border-sas-border\">\n <span className=\"text-sm font-medium text-sas-text\" data-testid={`${testIdPrefix}-title`}>\n {title}\n </span>\n </div>\n\n {/* Body */}\n <div\n className=\"px-4 py-3 text-xs text-sas-muted leading-relaxed break-words\"\n data-testid={`${testIdPrefix}-message`}\n >\n {message}\n </div>\n\n {/* Footer */}\n <div className=\"flex justify-end gap-2 px-4 py-3 border-t border-sas-border\">\n <button\n ref={cancelRef}\n type=\"button\"\n className=\"px-3 py-1 rounded-sm text-xs font-medium border border-sas-border bg-sas-panel-alt text-sas-text hover:border-sas-accent hover:text-sas-accent transition-colors\"\n onClick={onCancel}\n data-testid={`${testIdPrefix}-cancel`}\n >\n {cancelLabel}\n </button>\n <button\n type=\"button\"\n className={`px-3 py-1 rounded-sm text-xs font-medium border transition-colors ${\n destructive\n ? 'border-sas-danger bg-sas-danger/20 text-sas-danger hover:bg-sas-danger hover:text-sas-bg'\n : 'border-sas-accent bg-sas-accent/20 text-sas-accent hover:bg-sas-accent hover:text-sas-bg'\n }`}\n onClick={onConfirm}\n data-testid={`${testIdPrefix}-confirm`}\n >\n {confirmLabel}\n </button>\n </div>\n </div>\n </Modal>\n );\n}\n\nexport default ConfirmDialog;\n","/**\n * Modal — the SDK's one modal-stacking primitive (portal + z-tier + backdrop).\n *\n * Every SDK modal renders INSIDE a plugin's accordion section, whose animated\n * `overflow-hidden` + `transition-all` wrapper establishes a stacking context.\n * An inline `position: fixed` overlay is therefore scoped to that section and\n * can be painted UNDER a neighbouring panel (the \"import modal invisible on a\n * later open\" bug). This component solves that once: it portals the overlay to\n * <body> — out of every panel's stacking context — at a z-tier above all the\n * app's `z-50` dropdowns/banners but below the toast tier (`z-[9999]`), so\n * toasts still float over modals.\n *\n * Controlled: the caller owns `open` and `onClose`. The caller renders its own\n * dialog box as `children` (keep the box's `onClick={e => e.stopPropagation()}`\n * so inside-clicks don't dismiss). Escape and a backdrop click both close.\n *\n * @since SDK 2.21.0\n */\n\nimport React, { useEffect } from 'react';\nimport { createPortal } from 'react-dom';\n\nexport interface ModalProps {\n /** Controls visibility (the caller owns open/closed). */\n open: boolean;\n /** Close handler — fired on Escape and backdrop click. */\n onClose: () => void;\n /** The dialog box. Give it `onClick={e => e.stopPropagation()}`. */\n children: React.ReactNode;\n /** data-testid prefix; the backdrop is `${testIdPrefix}-overlay`. */\n testIdPrefix?: string;\n /** Close when the backdrop is clicked (default true). */\n closeOnBackdrop?: boolean;\n /** Close on Escape (default true). */\n closeOnEscape?: boolean;\n /** Focused when the modal opens (e.g. a Cancel button) so a reflexive Enter is safe. */\n initialFocusRef?: React.RefObject<HTMLElement>;\n}\n\nexport function Modal({\n open,\n onClose,\n children,\n testIdPrefix = 'modal',\n closeOnBackdrop = true,\n closeOnEscape = true,\n initialFocusRef,\n}: ModalProps): React.ReactElement | null {\n // Escape closes; focus the requested element on open.\n useEffect(() => {\n if (!open) return undefined;\n const onKey = (e: KeyboardEvent): void => {\n if (closeOnEscape && e.key === 'Escape') {\n e.preventDefault();\n onClose();\n }\n };\n window.addEventListener('keydown', onKey);\n initialFocusRef?.current?.focus();\n return () => window.removeEventListener('keydown', onKey);\n }, [open, onClose, closeOnEscape, initialFocusRef]);\n\n if (!open) return null;\n\n return createPortal(\n <div\n className=\"fixed inset-0 z-[1000] flex items-center justify-center bg-black/60\"\n data-testid={`${testIdPrefix}-overlay`}\n onClick={closeOnBackdrop ? onClose : undefined}\n >\n {children}\n </div>,\n document.body,\n );\n}\n\nexport default Modal;\n","/**\n * Shared level-meter component.\n *\n * Renders a horizontal LED-style bar over -60dBFS → 0dBFS:\n * - A fixed left-to-right gradient (green → orange → red), so the color is\n * tied to POSITION: a quiet signal lights only the green left, a hot signal\n * reaches the red right. An \"unlit\" mask hides the gradient beyond the\n * current level.\n * - A deterministic segment grid (the \"LED monitor\" look) drawn as a pure-CSS\n * repeating overlay — constant DOM, no per-frame cost.\n * - An optional peak-hold marker (`peakHoldDb`) — a bright line at the recent\n * maximum that the caller holds/decays (see `useTrackMeter`).\n * - An optional CLIP badge the caller wires up.\n *\n * Pure presentational: takes the current dB + `active` flag (+ optional held\n * peak) and draws. The only production consumer is the per-track strip\n * (`TrackMeterStrip`, via `compact`). `compact` shrinks the bar and drops the\n * numeric dB readout.\n */\n\nimport React from 'react';\n\n// Traffic-light gradient (introduced for the LED meter; the Magic Terminal\n// palette has no green/orange/red tokens). Tweakable.\nconst COLOR_GREEN = '#2BD576';\nconst COLOR_ORANGE = '#F5A623';\nconst COLOR_RED = '#FF4D5E';\nconst COLOR_TRACK_BG = '#121822'; // panel-alt — the unlit bar / mask\nconst COLOR_TRACK_BORDER = '#1F2A3A'; // border\nconst COLOR_SEGMENT_GAP = '#0A0E14'; // dark gutter between LED cells\nconst COLOR_PEAK = '#F7FFFB'; // held-peak marker (bright)\n\n// The positional gradient. Mostly green, orange in the upper-mid, red near the\n// top — the classic meter feel, while still visibly tri-color across the bar.\nconst METER_GRADIENT = `linear-gradient(90deg, ${COLOR_GREEN} 0%, ${COLOR_GREEN} 45%, ${COLOR_ORANGE} 72%, ${COLOR_RED} 90%, ${COLOR_RED} 100%)`;\n\n// Deterministic LED sections + the gutter width between them.\nconst SEGMENTS = 22;\nconst SEGMENT_GAP_PX = 2;\n\n/** dBFS → bar % : -60dB → 0%, 0dB → 100%, clamped. */\nfunction dbToPct(db: number): number {\n return Math.max(0, Math.min(100, ((db + 60) / 60) * 100));\n}\n\nexport interface LevelMeterProps {\n /** Current peak level in dBFS. -120 means \"no signal\". */\n peakDb: number;\n /** True when the underlying audio callback is firing. False = floor. */\n active: boolean;\n /**\n * Held peak in dBFS for the peak-hold marker. Omit to draw no marker. The\n * marker is hidden when this is at/below the visible floor (-60).\n */\n peakHoldDb?: number;\n /** Latched clip flag. When true, render the CLIP badge. */\n clipped?: boolean;\n /** User-clickable handler to clear the latched clip indicator. */\n onClearClip?: () => void;\n /**\n * Thin strip mode for per-track meters: hides the numeric dB readout and\n * shrinks the bar. Keeps the (rare) CLIP badge.\n */\n compact?: boolean;\n /** Optional className overlaid on the wrapper for layout tweaks. */\n className?: string;\n /** Inline test id — make multiple instances distinguishable. */\n 'data-testid'?: string;\n}\n\nexport const LevelMeter: React.FC<LevelMeterProps> = ({\n peakDb,\n active,\n peakHoldDb,\n clipped,\n onClearClip,\n compact = false,\n className,\n 'data-testid': testId,\n}) => {\n const id = testId ?? 'sas-level-meter';\n const widthPct = active ? dbToPct(peakDb) : 0;\n const showPeak = peakHoldDb != null && active && peakHoldDb > -60;\n const peakHoldPct = showPeak ? dbToPct(peakHoldDb!) : 0;\n\n return (\n <div\n className={`sas-level-meter ${className ?? ''}`}\n data-testid={id}\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: compact ? 0 : 6,\n }}\n >\n <div\n style={{\n position: 'relative',\n flex: 1,\n height: compact ? 5 : 7,\n background: COLOR_TRACK_BG,\n border: `1px solid ${COLOR_TRACK_BORDER}`,\n borderRadius: 2,\n overflow: 'hidden',\n minWidth: compact ? 0 : 60,\n }}\n >\n {/* Positional green→orange→red gradient, full bar width. */}\n <div style={{ position: 'absolute', inset: 0, background: METER_GRADIENT }} />\n\n {/* Unlit mask: hides the gradient from the current level rightward. */}\n <div\n style={{\n position: 'absolute',\n top: 0,\n bottom: 0,\n left: `${widthPct}%`,\n right: 0,\n background: COLOR_TRACK_BG,\n transition: 'left 30ms linear',\n }}\n />\n\n {/* Deterministic LED segment gutters — pure CSS, constant DOM. */}\n <div\n data-testid={`${id}-segments`}\n style={{\n position: 'absolute',\n inset: 0,\n pointerEvents: 'none',\n backgroundImage: `linear-gradient(90deg, transparent 0, transparent calc(100% - ${SEGMENT_GAP_PX}px), ${COLOR_SEGMENT_GAP} calc(100% - ${SEGMENT_GAP_PX}px), ${COLOR_SEGMENT_GAP} 100%)`,\n backgroundSize: `calc(100% / ${SEGMENTS}) 100%`,\n }}\n />\n\n {/* Peak-hold marker: a bright line at the recent maximum. */}\n {showPeak && (\n <div\n data-testid={`${id}-peak`}\n style={{\n position: 'absolute',\n top: -1,\n bottom: -1,\n left: `${peakHoldPct}%`,\n width: 2,\n marginLeft: -1,\n background: COLOR_PEAK,\n boxShadow: '0 0 4px rgba(247, 255, 251, 0.7)',\n transition: 'left 80ms linear',\n }}\n title=\"Peak\"\n />\n )}\n </div>\n\n {!compact && (\n <span\n style={{\n fontSize: 10,\n color: 'var(--sas-muted, #888)',\n fontVariantNumeric: 'tabular-nums',\n minWidth: 48,\n textAlign: 'right',\n }}\n >\n {active && peakDb > -120 ? `${peakDb.toFixed(0)} dB` : '—'}\n </span>\n )}\n {clipped && (\n <span\n data-testid={`${id}-clip`}\n onClick={onClearClip}\n style={{\n padding: '1px 5px',\n fontSize: 9,\n fontWeight: 'bold',\n background: COLOR_RED,\n color: '#0A0E14',\n borderRadius: 2,\n cursor: onClearClip ? 'pointer' : 'default',\n marginLeft: compact ? 3 : 0,\n }}\n title={onClearClip ? 'Clipped — click to clear' : 'Clipped'}\n >\n CLIP\n </span>\n )}\n </div>\n );\n};\n\nexport default LevelMeter;\n","/**\n * useTrackLevels — drives the cosmetic per-track strip meters.\n *\n * The hard constraint for this feature is \"playback ALWAYS wins over the GUI;\n * NO blocking threads.\" This hook is built around that:\n *\n * - It polls `host.getTrackLevels()` at ~30Hz with a recursive setTimeout that\n * only schedules the NEXT tick AFTER the previous await resolves. That is\n * automatic backpressure: a slow/stalled engine simply slows the meter, it\n * can never queue a backlog of requests. (The host + bridge also coalesce,\n * so a busy engine yields a STALE snapshot, never a pile-up.)\n * - It writes into a ref-held Map and notifies row subscribers, so the OWNING\n * panel never re-renders at 30Hz. Each row reads its own value via\n * `useTrackLevel` and re-renders only itself.\n * - It polls while the panel is mounted and the window is visible, and pauses\n * when the window is hidden. It deliberately does NOT gate on transport\n * \"is playing\": this app drives playback through decks / the clip launcher,\n * and the linear-transport play flag does not track that reliably. When\n * audio is stopped the engine simply returns floor levels, so the bars are\n * empty anyway — no need (and no reliable signal) to stop polling.\n *\n * Usage (panel):\n * const levels = useTrackLevels(host);\n * ...<TrackRow levels={levels} ... /> // row calls useTrackLevel(levels, id)\n */\n\nimport { useEffect, useRef, useState } from 'react';\nimport type { PluginHost, PluginTrackLevel } from '../types/plugin-sdk.types';\n\n/** Polling cadence — matches the recording input meter (~30Hz). */\nconst POLL_INTERVAL_MS = 33;\n/** Slow idle re-check while the window is hidden (polling is paused). */\nconst HIDDEN_RECHECK_MS = 250;\n\n/** dBFS floor / \"no signal\" sentinel (matches PluginTrackLevel). */\nconst METER_FLOOR_DB = -120;\n/** Hold the peak marker this long after a fresh peak before it starts to fall. */\nconst PEAK_HOLD_MS = 1500;\n/** Fall rate once the hold window expires (dB per second). */\nconst PEAK_DECAY_DB_PER_SEC = 24;\n\n/**\n * Stable handle returned by {@link useTrackLevels}. Rows read their own level\n * and subscribe to per-tick notifications through it; its identity is stable\n * across renders so a row's subscription is set up once.\n */\nexport interface TrackLevelsHandle {\n /** Current level for a track, or null when idle/absent (renders an empty bar). */\n getLevel(trackId: string): PluginTrackLevel | null;\n /** Subscribe to per-tick updates. Returns an unsubscribe function. */\n subscribe(listener: () => void): () => void;\n}\n\nfunction isHidden(): boolean {\n return typeof document !== 'undefined' && document.hidden === true;\n}\n\n/**\n * Poll every owned track's level while mounted + visible. Returns a stable\n * handle; the owning component does NOT re-render per tick. Pass `enabled =\n * false` to turn it off entirely (e.g. a panel that wants no meters). Safe to\n * call even when the host predates `getTrackLevels` (older SDK) — it stays idle.\n */\nexport function useTrackLevels(\n host: PluginHost | null | undefined,\n enabled: boolean = true\n): TrackLevelsHandle {\n const mapRef = useRef<Map<string, PluginTrackLevel>>(new Map());\n const listenersRef = useRef<Set<() => void>>(new Set());\n\n // Built exactly once so the handle identity is stable across renders.\n const handleRef = useRef<TrackLevelsHandle | null>(null);\n if (handleRef.current === null) {\n handleRef.current = {\n getLevel: (trackId: string) => mapRef.current.get(trackId) ?? null,\n subscribe: (listener: () => void) => {\n listenersRef.current.add(listener);\n return () => {\n listenersRef.current.delete(listener);\n };\n },\n };\n }\n\n useEffect(() => {\n const notify = (): void => {\n listenersRef.current.forEach((l) => l());\n };\n\n const clearToIdle = (): void => {\n if (mapRef.current.size > 0) {\n mapRef.current.clear();\n notify();\n }\n };\n\n const canPoll =\n enabled && !!host && typeof host.getTrackLevels === 'function';\n\n if (!canPoll) {\n clearToIdle();\n return;\n }\n\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const schedule = (delay: number): void => {\n if (stopped) return;\n timer = setTimeout(tick, delay);\n };\n\n const tick = async (): Promise<void> => {\n if (stopped) return;\n\n // Paused while the window is hidden: do no engine work, just idle-poll\n // until it comes back. (visibilitychange below resumes immediately.)\n if (isHidden()) {\n schedule(HIDDEN_RECHECK_MS);\n return;\n }\n\n try {\n const levels = await host!.getTrackLevels!();\n if (stopped) return;\n\n // Rebuild the map: upsert present tracks, drop ones that vanished.\n const seen = new Set<string>();\n for (const lvl of levels) {\n mapRef.current.set(lvl.trackId, lvl);\n seen.add(lvl.trackId);\n }\n for (const key of Array.from(mapRef.current.keys())) {\n if (!seen.has(key)) mapRef.current.delete(key);\n }\n notify();\n } catch {\n // Cosmetic meter: swallow transient read failures and keep polling.\n }\n\n // Schedule the NEXT tick only now — backpressure: never overlap reads.\n schedule(POLL_INTERVAL_MS);\n };\n\n const onVisibility = (): void => {\n if (stopped) return;\n if (!isHidden()) {\n // Becoming visible: cancel the slow idle-poll and resume immediately.\n if (timer) clearTimeout(timer);\n void tick();\n }\n };\n\n if (typeof document !== 'undefined') {\n document.addEventListener('visibilitychange', onVisibility);\n }\n\n void tick();\n\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n if (typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', onVisibility);\n }\n // Leave the map intact on teardown; the next active effect rebuilds it.\n };\n }, [host, enabled]);\n\n return handleRef.current;\n}\n\n/** Cheap equality so unchanged rows skip re-rendering between ticks. */\nfunction sameLevel(\n a: PluginTrackLevel | null,\n b: PluginTrackLevel | null\n): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n return a.peakDb === b.peakDb && a.clipped === b.clipped;\n}\n\n/**\n * Per-row selector. Subscribes to the shared scheduler and re-renders ONLY the\n * calling component when this track's level changes. Returns null when idle\n * (transport stopped, window hidden, or the track has no meter yet).\n */\nexport function useTrackLevel(\n handle: TrackLevelsHandle | null | undefined,\n trackId: string\n): PluginTrackLevel | null {\n const [level, setLevel] = useState<PluginTrackLevel | null>(null);\n\n useEffect(() => {\n if (!handle) {\n setLevel(null);\n return;\n }\n const update = (): void => {\n const next = handle.getLevel(trackId);\n setLevel((prev) => (sameLevel(prev, next) ? prev : next));\n };\n update(); // seed immediately\n return handle.subscribe(update);\n }, [handle, trackId]);\n\n return level;\n}\n\n/**\n * Per-row meter view-model: the current level plus a held peak for the meter UI.\n */\nexport interface TrackMeterView {\n /** Current mono peak in dBFS (floored at -120). */\n peakDb: number;\n /** Held peak in dBFS — stays at the recent maximum for ~PEAK_HOLD_MS, then falls. */\n peakHoldDb: number;\n /** Latched clip flag for the last poll window. */\n clipped: boolean;\n /** True when the track currently has a live meter row. */\n active: boolean;\n}\n\nconst IDLE_METER_VIEW: TrackMeterView = {\n peakDb: METER_FLOOR_DB,\n peakHoldDb: METER_FLOOR_DB,\n clipped: false,\n active: false,\n};\n\n/** Equality gate for the meter view. Quantizes the held peak to ½ dB so a\n * steady hold and sub-pixel decay don't thrash renders, while a real change\n * (level jitter, decay step, clip, active) still re-renders the strip. */\nfunction sameMeter(a: TrackMeterView, b: TrackMeterView): boolean {\n return (\n a.active === b.active &&\n a.clipped === b.clipped &&\n a.peakDb === b.peakDb &&\n Math.round(a.peakHoldDb * 2) === Math.round(b.peakHoldDb * 2)\n );\n}\n\n/**\n * Per-row meter selector WITH PEAK-HOLD. Like {@link useTrackLevel} it subscribes\n * to the shared ~30Hz scheduler and re-renders only the calling component, but it\n * also tracks a held peak that stays at the recent maximum for ~PEAK_HOLD_MS then\n * decays — so the eye can register where the signal peaked while the bar itself\n * moves fast. No extra timers or rAF: the held value is recomputed on each\n * scheduler notify, using performance.now() for hold/decay timing.\n */\nexport function useTrackMeter(\n handle: TrackLevelsHandle | null | undefined,\n trackId: string\n): TrackMeterView {\n const [view, setView] = useState<TrackMeterView>(IDLE_METER_VIEW);\n\n // Peak-hold state lives in refs so it survives between notifies without\n // forcing a render; only the derived `view` is state.\n const heldDbRef = useRef(METER_FLOOR_DB);\n const heldAtRef = useRef(0);\n const lastTickRef = useRef(0);\n\n useEffect(() => {\n if (!handle) {\n heldDbRef.current = METER_FLOOR_DB;\n lastTickRef.current = 0;\n setView(IDLE_METER_VIEW);\n return;\n }\n\n const update = (): void => {\n const level = handle.getLevel(trackId);\n const now = performance.now();\n const dtSec = lastTickRef.current ? Math.max(0, (now - lastTickRef.current) / 1000) : 0;\n lastTickRef.current = now;\n\n if (level === null) {\n // No live row for this track — go idle and reset the hold.\n heldDbRef.current = METER_FLOOR_DB;\n setView((prev) => (sameMeter(prev, IDLE_METER_VIEW) ? prev : IDLE_METER_VIEW));\n return;\n }\n\n const p = level.peakDb;\n if (p >= heldDbRef.current) {\n // Fresh peak: snap the held value up and restart the hold window.\n heldDbRef.current = p;\n heldAtRef.current = now;\n } else if (now - heldAtRef.current > PEAK_HOLD_MS) {\n // Hold expired: fall toward the current level.\n heldDbRef.current = Math.max(p, heldDbRef.current - PEAK_DECAY_DB_PER_SEC * dtSec);\n }\n // else: still within the hold window — keep the held value steady.\n\n const next: TrackMeterView = {\n peakDb: p,\n peakHoldDb: heldDbRef.current,\n clipped: level.clipped,\n active: true,\n };\n setView((prev) => (sameMeter(prev, next) ? prev : next));\n };\n\n update(); // seed immediately\n return handle.subscribe(update);\n }, [handle, trackId]);\n\n return view;\n}\n\n/**\n * Track the transport's play/stop state for a plugin. Seeds from\n * `getTransportState()` and follows `onTransportEvent`. Use its result as the\n * `active` arg to {@link useTrackLevels} so meters animate only during playback.\n */\nexport function useTransportPlaying(host: PluginHost | null | undefined): boolean {\n const [playing, setPlaying] = useState(false);\n\n useEffect(() => {\n if (!host) {\n setPlaying(false);\n return;\n }\n let cancelled = false;\n\n host\n .getTransportState()\n .then((state) => {\n if (!cancelled) setPlaying(!!state.isPlaying);\n })\n .catch(() => {\n /* seed best-effort; events will correct it */\n });\n\n const unsub = host.onTransportEvent?.((evt) => {\n if (typeof evt.isPlaying === 'boolean') {\n setPlaying(evt.isPlaying);\n } else if (evt.type === 'play') {\n setPlaying(true);\n } else if (evt.type === 'stop' || evt.type === 'pause') {\n setPlaying(false);\n }\n });\n\n return () => {\n cancelled = true;\n unsub?.();\n };\n }, [host]);\n\n return playing;\n}\n","/**\n * TrackMeterStrip — the thin per-track peak meter welded to the bottom of a\n * track row. Cosmetic: gives a general sense of each track's level and adds\n * motion during playback.\n *\n * This is deliberately its OWN component so the per-row meter selector\n * (`useTrackMeter`) re-renders ONLY this strip at ~30Hz, never the heavy\n * TrackRow around it. Render it as a full-width sibling directly under a row\n * body; it welds on with a squared top edge (like the track drawer does).\n */\n\nimport React from 'react';\nimport { LevelMeter } from './LevelMeter';\nimport { useTrackMeter, type TrackLevelsHandle } from '../hooks/useTrackLevels';\n\nexport interface TrackMeterStripProps {\n /** Shared meter handle from `useTrackLevels(host, isPlaying)`. */\n levels: TrackLevelsHandle;\n /** Tracktion engine track id (matches `PluginTrackHandle.id`). */\n trackId: string;\n /** Round the bottom corners (false when a drawer welds on below). Default true. */\n roundBottom?: boolean;\n /** Optional className for layout tweaks on the wrapper. */\n className?: string;\n}\n\nexport const TrackMeterStrip: React.FC<TrackMeterStripProps> = ({\n levels,\n trackId,\n roundBottom = true,\n className,\n}) => {\n const meter = useTrackMeter(levels, trackId);\n\n return (\n <div\n data-testid=\"sdk-track-meter\"\n className={`w-full px-2 py-1 bg-sas-panel-alt border border-t-0 border-sas-border ${roundBottom ? 'rounded-b-sm' : ''} ${className ?? ''}`}\n >\n <LevelMeter\n compact\n active={meter.active}\n peakDb={meter.peakDb}\n peakHoldDb={meter.peakHoldDb}\n clipped={meter.clipped}\n data-testid={`sdk-track-meter-bar-${trackId}`}\n />\n </div>\n );\n};\n\nexport default TrackMeterStrip;\n","/**\n * VolumeSlider Component\n *\n * Compact horizontal volume slider for track volume control.\n * Uses native HTML range input with custom styling.\n */\n\nimport React, { useCallback, useState, useRef, useEffect } from 'react';\nimport { sliderToDb } from '../utils/volume-conversion';\n\ninterface VolumeSliderProps {\n /** Volume value from 0 to 1 */\n value: number;\n /** Called when volume changes (debounced) */\n onChange: (value: number) => void;\n /** Disable the slider */\n disabled?: boolean;\n /** Additional CSS classes */\n className?: string;\n}\n\n/**\n * Format slider value as dB for tooltip display\n */\nfunction formatDb(value: number): string {\n const db = sliderToDb(value);\n if (db <= -60) return '-∞ dB';\n const sign = db >= 0 ? '+' : '';\n return `${sign}${db.toFixed(1)} dB`;\n}\n\n/**\n * Debounce helper for volume changes\n */\nfunction useDebouncedCallback<T extends (...args: never[]) => void>(\n callback: T,\n delay: number\n): T {\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n\n // Update callback ref when callback changes\n useEffect(() => {\n callbackRef.current = callback;\n }, [callback]);\n\n const debouncedCallback = useCallback(\n (...args: Parameters<T>) => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n timeoutRef.current = setTimeout(() => {\n callbackRef.current(...args);\n }, delay);\n },\n [delay]\n ) as T;\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return debouncedCallback;\n}\n\nexport const VolumeSlider: React.FC<VolumeSliderProps> = ({\n value,\n onChange,\n disabled = false,\n className = '',\n}) => {\n // Local state for immediate visual feedback\n const [localValue, setLocalValue] = useState(value);\n const [isDragging, setIsDragging] = useState(false);\n\n // Sync local value with prop when not dragging\n useEffect(() => {\n if (!isDragging) {\n setLocalValue(value);\n }\n }, [value, isDragging]);\n\n // Debounced onChange to prevent IPC spam\n const debouncedOnChange = useDebouncedCallback(onChange, 50);\n\n const handleChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const newValue = parseFloat(e.target.value);\n setLocalValue(newValue);\n debouncedOnChange(newValue);\n },\n [debouncedOnChange]\n );\n\n const handleMouseDown = useCallback(() => {\n setIsDragging(true);\n }, []);\n\n const handleMouseUp = useCallback(() => {\n setIsDragging(false);\n // Send final value immediately on release\n onChange(localValue);\n }, [localValue, onChange]);\n\n return (\n <div\n className={`flex items-center ${className}`}\n title={`Volume: ${formatDb(localValue)}`}\n >\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={localValue}\n onChange={handleChange}\n onMouseDown={handleMouseDown}\n onMouseUp={handleMouseUp}\n onTouchStart={handleMouseDown}\n onTouchEnd={handleMouseUp}\n disabled={disabled}\n className={`\n w-full h-1.5 rounded-full appearance-none cursor-pointer\n bg-gray-700\n disabled:opacity-50 disabled:cursor-not-allowed\n [&::-webkit-slider-thumb]:appearance-none\n [&::-webkit-slider-thumb]:w-3\n [&::-webkit-slider-thumb]:h-3\n [&::-webkit-slider-thumb]:rounded-full\n [&::-webkit-slider-thumb]:bg-sas-accent\n [&::-webkit-slider-thumb]:cursor-pointer\n [&::-webkit-slider-thumb]:transition-transform\n [&::-webkit-slider-thumb]:hover:scale-110\n [&::-moz-range-thumb]:w-3\n [&::-moz-range-thumb]:h-3\n [&::-moz-range-thumb]:rounded-full\n [&::-moz-range-thumb]:bg-sas-accent\n [&::-moz-range-thumb]:border-0\n [&::-moz-range-thumb]:cursor-pointer\n `}\n />\n </div>\n );\n};\n\nexport default VolumeSlider;\n","/**\n * Volume Conversion Utilities\n *\n * Converts between UI slider position (0-1) and engine dB values using a power\n * curve with +6 dB headroom. The curve places unity gain (0 dB) at slider 0.75,\n * giving the top 25% of the slider a meaningful 6 dB boost range instead of the\n * previous perceptual dead zone.\n *\n * Mapping:\n * slider 0.00 → -60 dB (silence)\n * slider 0.75 → 0 dB (unity gain)\n * slider 1.00 → +6 dB (max boost)\n */\n\n/** Slider position that maps to 0 dB (unity gain) */\nexport const SLIDER_UNITY = 0.75;\n\n/** Maximum dB value at slider = 1.0 */\nexport const DB_MAX = 6;\n\n/** Minimum dB value (silence floor) */\nexport const DB_MIN = -60;\n\n/**\n * Exponent derived so that slider=1.0 yields exactly DB_MAX dB.\n *\n * gain_at_1 = (1 / SLIDER_UNITY) ^ EXPONENT = 10^(DB_MAX/20)\n * EXPONENT = log(10^(DB_MAX/20)) / log(1/SLIDER_UNITY)\n */\nconst EXPONENT: number =\n Math.log(Math.pow(10, DB_MAX / 20)) / Math.log(1 / SLIDER_UNITY);\n\n/**\n * Convert a UI slider position (0-1) to engine dB.\n *\n * @param slider - Slider value in [0, 1]\n * @returns dB value in [DB_MIN, DB_MAX]\n */\nexport function sliderToDb(slider: number): number {\n if (slider <= 0) return DB_MIN;\n const gain = Math.pow(slider / SLIDER_UNITY, EXPONENT);\n const db = 20 * Math.log10(gain);\n return Math.max(DB_MIN, Math.min(DB_MAX, db));\n}\n\n/**\n * Convert an engine dB value back to a UI slider position (0-1).\n * Inverse of sliderToDb().\n *\n * @param db - Volume in dB\n * @returns Slider value in [0, 1]\n */\nexport function dbToSlider(db: number): number {\n if (db <= DB_MIN) return 0;\n if (db >= DB_MAX) return 1;\n const gain = Math.pow(10, db / 20);\n const slider = SLIDER_UNITY * Math.pow(gain, 1 / EXPONENT);\n return Math.min(1, Math.max(0, slider));\n}\n","/**\n * PanSlider Component\n *\n * Compact horizontal pan slider for track stereo positioning.\n * Range: -1 (left) to +1 (right), 0 = center.\n * No text label - tooltip only.\n */\n\nimport React, { useCallback, useState, useRef, useEffect } from 'react';\n\ninterface PanSliderProps {\n /** Pan value from -1 (left) to 1 (right), 0 = center */\n value: number;\n /** Called when pan changes (debounced) */\n onChange: (value: number) => void;\n /** Disable the slider */\n disabled?: boolean;\n /** Additional CSS classes */\n className?: string;\n}\n\n/**\n * Convert pan value (-1 to 1) to display string\n */\nfunction toPanDisplay(value: number): string {\n if (Math.abs(value) < 0.02) {\n return 'Center';\n }\n const percent = Math.abs(Math.round(value * 100));\n return value < 0 ? `L${percent}` : `R${percent}`;\n}\n\n/**\n * Debounce helper for pan changes\n */\nfunction useDebouncedCallback<T extends (...args: never[]) => void>(\n callback: T,\n delay: number\n): T {\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n\n useEffect(() => {\n callbackRef.current = callback;\n }, [callback]);\n\n const debouncedCallback = useCallback(\n (...args: Parameters<T>) => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n timeoutRef.current = setTimeout(() => {\n callbackRef.current(...args);\n }, delay);\n },\n [delay]\n ) as T;\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return debouncedCallback;\n}\n\nexport const PanSlider: React.FC<PanSliderProps> = ({\n value,\n onChange,\n disabled = false,\n className = '',\n}) => {\n // Local state for immediate visual feedback\n const [localValue, setLocalValue] = useState(value);\n const [isDragging, setIsDragging] = useState(false);\n\n // Sync local value with prop when not dragging\n useEffect(() => {\n if (!isDragging) {\n setLocalValue(value);\n }\n }, [value, isDragging]);\n\n // Debounced onChange to prevent IPC spam\n const debouncedOnChange = useDebouncedCallback(onChange, 50);\n\n const handleChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const newValue = parseFloat(e.target.value);\n setLocalValue(newValue);\n debouncedOnChange(newValue);\n },\n [debouncedOnChange]\n );\n\n const handleMouseDown = useCallback(() => {\n setIsDragging(true);\n }, []);\n\n const handleMouseUp = useCallback(() => {\n setIsDragging(false);\n // Send final value immediately on release\n onChange(localValue);\n }, [localValue, onChange]);\n\n // Double-click to reset to center\n const handleDoubleClick = useCallback(() => {\n setLocalValue(0);\n onChange(0);\n }, [onChange]);\n\n return (\n <div\n className={`flex items-center ${className}`}\n title={`Pan: ${toPanDisplay(localValue)}`}\n >\n <input\n type=\"range\"\n min=\"-1\"\n max=\"1\"\n step=\"0.01\"\n value={localValue}\n onChange={handleChange}\n onMouseDown={handleMouseDown}\n onMouseUp={handleMouseUp}\n onTouchStart={handleMouseDown}\n onTouchEnd={handleMouseUp}\n onDoubleClick={handleDoubleClick}\n disabled={disabled}\n className={`\n w-full h-1.5 rounded-full appearance-none cursor-pointer\n bg-gray-700\n disabled:opacity-50 disabled:cursor-not-allowed\n [&::-webkit-slider-thumb]:appearance-none\n [&::-webkit-slider-thumb]:w-3\n [&::-webkit-slider-thumb]:h-3\n [&::-webkit-slider-thumb]:rounded-full\n [&::-webkit-slider-thumb]:bg-sas-accent\n [&::-webkit-slider-thumb]:cursor-pointer\n [&::-webkit-slider-thumb]:transition-transform\n [&::-webkit-slider-thumb]:hover:scale-110\n [&::-moz-range-thumb]:w-3\n [&::-moz-range-thumb]:h-3\n [&::-moz-range-thumb]:rounded-full\n [&::-moz-range-thumb]:bg-sas-accent\n [&::-moz-range-thumb]:border-0\n [&::-moz-range-thumb]:cursor-pointer\n `}\n />\n </div>\n );\n};\n\nexport default PanSlider;\n","/**\n * SorceryProgressBar Component\n *\n * A progress bar for long, uncertain wait times (10-30s). Supports two modes:\n *\n * 1. **Time-based mode** (when `estimatedDurationMs` is provided):\n * Uses elapsed time and an ease-out curve to pace progress realistically.\n * Reaches ~90% at the estimated completion time, then asymptotically\n * approaches 95% if the operation runs long.\n *\n * 2. **Phase-based mode** (legacy fallback, no `estimatedDurationMs`):\n * \"Zeno's Paradox\" style - progress moves quickly at first, then\n * asymptotically slows toward 95%.\n *\n * Visual style: Segmented \"retro CLI\" look with glowing teal accent,\n * diagonal stripes, and subtle pulse animation.\n */\n\nimport React, { useState, useEffect, useRef } from 'react';\n\n/**\n * Props for SorceryProgressBar component\n */\ninterface SorceryProgressBarProps {\n /** Whether loading is in progress */\n isLoading: boolean;\n /** Text shown during loading (default: \"CONJURING...\") */\n statusText?: string;\n /** Text shown on completion (default: \"COMPLETE\") */\n completeText?: string;\n /** Callback when loading completes */\n onComplete?: () => void;\n /** Height class override (default: \"h-10\") */\n heightClass?: string;\n /** Initial progress value (0-100) to resume from - persists across scene switches */\n initialProgress?: number;\n /** Callback when progress changes - use to persist progress in parent state */\n onProgressChange?: (progress: number) => void;\n /** Estimated total duration in ms - enables time-aware pacing */\n estimatedDurationMs?: number;\n}\n\n/**\n * Calculates target progress based on elapsed time and estimated duration.\n * Uses an ease-out power curve for natural-feeling progress:\n * - At 10% of estimated time: ~21% (feels responsive early)\n * - At 30% of estimated time: ~53% (good midpoint feel)\n * - At 50% of estimated time: ~74% (past halfway visually)\n * - At 80% of estimated time: ~88% (approaching completion)\n * - At 100% of estimated time: 90% (leaves room for overshoot)\n * - Beyond estimate: asymptotically approaches 95%\n */\nexport function calculateTimeBasedTarget(elapsedMs: number, estimatedDurationMs: number): number {\n const t = elapsedMs / estimatedDurationMs;\n if (t <= 0) return 0;\n\n if (t <= 1.0) {\n // Ease-out power curve reaching 90% at t=1.0\n return 90 * (1 - Math.pow(1 - t, 2.5));\n }\n\n // Beyond estimate: asymptotically approach 95%\n const overshootRatio = (elapsedMs - estimatedDurationMs) / estimatedDurationMs;\n return 90 + 5 * (1 - Math.exp(-overshootRatio * 3));\n}\n\n/**\n * Calculates the next progress value using \"Zeno's Paradox\" algorithm (legacy fallback).\n * - Phase 1 (0-20%): Rapid progress (5-15% per tick)\n * - Phase 2 (20-60%): Steady progress (2-7% per tick)\n * - Phase 3 (60-95%): Asymptotic slowdown\n * - Caps at 95% until actual completion\n */\nfunction calculateNextProgress(currentProgress: number): number {\n if (currentProgress < 20) {\n return currentProgress + Math.random() * 10 + 5;\n }\n if (currentProgress < 60) {\n return currentProgress + Math.random() * 5 + 2;\n }\n if (currentProgress < 95) {\n const remaining = 95 - currentProgress;\n const increment = remaining * (Math.random() * 0.2 + 0.1);\n return currentProgress + Math.max(increment, 0.1);\n }\n return 95;\n}\n\n/**\n * Calculates the next tick interval for phase-based mode (legacy fallback).\n */\nfunction calculateNextTickInterval(progress: number): number {\n if (progress < 30) {\n return Math.random() * 200 + 150; // 150-350ms\n }\n if (progress < 70) {\n return Math.random() * 300 + 200; // 200-500ms\n }\n return Math.random() * 600 + 400; // 400-1000ms\n}\n\n/** Tick interval for time-based mode (ms) */\nconst TIME_BASED_TICK_MIN = 200;\nconst TIME_BASED_TICK_RANGE = 100;\n\n/**\n * SorceryProgressBar - A mystical progress bar for uncertain wait times\n */\nexport function SorceryProgressBar({\n isLoading,\n statusText = 'CONJURING...',\n completeText = 'COMPLETE',\n onComplete,\n heightClass = 'h-10',\n initialProgress = 0,\n onProgressChange,\n estimatedDurationMs,\n}: SorceryProgressBarProps): React.ReactElement | null {\n const [progress, setProgress] = useState<number>(initialProgress);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n // Initialize to false so first render with isLoading=true triggers animation start\n const isLoadingRef = useRef<boolean>(false);\n const hasStartedRef = useRef<boolean>(false);\n const startTimeRef = useRef<number>(0);\n\n // Store callbacks in refs to avoid dependency issues\n const onProgressChangeRef = useRef(onProgressChange);\n const onCompleteRef = useRef(onComplete);\n onProgressChangeRef.current = onProgressChange;\n onCompleteRef.current = onComplete;\n\n // Store props in refs - only used when loading starts, not as dependencies\n const initialProgressRef = useRef(initialProgress);\n initialProgressRef.current = initialProgress;\n const estimatedDurationMsRef = useRef(estimatedDurationMs);\n estimatedDurationMsRef.current = estimatedDurationMs;\n\n // Effect to handle loading state changes - ONLY depends on isLoading\n useEffect(() => {\n const wasLoading = isLoadingRef.current;\n isLoadingRef.current = isLoading;\n\n if (isLoading && !wasLoading) {\n // Loading just started\n hasStartedRef.current = true;\n startTimeRef.current = Date.now();\n\n // Start fresh or resume from initial progress (read from ref)\n const startProgress = initialProgressRef.current > 0 ? initialProgressRef.current : 0;\n setProgress(startProgress);\n\n const duration = estimatedDurationMsRef.current;\n\n if (duration && duration > 0) {\n // Time-based mode: pace progress using elapsed time\n const tick = (): void => {\n setProgress((prev) => {\n const elapsed = Date.now() - startTimeRef.current;\n const target = calculateTimeBasedTarget(elapsed, duration);\n\n // Add subtle jitter for organic feel (±0.5%)\n const jitter = (Math.random() - 0.5) * 1.0;\n // Move toward target, ensure monotonically increasing, cap at 95%\n const next = Math.min(Math.max(target + jitter, prev + 0.05), 95);\n\n onProgressChangeRef.current?.(next);\n timerRef.current = setTimeout(tick, TIME_BASED_TICK_MIN + Math.random() * TIME_BASED_TICK_RANGE);\n return next;\n });\n };\n\n timerRef.current = setTimeout(tick, TIME_BASED_TICK_MIN);\n } else {\n // Phase-based mode (legacy fallback)\n const tick = (): void => {\n setProgress((prev) => {\n if (prev >= 95) {\n timerRef.current = setTimeout(tick, 1000);\n return 95;\n }\n\n const next = Math.min(calculateNextProgress(prev), 95);\n onProgressChangeRef.current?.(next);\n\n const interval = calculateNextTickInterval(next);\n timerRef.current = setTimeout(tick, interval);\n\n return next;\n });\n };\n\n const firstInterval = calculateNextTickInterval(startProgress);\n timerRef.current = setTimeout(tick, firstInterval);\n }\n } else if (!isLoading && wasLoading && hasStartedRef.current) {\n // Loading just finished - jump to 100%\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n setProgress(100);\n onProgressChangeRef.current?.(100);\n onCompleteRef.current?.();\n hasStartedRef.current = false;\n }\n\n // Cleanup on unmount only\n return () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n // ONLY depend on isLoading - other props are read from refs\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isLoading]);\n\n // Don't render if not loading and progress is 0\n if (!isLoading && progress === 0) {\n return null;\n }\n\n const displayProgress = Math.floor(progress);\n const isComplete = !isLoading && progress === 100;\n\n // Calculate transition duration based on progress phase\n const transitionDuration = progress < 50 ? '300ms' : progress < 80 ? '500ms' : '700ms';\n\n return (\n <div\n className={`relative w-full ${heightClass} bg-sas-panel-alt border border-sas-border rounded-sm overflow-hidden shadow-inner`}\n >\n {/* Progress fill with stripes and glow */}\n <div\n className={`\n h-full\n bg-gradient-to-r from-sas-accent/70 to-sas-accent\n shadow-glow-soft\n sorcery-progress-fill\n animate-progress-stripes\n ${progress > 70 ? 'animate-progress-pulse' : ''}\n transition-all ease-out\n `}\n style={{\n width: `${progress}%`,\n transitionDuration,\n }}\n />\n\n {/* Text overlay */}\n <div className=\"absolute inset-0 flex items-center justify-center\">\n {isLoading && progress < 100 ? (\n <span className=\"font-mono text-xs text-sas-accent font-bold drop-shadow-md tracking-wider\">\n {statusText} {displayProgress}%\n </span>\n ) : isComplete ? (\n <span className=\"font-mono text-xs text-sas-text font-bold drop-shadow-md tracking-wider\">\n {completeText}\n </span>\n ) : null}\n </div>\n\n {/* Scanline overlay for retro CRT effect */}\n <div\n className=\"absolute inset-0 pointer-events-none opacity-10\"\n style={{\n backgroundImage: `repeating-linear-gradient(\n to bottom,\n transparent,\n transparent 2px,\n rgba(0, 0, 0, 0.3) 2px,\n rgba(0, 0, 0, 0.3) 4px\n )`,\n }}\n />\n </div>\n );\n}\n\nexport default SorceryProgressBar;\n","/**\n * CrossfadeTrackRow — a transition \"crossfade track\": two stacked TrackRows\n * (origin on top, target on bottom) joined by a horizontal crossfade slider.\n *\n * Both layers play the SAME generated MIDI; the top wears the ORIGIN scene\n * track's preset and the bottom wears the TARGET scene track's preset. The user\n * cannot regenerate, shuffle, or change the preset/sample on either layer —\n * those controls are simply not wired into the inner TrackRows (the SDK\n * TrackRow is \"controlled by omission\"). What remains: per-layer volume/pan,\n * GROUP mute/solo (both layers toggle together), and a single delete that\n * removes the whole pair.\n *\n * The slider represents WHERE the crossfade happens. In this phase it is\n * centered and non-functional (omit `onSliderChange` → it renders disabled); a\n * later phase wires it to fade origin→target across the bars.\n *\n * @since SDK 2.22.0\n */\nimport React from 'react';\nimport { TrackRow } from './TrackRow';\nimport { ConfirmDialog } from './ConfirmDialog';\nimport { EMPTY_FX_DETAIL_STATE } from '../types/fx-toggle.types';\nimport type { TrackLevelsHandle } from '../hooks/useTrackLevels';\nimport type { CrossfadeSlot } from '../crossfade-meta';\n\n/** One layer (engine track) of a crossfade pair. */\nexport interface CrossfadeLayer {\n /** Engine track id of this layer's track (also the meter key). */\n trackId: string;\n /** Display name of this layer's (newly created) track. */\n name: string;\n /** Musical role (same for both layers — crossfades are same-role). */\n role?: string;\n /** Name of the SOURCE track this layer was cloned from (origin/target scene). */\n sourceName?: string;\n /** Human label of the copied preset/sound, shown in the caption. */\n soundLabel?: string;\n /** Playback state for this layer. */\n runtimeState: { muted: boolean; solo: boolean; volume: number; pan: number };\n}\n\nexport interface CrossfadeTrackRowProps {\n /** Top layer — wears the origin (from) scene track's preset. */\n origin: CrossfadeLayer;\n /** Bottom layer — wears the target (to) scene track's preset. */\n target: CrossfadeLayer;\n /** Crossfade position 0..1 (0 = all origin, 1 = all target). Defaults centered. */\n sliderPos?: number;\n /** Toggle mute on BOTH layers together (group mute). */\n onMuteToggle: () => void;\n /** Toggle solo on BOTH layers together (group solo). */\n onSoloToggle: () => void;\n /** Change one layer's volume (per-layer). */\n onVolumeChange: (slot: CrossfadeSlot, volume: number) => void;\n /** Change one layer's pan (per-layer). */\n onPanChange: (slot: CrossfadeSlot, pan: number) => void;\n /** Delete the whole pair. */\n onDelete: () => void;\n /** Move the crossfade point. Omit to render the slider read-only (phase 1). */\n onSliderChange?: (pos: number) => void;\n /** Shared meter handle (welds a peak meter to each layer). */\n levels?: TrackLevelsHandle;\n /** Left-border accent. Defaults to transition purple. */\n accentColor?: string;\n}\n\nfunction LayerCaption({ tag, layer }: { tag: string; layer: CrossfadeLayer }): React.ReactElement {\n return (\n <div className=\"flex items-center gap-1.5 min-w-0 px-2 py-0.5\">\n <span className=\"text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0\">{tag}</span>\n <span className=\"text-[11px] text-sas-text truncate\" title={layer.sourceName ?? layer.name}>\n {layer.sourceName ?? layer.name}\n </span>\n {layer.soundLabel && (\n <span className=\"text-[9px] text-sas-muted/60 truncate flex-shrink-0\" title={layer.soundLabel}>\n · {layer.soundLabel}\n </span>\n )}\n </div>\n );\n}\n\nexport function CrossfadeTrackRow({\n origin,\n target,\n sliderPos = 0.5,\n onMuteToggle,\n onSoloToggle,\n onVolumeChange,\n onPanChange,\n onDelete,\n onSliderChange,\n levels,\n accentColor = '#9333EA',\n}: CrossfadeTrackRowProps): React.ReactElement {\n const [confirmDelete, setConfirmDelete] = React.useState(false);\n\n // A locked crossfade layer. The inner track's `name` is suppressed (the\n // meaningful name lives in the caption); every sound/generation handler is\n // omitted so shuffle / Create / preset-pick / FX / drawer / delete never\n // render. Mute/solo are GROUP-wired (same handler on both layers); volume/pan\n // are per-layer.\n const renderLayer = (layer: CrossfadeLayer, slot: CrossfadeSlot, tag: string): React.ReactElement => (\n <TrackRow\n track={{ id: layer.trackId, name: '', role: layer.role }}\n runtimeState={layer.runtimeState}\n fxDetailState={EMPTY_FX_DETAIL_STATE}\n drawerOpen={false}\n drawerTab=\"fx\"\n levels={levels}\n accentColor={accentColor}\n contentSlot={<LayerCaption tag={tag} layer={layer} />}\n onMuteToggle={onMuteToggle}\n onSoloToggle={onSoloToggle}\n onVolumeChange={(v: number) => onVolumeChange(slot, v)}\n onPanChange={(p: number) => onPanChange(slot, p)}\n />\n );\n\n return (\n <div\n data-testid=\"crossfade-track-row\"\n className=\"w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden\"\n style={{ borderLeftColor: accentColor, borderLeftWidth: '3px' }}\n >\n {/* Header — crossfade label + single delete for the whole pair. */}\n <div className=\"flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60\">\n <span className=\"text-[10px] font-bold uppercase tracking-wide\" style={{ color: accentColor }}>\n ⇄ Crossfade\n </span>\n <button\n data-testid=\"crossfade-delete-button\"\n onClick={() => setConfirmDelete(true)}\n className=\"text-sas-danger/70 hover:text-sas-danger px-1 transition-colors text-sm\"\n title=\"Delete crossfade pair\"\n aria-label=\"Delete crossfade pair\"\n >\n x\n </button>\n </div>\n\n {renderLayer(origin, 'origin', 'Origin')}\n\n {/* Crossfade slider — represents WHERE origin fades into target. Read-only\n (disabled) until the functional fader ships. */}\n <div className=\"flex items-center gap-2 px-3 py-1.5\" data-testid=\"crossfade-slider-row\">\n <span\n className=\"text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0\"\n title={origin.sourceName ?? origin.name}\n >\n {origin.sourceName ?? origin.name}\n </span>\n <input\n type=\"range\"\n data-testid=\"crossfade-slider\"\n min={0}\n max={1}\n step={0.01}\n value={sliderPos}\n disabled={!onSliderChange}\n onChange={\n onSliderChange\n ? (e: React.ChangeEvent<HTMLInputElement>) => onSliderChange(Number(e.target.value))\n : undefined\n }\n style={{ accentColor }}\n className=\"flex-1 disabled:opacity-60 disabled:cursor-not-allowed\"\n aria-label=\"Crossfade position\"\n />\n <span\n className=\"text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0\"\n title={target.sourceName ?? target.name}\n >\n {target.sourceName ?? target.name}\n </span>\n </div>\n\n {renderLayer(target, 'target', 'Target')}\n\n <ConfirmDialog\n open={confirmDelete}\n title=\"Delete crossfade?\"\n message={\n <>\n This crossfade pair (both layers) will be permanently removed from this scene. This cannot\n be undone.\n </>\n }\n confirmLabel=\"Delete\"\n onConfirm={() => {\n setConfirmDelete(false);\n onDelete();\n }}\n onCancel={() => setConfirmDelete(false)}\n testIdPrefix=\"crossfade-delete-confirm\"\n />\n </div>\n );\n}\n\nexport default CrossfadeTrackRow;\n","/**\n * Crossfade-pair metadata — family-agnostic types + parsing shared by every\n * generator panel that supports transition crossfades (synth / drum / instrument).\n *\n * A crossfade pair is two normal tracks linked by a shared `groupId`, persisted\n * in scene plugin_data under `track:<dbId>:crossfade`. Both members play the\n * same MIDI; one wears the origin preset, the other the target preset. The panel\n * owns the family-specific create flow (how a preset/sample is copied) and the\n * render; this module owns only the shape + the scene-data → pairs parse so the\n * logic can't drift across the three panels.\n *\n * @since SDK 2.23.0\n */\n\n/** Which half of the pair a per-layer control / member targets. */\nexport type CrossfadeSlot = 'origin' | 'target';\n\n/**\n * Equal-power center gain (~-3 dB, 1/√2) applied to BOTH crossfade layers so a\n * centered, non-functional slider already sounds like a midpoint blend. The\n * per-layer volume sliders start here; a later phase's fader drives them.\n */\nexport const EQUAL_POWER_GAIN = 0.707;\n\n/**\n * Per-member crossfade metadata (one scene-data value per member track). The two\n * members (origin/target) of a pair share a `groupId`.\n */\nexport interface CrossfadeMeta {\n groupId: string;\n slot: CrossfadeSlot;\n /** DB id of the partner member track. */\n partnerDbId: string;\n /** DB id of the SOURCE track this layer's preset/sample was copied from. */\n sourceTrackDbId: string;\n /** DB id of the scene the source track lives in (the from/to scene). */\n sourceSceneId: string;\n /** Source track display name (shown in the caption). */\n sourceName: string;\n /** Copied preset/sample label (shown in the caption). */\n soundLabel: string;\n /** Crossfade position 0..1 (kept identical on both members). */\n sliderPos: number;\n}\n\n/** A complete crossfade pair (both members present), keyed by groupId. */\nexport interface CrossfadePairMeta {\n groupId: string;\n sliderPos: number;\n originDbId: string;\n targetDbId: string;\n originSourceName: string;\n originSoundLabel: string;\n targetSourceName: string;\n targetSoundLabel: string;\n}\n\n/** Narrow an unknown scene-data value to CrossfadeMeta (defensive — survives partial blobs). */\nexport function asCrossfadeMeta(val: unknown): CrossfadeMeta | null {\n if (!val || typeof val !== 'object') return null;\n const m = val as Partial<CrossfadeMeta>;\n if (typeof m.groupId !== 'string' || (m.slot !== 'origin' && m.slot !== 'target')) return null;\n if (typeof m.partnerDbId !== 'string') return null;\n return {\n groupId: m.groupId,\n slot: m.slot,\n partnerDbId: m.partnerDbId,\n sourceTrackDbId: typeof m.sourceTrackDbId === 'string' ? m.sourceTrackDbId : '',\n sourceSceneId: typeof m.sourceSceneId === 'string' ? m.sourceSceneId : '',\n sourceName: typeof m.sourceName === 'string' ? m.sourceName : '',\n soundLabel: typeof m.soundLabel === 'string' ? m.soundLabel : '',\n sliderPos: typeof m.sliderPos === 'number' ? m.sliderPos : 0.5,\n };\n}\n\n/**\n * Scan all `track:<dbId>:crossfade` keys in a scene's plugin_data and assemble\n * COMPLETE pairs (both origin + target present). A half-broken group (partner\n * deleted underneath) is omitted, so its surviving member falls back to a normal\n * row instead of vanishing.\n */\nexport function parseCrossfadePairs(sceneData: Record<string, unknown>): CrossfadePairMeta[] {\n const groups = new Map<\n string,\n { origin?: { dbId: string; meta: CrossfadeMeta }; target?: { dbId: string; meta: CrossfadeMeta } }\n >();\n for (const [key, val] of Object.entries(sceneData)) {\n const match = /^track:(.+):crossfade$/.exec(key);\n if (!match) continue;\n const meta = asCrossfadeMeta(val);\n if (!meta) continue;\n const dbId = match[1];\n const g = groups.get(meta.groupId) ?? {};\n if (meta.slot === 'origin') g.origin = { dbId, meta };\n else g.target = { dbId, meta };\n groups.set(meta.groupId, g);\n }\n const pairs: CrossfadePairMeta[] = [];\n for (const [groupId, g] of groups) {\n if (!g.origin || !g.target) continue;\n pairs.push({\n groupId,\n sliderPos: g.origin.meta.sliderPos,\n originDbId: g.origin.dbId,\n targetDbId: g.target.dbId,\n originSourceName: g.origin.meta.sourceName,\n originSoundLabel: g.origin.meta.soundLabel,\n targetSourceName: g.target.meta.sourceName,\n targetSoundLabel: g.target.meta.soundLabel,\n });\n }\n return pairs;\n}\n\n// ============================================================================\n// Crossfade volume automation (Phase 3 — the functional fader)\n// ============================================================================\n\n/** One volume-automation point: a dB value at a time offset (seconds from clip start). */\nexport interface VolumeAutomationPoint {\n time: number; // seconds\n db: number; // gain in dB (-80 ≈ silent, 0 = unity)\n}\n\n/** Origin + target volume curves for one crossfade pair. */\nexport interface CrossfadeVolumeCurves {\n origin: VolumeAutomationPoint[];\n target: VolumeAutomationPoint[];\n}\n\nconst FADE_FLOOR_DB = -80;\n\nfunction gainToDb(gain: number): number {\n return gain <= 1e-4 ? FADE_FLOOR_DB : Math.max(FADE_FLOOR_DB, 20 * Math.log10(gain));\n}\n\n/**\n * Equal-power crossfade volume curves over a transition of `bars` at `bpm`.\n * The ORIGIN layer fades OUT and the TARGET fades IN; `sliderPos` (0..1) sets\n * WHERE in time the equal-power (-3 dB) crossover sits — 0 = hand off near the\n * start, 1 = hold the origin until near the end. Points span the clip window\n * [0, durationSeconds] so the engine re-reads them each loop (re-fade per loop).\n * `steps`+1 points with linear interpolation approximate the cos/sin curve.\n *\n * Returns dB point arrays for `host.setTrackVolumeAutomation` — origin on the top\n * layer, target on the bottom. @since SDK 2.25.0\n */\nexport function buildCrossfadeVolumeCurves(\n bars: number,\n bpm: number,\n sliderPos: number,\n steps = 32,\n): CrossfadeVolumeCurves {\n const durationSeconds = (bars * 4 * 60) / Math.max(1, bpm);\n // Keep the crossover off the exact ends so there's always an actual fade.\n const s = Math.min(0.98, Math.max(0.02, sliderPos));\n const round = (n: number): number => Math.round(n * 1000) / 1000;\n const origin: VolumeAutomationPoint[] = [];\n const target: VolumeAutomationPoint[] = [];\n for (let i = 0; i <= steps; i++) {\n const x = i / steps; // normalized time 0..1\n const time = round(x * durationSeconds);\n // Piecewise-linear angle so the equal-power crossover (π/4) lands at x = s.\n const theta = x <= s ? (x / s) * (Math.PI / 4) : Math.PI / 4 + ((x - s) / (1 - s)) * (Math.PI / 4);\n origin.push({ time, db: Math.round(gainToDb(Math.cos(theta)) * 100) / 100 });\n target.push({ time, db: Math.round(gainToDb(Math.sin(theta)) * 100) / 100 });\n }\n return { origin, target };\n}\n","/**\n * Crossfade MIDI inpainting — builds the LLM user-prompt for a bridge that\n * MORPHS the ORIGIN part into the TARGET part.\n *\n * A normal scene generation composes a part standalone from the scene's chords.\n * A crossfade bridge is different: it is INPAINTING between two fixed endpoints.\n * The generated part must begin feeling continuous with the origin pattern and\n * end feeling continuous with the target pattern, transforming between them\n * across the transition's bars.\n *\n * The harmonic frame — Key / mode / BPM / bars / the transition chord\n * progression (with beat timing) / scene contract — is injected AUTOMATICALLY by\n * `host.generateWithLLM` (it prepends the active scene's \"Musical Context\" block\n * unless `skipContextPrefix` is set). So this prompt does NOT restate key/bpm/\n * chords — it adds only the two endpoint patterns + the morph instructions, and\n * references the harmonic frame as \"given above\".\n *\n * REPRESENTATION (researched for Gemini): ABC notation is the LLM-native format\n * for melodic generation, but it's weak for percussion, would need a separate\n * output parser (our output is JSON note-events, already proven with Gemini),\n * and an inpainting task wants input/output FORMAT SYMMETRY. So each endpoint is\n * given as the exact JSON note-events PLUS a pitch-named, bar-structured \"gloss\"\n * — the transferable wins from the research (pitch NAMES over raw MIDI numbers,\n * explicit bar/beat structure) layered on the precise, symmetric JSON. Drums\n * (uniform pitch) get a rhythmic gloss instead of pitch names.\n *\n * This changes only the LLM INPUT framing: the OUTPUT schema is unchanged, so the\n * calling panel keeps its system prompt + parser (and, for drums, its flatten step).\n *\n * @since SDK 2.24.0\n */\nimport type { PluginMidiNote } from './types/plugin-sdk.types';\n\nexport interface CrossfadeInpaintInput {\n /** Musical role of the bridge part (e.g. 'bass'). '' falls back to \"melodic\". */\n role: string;\n /** Transition length in bars (the morph timeline). */\n bars: number;\n /** Display name of the ORIGIN source track (the part the bridge begins from). */\n originName: string;\n /** Display name of the TARGET source track (the part the bridge arrives at). */\n targetName: string;\n /** ORIGIN source scene's key label (e.g. \"G minor\"). Null/omitted = unknown. */\n originKey?: string | null;\n /** TARGET source scene's key label. Null/omitted = unknown. */\n targetKey?: string | null;\n /** ORIGIN pattern notes (beat-based; from the FROM scene). May be empty. */\n originNotes: readonly PluginMidiNote[];\n /** TARGET pattern notes (beat-based; from the TO scene). May be empty. */\n targetNotes: readonly PluginMidiNote[];\n /** Drums: pitch is uniform (flattened), so gloss RHYTHM instead of pitch names. */\n percussive?: boolean;\n}\n\nconst PITCH_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];\n\n/** Round to 3 dp, dropping trailing-zero noise. */\nfunction round3(n: number): number {\n return Math.round(n * 1000) / 1000;\n}\n\n/** MIDI number → scientific pitch name (60 → C4), the app's octave convention. */\nfunction pitchName(p: number): string {\n return `${PITCH_NAMES[((p % 12) + 12) % 12]}${Math.floor(p / 12) - 1}`;\n}\n\n/** Compact a note to the 4 fields the LLM needs (drops channel), beats rounded. */\nfunction compactNote(n: PluginMidiNote): { pitch: number; startBeat: number; durationBeats: number; velocity: number } {\n return { pitch: n.pitch, startBeat: round3(n.startBeat), durationBeats: round3(n.durationBeats), velocity: n.velocity };\n}\n\n/** One-line shape summary so the LLM grasps register/density before the detail. */\nfunction summarize(notes: readonly PluginMidiNote[], percussive: boolean): string {\n if (notes.length === 0) return 'empty (no notes)';\n const span = round3(Math.max(...notes.map((n) => n.startBeat + n.durationBeats)));\n if (percussive) return `${notes.length} hits, spans ~${span} beats`;\n const pitches = notes.map((n) => n.pitch);\n return `${notes.length} notes, ${pitchName(Math.min(...pitches))}–${pitchName(Math.max(...pitches))}, spans ~${span} beats`;\n}\n\n/** Pitch-named (melodic) or rhythmic (drums) gloss, grouped by inferred bar. */\nfunction gloss(notes: readonly PluginMidiNote[], percussive: boolean): string {\n const sorted = [...notes].sort((a, b) => a.startBeat - b.startBeat);\n const maxEnd = Math.max(...sorted.map((n) => n.startBeat + n.durationBeats));\n const bars = Math.max(1, Math.ceil(maxEnd / 4));\n const lines: string[] = [];\n for (let b = 0; b < bars; b++) {\n const inBar = sorted.filter((n) => n.startBeat >= b * 4 && n.startBeat < (b + 1) * 4);\n if (inBar.length === 0) continue;\n const body = percussive\n ? inBar.map((n) => `${round3(n.startBeat)}(v${n.velocity})`).join(' ')\n : inBar.map((n) => `${pitchName(n.pitch)}@${round3(n.startBeat)}`).join(' ');\n lines.push(` Bar ${b + 1}: ${body}`);\n }\n return lines.join('\\n');\n}\n\nfunction patternBlock(\n label: string,\n name: string,\n key: string | null | undefined,\n notes: readonly PluginMidiNote[],\n percussive: boolean,\n): string {\n const keyLabel = key ? ` in ${key}` : '';\n const header = `${label} — \"${name}\"${keyLabel} (${summarize(notes, percussive)}):`;\n if (notes.length === 0) return `${header}\\n (no notes — treat this end as open)`;\n return `${header}\\n${gloss(notes, percussive)}\\n exact JSON: ${JSON.stringify(notes.map(compactNote))}`;\n}\n\n/**\n * Build the inpainting user-prompt. The result is the prompt BODY only — pass it\n * as `request.user` to `host.generateWithLLM` with the panel's normal system\n * prompt and `responseFormat: 'json'`; the harmonic context auto-prefixes.\n */\nexport function buildCrossfadeInpaintPrompt(input: CrossfadeInpaintInput): string {\n const { role, bars, originName, targetName, originKey, targetKey, originNotes, targetNotes } = input;\n const percussive = input.percussive ?? false;\n const part = role || (percussive ? 'drum' : 'melodic');\n const modulation =\n originKey && targetKey\n ? originKey === targetKey\n ? `stays in ${targetKey}`\n : `modulates from ${originKey} toward ${targetKey}`\n : 'resolves toward the destination key';\n\n const lines: string[] = [\n `TASK — TRANSITION BRIDGE (musical inpainting).`,\n `Compose a ${part} part that MORPHS from the ORIGIN pattern into the TARGET pattern across the ${bars} bars`,\n `of this transition. The Key / BPM / chord progression are given above — it ${modulation}; honour that`,\n `frame, don't restate it. Each pattern below is shown as a pitch/rhythm gloss for musicality plus its exact`,\n `JSON; output your bridge in the same JSON note schema (per the system prompt).`,\n ``,\n patternBlock('ORIGIN pattern (where the bridge BEGINS)', originName, originKey, originNotes, percussive),\n ``,\n patternBlock('TARGET pattern (where the bridge must ARRIVE)', targetName, targetKey, targetNotes, percussive),\n ``,\n `Requirements:`,\n `- The FIRST bar feels continuous with the ORIGIN — borrow its register, rhythm, and contour so the seam`,\n ` from the previous scene is seamless.`,\n `- Across the middle bars, gradually transform toward the TARGET (shift register / rhythm / motifs step by step).`,\n `- The LAST bar lands on the TARGET's material and resolves onto the destination chord, so the seam into the`,\n ` next scene is seamless.`,\n `- Stay within the transition chord progression above; favour chord tones at the bar boundaries.`,\n `- This is inpainting between two FIXED endpoints — a listener should not be able to point to where the`,\n ` origin ends or the target begins.`,\n ];\n\n if (originNotes.length === 0 || targetNotes.length === 0) {\n lines.push(\n ``,\n originNotes.length === 0 && targetNotes.length === 0\n ? `(Both endpoints are empty — compose a short ${part} bridge from the chords alone.)`\n : originNotes.length === 0\n ? `(The ORIGIN is empty — begin sparse and grow INTO the TARGET.)`\n : `(The TARGET is empty — begin from the ORIGIN and dissolve toward the destination chord.)`,\n );\n }\n\n return lines.join('\\n');\n}\n","/**\n * ImportTrackModal — \"import a track from another scene\" picker (SDK component).\n *\n * Shared by all five generator panels (drums / instruments / synths / loops /\n * stems). Self-fetching: given the scoped `host`, it calls\n * `host.listImportableTracks()` to enumerate candidates (already filtered to\n * the calling panel's type and gate-annotated by the host) and\n * `host.importTrack()` to perform the copy. The UI only renders `importable` +\n * `disabledReason` — it never computes the harmonic/length/tempo gate itself.\n *\n * Two-step picker: choose a source scene, then a track in it. Incompatible\n * tracks render disabled with a reason tooltip (never hidden), per product\n * decision.\n *\n * @since SDK 2.13.0\n */\n\nimport React, { useCallback, useEffect, useState } from 'react';\nimport { Modal } from './Modal';\nimport type {\n PluginHost,\n ImportCandidateScene,\n ImportCandidateTrack,\n PluginTrackHandle,\n} from '../types/plugin-sdk.types';\n\nexport interface ImportTrackModalProps {\n /** Scoped host — the modal calls listImportableTracks / importTrack itself. */\n host: PluginHost;\n /** Controls visibility (the panel owns open/closed from its header button). */\n open: boolean;\n /** Close handler (Escape, backdrop, Cancel, or after a successful import). */\n onClose: () => void;\n /** Fired after a successful import with the new track handle. */\n onImported: (handle: PluginTrackHandle) => void;\n /** Optional modal title (default names the whole-track import). */\n title?: string;\n /** data-testid prefix so each panel's modal is addressable in tests. */\n testIdPrefix?: string;\n /**\n * 'track' (default) imports a whole track via `importTrack`. 'sound' copies\n * ONLY the sound onto an existing track: every candidate is selectable (the\n * contract gate is ignored) and the chosen track is handed back via `onPick`\n * instead of being imported — the panel applies it via `host.getTrackSound`.\n */\n mode?: 'track' | 'sound';\n /** Sound-mode pick handler — required when `mode='sound'`. */\n onPick?: (sel: { sourceTrackDbId: string; trackName: string; sceneName: string }) => void | Promise<void>;\n /**\n * Cross-panel port handler (track mode). When provided, the modal also lists\n * the ACTIVE scene's tracks owned by OTHER panels as a `sameScene` group —\n * shown first and selected by default — and routes a pick there to this\n * callback instead of `importTrack`. The panel re-sounds the part on its own\n * instrument (create track → copy MIDI → load native sound). @since SDK 2.20.0\n */\n onPortTrack?: (sel: { sourceTrackDbId: string; trackName: string; role?: string }) => void | Promise<void>;\n}\n\ntype LoadState =\n | { status: 'loading' }\n | { status: 'error'; message: string }\n | { status: 'ready'; scenes: ImportCandidateScene[] };\n\nexport function ImportTrackModal({\n host,\n open,\n onClose,\n onImported,\n title = 'Import track from scene (must match contract)',\n testIdPrefix = 'import-track',\n mode = 'track',\n onPick,\n onPortTrack,\n}: ImportTrackModalProps): React.ReactElement | null {\n const [load, setLoad] = useState<LoadState>({ status: 'loading' });\n const [selectedSceneId, setSelectedSceneId] = useState<string | null>(null);\n const [importingTrackId, setImportingTrackId] = useState<string | null>(null);\n\n const refresh = useCallback(async (): Promise<void> => {\n if (!host.listImportableTracks) {\n setLoad({ status: 'error', message: 'This host does not support importing tracks.' });\n return;\n }\n setLoad({ status: 'loading' });\n try {\n // Track mode with a port handler also wants the \"this scene — other\n // panels\" group (cross-panel re-sound source); plain/sound flows don't.\n const wantsPort = mode === 'track' && !!onPortTrack;\n const scenes = await host.listImportableTracks(wantsPort ? { includeSameScene: true } : undefined);\n setLoad({ status: 'ready', scenes });\n // Default to the same-scene group when present so the user lands on\n // cross-panel tracks (they can ← back to pick another scene).\n const sameScene = scenes.find((s) => s.sameScene);\n if (sameScene) setSelectedSceneId(sameScene.sceneId);\n } catch (err: unknown) {\n setLoad({ status: 'error', message: err instanceof Error ? err.message : 'Failed to load scenes.' });\n }\n }, [host, mode, onPortTrack]);\n\n // Fetch candidates each time the modal opens; reset selection on close.\n useEffect(() => {\n if (open) {\n setSelectedSceneId(null);\n setImportingTrackId(null);\n void refresh();\n }\n }, [open, refresh]);\n\n const handleImport = useCallback(\n async (\n track: ImportCandidateTrack,\n sourceSceneId: string,\n sceneName: string,\n isSameScene: boolean,\n ): Promise<void> => {\n // Same-scene, other-panel pick: re-sound the part on THIS panel's\n // instrument. The panel creates a track, copies the MIDI, and loads its\n // own sound (see onPortTrack) — never a faithful copy / importTrack.\n if (isSameScene && onPortTrack) {\n if (!track.importable) return;\n setImportingTrackId(track.trackId);\n try {\n await onPortTrack({ sourceTrackDbId: track.dbId, trackName: track.name, role: track.role });\n onClose();\n } catch (err: unknown) {\n host.showToast?.('error', err instanceof Error ? err.message : 'Import failed');\n setImportingTrackId(null);\n }\n return;\n }\n // Sound mode: ignore the gate and hand the pick back to the panel, which\n // reads the source sound via host.getTrackSound and applies it itself.\n if (mode === 'sound') {\n setImportingTrackId(track.trackId);\n try {\n await onPick?.({ sourceTrackDbId: track.dbId, trackName: track.name, sceneName });\n onClose();\n } catch (err: unknown) {\n host.showToast?.('error', err instanceof Error ? err.message : 'Import failed');\n setImportingTrackId(null);\n }\n return;\n }\n if (!track.importable || !host.importTrack) return;\n setImportingTrackId(track.trackId);\n try {\n const handle = await host.importTrack({ sourceSceneId, sourceTrackId: track.trackId });\n onImported(handle);\n onClose();\n } catch (err: unknown) {\n host.showToast?.('error', err instanceof Error ? err.message : 'Import failed');\n setImportingTrackId(null);\n }\n },\n [host, onImported, onClose, mode, onPick, onPortTrack],\n );\n\n if (!open) return null;\n\n const scenes = load.status === 'ready' ? load.scenes : [];\n const selectedScene = scenes.find((s) => s.sceneId === selectedSceneId) ?? null;\n\n return (\n <Modal open={open} onClose={onClose} testIdPrefix={testIdPrefix}>\n <div\n className=\"w-[420px] max-h-[70vh] overflow-hidden flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl\"\n onClick={(e) => e.stopPropagation()}\n data-testid={`${testIdPrefix}-modal`}\n >\n {/* Header */}\n <div className=\"flex items-center justify-between px-3 py-2 border-b border-sas-border\">\n <div className=\"flex items-center gap-2\">\n {selectedScene && (\n <button\n className=\"text-sas-muted hover:text-sas-accent text-xs\"\n onClick={() => setSelectedSceneId(null)}\n data-testid={`${testIdPrefix}-back`}\n >\n ←\n </button>\n )}\n <span className=\"text-sm font-medium text-sas-text\">\n {selectedScene ? selectedScene.sceneName : title}\n </span>\n </div>\n <button\n className=\"text-sas-muted hover:text-sas-accent text-sm\"\n onClick={onClose}\n data-testid={`${testIdPrefix}-close`}\n >\n ✕\n </button>\n </div>\n\n {/* Body */}\n <div className=\"overflow-y-auto p-2 flex-1\">\n {load.status === 'loading' && (\n <div className=\"py-8 text-center text-xs text-sas-muted\" data-testid={`${testIdPrefix}-loading`}>\n Loading scenes…\n </div>\n )}\n\n {load.status === 'error' && (\n <div className=\"py-8 text-center text-xs text-red-400\" data-testid={`${testIdPrefix}-error`}>\n {load.message}\n </div>\n )}\n\n {load.status === 'ready' && scenes.length === 0 && (\n <div className=\"py-8 text-center text-xs text-sas-muted\" data-testid={`${testIdPrefix}-empty`}>\n {mode === 'sound'\n ? 'No other scenes have a sound to import.'\n : 'No other scenes have a compatible track to import.'}\n </div>\n )}\n\n {/* Scene list */}\n {load.status === 'ready' && scenes.length > 0 && !selectedScene && (\n <ul className=\"flex flex-col gap-1\" data-testid={`${testIdPrefix}-scene-list`}>\n {scenes.map((scene) => (\n <li key={scene.sceneId}>\n <button\n className=\"w-full flex items-center justify-between px-2 py-1.5 rounded-sm border border-sas-border bg-sas-panel-alt text-left text-xs text-sas-text hover:border-sas-accent hover:text-sas-accent transition-colors\"\n onClick={() => setSelectedSceneId(scene.sceneId)}\n data-testid={`${testIdPrefix}-scene`}\n >\n <span className=\"truncate\">{scene.sceneName}</span>\n <span className=\"text-sas-muted\">{scene.tracks.length} →</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n\n {/* Track list */}\n {selectedScene && (\n <ul className=\"flex flex-col gap-1\" data-testid={`${testIdPrefix}-track-list`}>\n {selectedScene.tracks.map((track) => {\n const busy = importingTrackId === track.trackId;\n // Sound mode ignores the contract gate — every candidate is a\n // valid sound source. Track mode honors `importable`.\n const gated = mode === 'track' && !track.importable;\n const disabled = gated || busy;\n return (\n <li key={track.dbId}>\n <button\n className={`w-full flex items-center justify-between px-2 py-1.5 rounded-sm border text-left text-xs transition-colors ${\n disabled\n ? 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n : 'bg-sas-panel-alt border-sas-border text-sas-text hover:border-sas-accent hover:text-sas-accent'\n }`}\n disabled={disabled}\n title={gated ? track.disabledReason : undefined}\n onClick={() => void handleImport(track, selectedScene.sceneId, selectedScene.sceneName, !!selectedScene.sameScene)}\n data-testid={`${testIdPrefix}-track`}\n data-importable={mode === 'sound' || track.importable ? 'true' : 'false'}\n >\n <span className=\"truncate\">\n {track.name}\n {track.role ? <span className=\"text-sas-muted\"> · {track.role}</span> : null}\n </span>\n {busy ? (\n <span className=\"text-sas-muted\">…</span>\n ) : gated ? (\n <span className=\"text-sas-muted\">⊘</span>\n ) : null}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n </div>\n </Modal>\n );\n}\n","/**\n * CrossfadeModal — \"add a crossfade track\" picker for a transition scene.\n *\n * Shown only inside a `scene_type='transition'` scene. The user picks an ORIGIN\n * track (from the transition's FROM scene) and a TARGET track (from its TO\n * scene). Crossfades are same-role: once an origin is chosen, the target\n * dropdown is filtered to the origin's role.\n *\n * Self-fetching: given the scoped `host`, it calls `host.listSceneFamilyTracks`\n * for both scenes (ungated — a transition deliberately bridges different keys).\n * It does NOT build the pair itself; it hands the two selections to `onCreate`,\n * which the panel implements (create two tracks, generate one shared MIDI clip,\n * copy each preset). `onCreate` should reject on failure so the modal can show\n * it and stay open.\n *\n * @since SDK 2.22.0\n */\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { Modal } from './Modal';\nimport type { PluginHost, SceneFamilyTrack } from '../types/plugin-sdk.types';\n\n/** A picked source track handed to `onCreate`. */\nexport interface CrossfadeSelection {\n /** Source track DB id (selector for getTrackSound + crossfade metadata). */\n dbId: string;\n /** Display name (for the row caption). */\n name: string;\n /** Musical role (same for both — enforced by the picker). */\n role?: string;\n}\n\nexport interface CrossfadeModalProps {\n /** Scoped host — the modal calls listSceneFamilyTracks itself. */\n host: PluginHost;\n /** Controls visibility (the panel owns open/closed from its header button). */\n open: boolean;\n /** DB id of the transition's FROM (origin) scene. */\n fromSceneId: string;\n /** DB id of the transition's TO (target) scene. */\n toSceneId: string;\n /** Display name for the origin scene heading (optional). */\n fromSceneName?: string;\n /** Display name for the target scene heading (optional). */\n toSceneName?: string;\n /** Close handler (Escape, backdrop, Cancel, or after a successful create). */\n onClose: () => void;\n /** Build the crossfade pair. Should reject on failure so the modal shows it. */\n onCreate: (origin: CrossfadeSelection, target: CrossfadeSelection) => Promise<void>;\n /** data-testid prefix. */\n testIdPrefix?: string;\n}\n\ntype LoadState =\n | { status: 'loading' }\n | { status: 'error'; message: string }\n | { status: 'ready'; origin: SceneFamilyTrack[]; target: SceneFamilyTrack[] };\n\nexport function CrossfadeModal({\n host,\n open,\n fromSceneId,\n toSceneId,\n fromSceneName,\n toSceneName,\n onClose,\n onCreate,\n testIdPrefix = 'crossfade-modal',\n}: CrossfadeModalProps): React.ReactElement | null {\n const [load, setLoad] = useState<LoadState>({ status: 'loading' });\n const [originDbId, setOriginDbId] = useState<string>('');\n const [targetDbId, setTargetDbId] = useState<string>('');\n const [isCreating, setIsCreating] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const cancelRef = useRef<HTMLButtonElement>(null);\n\n const refresh = useCallback(async (): Promise<void> => {\n if (!host.listSceneFamilyTracks) {\n setLoad({ status: 'error', message: 'This host does not support crossfade tracks.' });\n return;\n }\n setLoad({ status: 'loading' });\n try {\n const [origin, target] = await Promise.all([\n host.listSceneFamilyTracks(fromSceneId),\n host.listSceneFamilyTracks(toSceneId),\n ]);\n setLoad({ status: 'ready', origin, target });\n setOriginDbId(origin[0]?.dbId ?? '');\n } catch (err: unknown) {\n setLoad({ status: 'error', message: err instanceof Error ? err.message : 'Failed to load tracks.' });\n }\n }, [host, fromSceneId, toSceneId]);\n\n // Fetch on open; reset state.\n useEffect(() => {\n if (open) {\n setError(null);\n setIsCreating(false);\n setOriginDbId('');\n setTargetDbId('');\n void refresh();\n }\n }, [open, refresh]);\n\n const originTrack = useMemo(\n () => (load.status === 'ready' ? load.origin.find((t) => t.dbId === originDbId) ?? null : null),\n [load, originDbId],\n );\n const originRole = originTrack?.role;\n\n // Same-role pairing: target candidates filtered to the origin's role. If the\n // origin track has no role at all, don't over-filter (degenerate case).\n const targetCandidates = useMemo(() => {\n if (load.status !== 'ready') return [];\n if (!originRole) return load.target;\n return load.target.filter((t) => t.role === originRole);\n }, [load, originRole]);\n\n // Keep the target selection valid as the origin (and thus the filter) changes.\n useEffect(() => {\n if (!targetCandidates.some((t) => t.dbId === targetDbId)) {\n setTargetDbId(targetCandidates[0]?.dbId ?? '');\n }\n }, [targetCandidates, targetDbId]);\n\n const targetTrack = targetCandidates.find((t) => t.dbId === targetDbId) ?? null;\n const canCreate = !isCreating && !!originTrack && !!targetTrack;\n\n const handleClose = useCallback((): void => {\n if (!isCreating) onClose();\n }, [isCreating, onClose]);\n\n const handleCreate = useCallback(async (): Promise<void> => {\n if (!originTrack || !targetTrack) return;\n setIsCreating(true);\n setError(null);\n try {\n await onCreate(\n { dbId: originTrack.dbId, name: originTrack.name, role: originTrack.role },\n { dbId: targetTrack.dbId, name: targetTrack.name, role: targetTrack.role },\n );\n onClose();\n } catch (err: unknown) {\n setError(err instanceof Error ? err.message : 'Failed to create crossfade.');\n setIsCreating(false);\n }\n }, [originTrack, targetTrack, onCreate, onClose]);\n\n if (!open) return null;\n\n return (\n <Modal open={open} onClose={handleClose} testIdPrefix={testIdPrefix} initialFocusRef={cancelRef}>\n <div\n className=\"bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3\"\n onClick={(e: React.MouseEvent) => e.stopPropagation()}\n data-testid={`${testIdPrefix}-box`}\n >\n <h3 className=\"text-sm font-bold text-sas-text\">Add crossfade</h3>\n <p className=\"text-[11px] text-sas-muted leading-relaxed\">\n Bridge a track from{' '}\n <span className=\"text-sas-text\">{fromSceneName ?? 'the origin scene'}</span> into one from{' '}\n <span className=\"text-sas-text\">{toSceneName ?? 'the target scene'}</span>. Both layers share\n one generated part; each keeps its own preset.\n </p>\n\n {load.status === 'loading' && (\n <div className=\"text-xs text-sas-muted py-4 text-center\">Loading tracks…</div>\n )}\n {load.status === 'error' && (\n <div className=\"text-xs text-sas-danger py-4 text-center\">{load.message}</div>\n )}\n {load.status === 'ready' &&\n (load.origin.length === 0 ? (\n <div\n className=\"text-xs text-sas-muted py-4 text-center\"\n data-testid={`${testIdPrefix}-empty-origin`}\n >\n No matching tracks in the origin scene. Add one there first.\n </div>\n ) : (\n <>\n <label className=\"block\">\n <span className=\"text-[10px] uppercase tracking-wide text-sas-muted\">Origin (top)</span>\n <select\n data-testid={`${testIdPrefix}-origin-select`}\n value={originDbId}\n onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setOriginDbId(e.target.value)}\n disabled={isCreating}\n className=\"sas-input w-full mt-0.5 text-xs\"\n >\n {load.origin.map((t) => (\n <option key={t.dbId} value={t.dbId}>\n {t.name}\n {t.role ? ` · ${t.role}` : ''}\n </option>\n ))}\n </select>\n </label>\n\n <label className=\"block\">\n <span className=\"text-[10px] uppercase tracking-wide text-sas-muted\">\n Target (bottom){originRole ? ` · ${originRole}` : ''}\n </span>\n {targetCandidates.length === 0 ? (\n <div className=\"text-xs text-sas-danger mt-0.5\" data-testid={`${testIdPrefix}-empty-target`}>\n No {originRole ?? 'matching'} track in the target scene to crossfade into.\n </div>\n ) : (\n <select\n data-testid={`${testIdPrefix}-target-select`}\n value={targetDbId}\n onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setTargetDbId(e.target.value)}\n disabled={isCreating}\n className=\"sas-input w-full mt-0.5 text-xs\"\n >\n {targetCandidates.map((t) => (\n <option key={t.dbId} value={t.dbId}>\n {t.name}\n {t.role ? ` · ${t.role}` : ''}\n </option>\n ))}\n </select>\n )}\n </label>\n </>\n ))}\n\n {error && (\n <div className=\"text-xs text-sas-danger\" data-testid={`${testIdPrefix}-error`}>\n {error}\n </div>\n )}\n\n <div className=\"flex justify-end gap-2 pt-1\">\n <button\n ref={cancelRef}\n data-testid={`${testIdPrefix}-cancel`}\n onClick={onClose}\n disabled={isCreating}\n className=\"px-3 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-text disabled:opacity-50\"\n >\n Cancel\n </button>\n <button\n data-testid={`${testIdPrefix}-confirm`}\n onClick={handleCreate}\n disabled={!canCreate}\n className={`px-3 py-1 text-xs rounded-sm border transition-colors ${\n canCreate\n ? 'bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg'\n : 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n }`}\n >\n {isCreating ? 'Generating bridge…' : 'Create crossfade'}\n </button>\n </div>\n </div>\n </Modal>\n );\n}\n\nexport default CrossfadeModal;\n","/**\n * DownloadPackButton — versioned-pack download trigger (SDK component).\n *\n * Parameterized by `packId`; drives the download through the host\n * (`host.startSamplePackDownload` / `host.onSamplePackProgress`) so plugins\n * never reach into the app's IPC (`window.electronAPI`). Two display variants:\n * - 'compact' (default) — small uppercase button for panel headers\n * - 'large' — bigger CTA used inside SamplePackCTACard\n *\n * @since SDK 2.8.0 (moved from the app and refactored onto PluginHost).\n */\n\nimport React, { useCallback, useEffect, useState } from 'react';\nimport type { PluginHost } from '../types/plugin-sdk.types';\n\nexport type DownloadPackButtonVariant = 'compact' | 'large';\n\ntype PackDownloadStatus =\n | 'idle'\n | 'downloading'\n | 'verifying'\n | 'extracting'\n | 'installing'\n | 'complete'\n | 'error';\n\nexport interface DownloadPackButtonProps {\n /** Host the plugin received; drives the download + progress. */\n host: PluginHost;\n packId: string;\n /** Pack display name, e.g. 'Drum Sample Library'. Used in tooltips/labels. */\n displayName: string;\n /** Bundle size in bytes (shown in the large-variant label). */\n sizeBytes?: number;\n variant?: DownloadPackButtonVariant;\n /** Called once after the install completes (status === 'complete'). */\n onDownloadComplete?: () => void;\n}\n\n// Base-1024 (GiB/MiB) to match the host's own SamplePackDownloader formatter and\n// the `_pack-version.json` / sample-packs.ts size comments (e.g. a 28.5e9-byte\n// instrument bundle reads as \"26.6 GB\", not the decimal \"28.5 GB\").\nfunction formatSize(bytes?: number): string {\n if (!bytes || bytes <= 0) return '';\n const gb = bytes / 1024 ** 3;\n if (gb >= 1) return `${gb.toFixed(1)} GB`;\n const mb = bytes / 1024 ** 2;\n return `${Math.round(mb)} MB`;\n}\n\nexport const DownloadPackButton: React.FC<DownloadPackButtonProps> = ({\n host,\n packId,\n displayName,\n sizeBytes,\n variant = 'compact',\n onDownloadComplete,\n}) => {\n const [status, setStatus] = useState<PackDownloadStatus>('idle');\n const [progress, setProgress] = useState(0);\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n\n useEffect(() => {\n const unsub = host.onSamplePackProgress(packId, (p) => {\n setStatus(p.status as PackDownloadStatus);\n setProgress(p.progress);\n if (p.status === 'error') {\n setErrorMessage(p.message || 'Download failed');\n } else if (p.status === 'complete') {\n setErrorMessage(null);\n setTimeout(() => onDownloadComplete?.(), 250);\n } else {\n setErrorMessage(null);\n }\n });\n return unsub;\n }, [host, packId, onDownloadComplete]);\n\n const handleClick = useCallback(async (): Promise<void> => {\n if (status !== 'idle' && status !== 'error') return;\n try {\n setStatus('downloading');\n setProgress(0);\n setErrorMessage(null);\n const result = await host.startSamplePackDownload(packId);\n if (!result.success) {\n setStatus('error');\n setErrorMessage(result.error || 'Download failed');\n }\n } catch (err) {\n console.error('[DownloadPackButton] start failed:', err);\n setStatus('error');\n setErrorMessage(err instanceof Error ? err.message : String(err));\n }\n }, [host, packId, status]);\n\n const isWorking =\n status === 'downloading' ||\n status === 'verifying' ||\n status === 'extracting' ||\n status === 'installing';\n const isDisabled = isWorking || status === 'complete';\n\n const buttonLabel = (() => {\n switch (status) {\n case 'downloading':\n return `${progress}%`;\n case 'verifying':\n return 'Verifying...';\n case 'extracting':\n return 'Extracting...';\n case 'installing':\n return 'Installing...';\n case 'complete':\n return 'Done!';\n case 'error':\n return 'Retry';\n default:\n return variant === 'large'\n ? `Download ${displayName}${sizeBytes ? ` (${formatSize(sizeBytes)})` : ''}`\n : 'Download';\n }\n })();\n\n const tooltip = (() => {\n if (status === 'error') return errorMessage || 'Download failed. Click to retry.';\n if (isWorking) return `${buttonLabel} — ${displayName}`;\n if (status === 'complete') return 'Installation complete';\n return `Download ${displayName}${sizeBytes ? ` (${formatSize(sizeBytes)})` : ''}`;\n })();\n\n const baseClasses =\n variant === 'large'\n ? 'px-4 py-2 text-sm font-medium rounded border transition-colors'\n : 'px-2 py-0.5 text-[10px] uppercase tracking-wide rounded-sm border transition-colors';\n\n let className: string;\n if (status === 'error') {\n className = `${baseClasses} text-red-400 border-red-400/50 hover:text-red-300 hover:border-red-300`;\n } else if (status === 'complete') {\n className = `${baseClasses} text-green-400 border-green-400/50`;\n } else if (isDisabled) {\n className = `${baseClasses} text-sas-accent border-sas-accent/50 cursor-wait`;\n } else {\n className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;\n }\n\n return (\n <div>\n <button\n data-testid={`download-pack-button-${packId}`}\n onClick={handleClick}\n disabled={isDisabled}\n className={className}\n title={tooltip}\n >\n {buttonLabel}\n </button>\n {variant === 'large' && status === 'error' && errorMessage && (\n <div className=\"text-xs text-sas-danger mt-2\" data-testid={`download-pack-error-${packId}`}>\n {errorMessage}\n </div>\n )}\n </div>\n );\n};\n\nexport default DownloadPackButton;\n","/**\n * SamplePackCTACard — empty-state card a generator panel renders when its\n * sample pack is missing OR a newer version is available. Wraps\n * DownloadPackButton in a centered card. The completion callback should\n * re-fetch pack status on the parent so the card unmounts and the normal panel\n * UI takes over.\n *\n * @since SDK 2.8.0 (moved from the app; download driven through PluginHost).\n */\n\nimport React from 'react';\nimport type { PluginHost } from '../types/plugin-sdk.types';\nimport { DownloadPackButton } from './DownloadPackButton';\n\nexport type SamplePackCTACardStatus = 'missing' | 'stale' | 'checking';\n\n/** Minimal pack info the card needs. A PackConfig is structurally compatible. */\nexport interface SamplePackCardInfo {\n packId: string;\n displayName: string;\n description: string;\n sizeBytes?: number;\n}\n\nexport interface SamplePackCTACardProps {\n /** Host the plugin received; drives the download. */\n host: PluginHost;\n pack: SamplePackCardInfo;\n status: SamplePackCTACardStatus;\n onDownloadComplete?: () => void;\n}\n\nexport const SamplePackCTACard: React.FC<SamplePackCTACardProps> = ({\n host,\n pack,\n status,\n onDownloadComplete,\n}) => {\n if (status === 'checking') {\n return (\n <div\n data-testid={`sample-pack-cta-checking-${pack.packId}`}\n className=\"flex items-center justify-center py-16 text-sas-muted text-sm\"\n >\n Checking sample library...\n </div>\n );\n }\n\n const headline =\n status === 'stale'\n ? `${pack.displayName} update available`\n : `${pack.displayName} not installed`;\n\n const sublabel =\n status === 'stale'\n ? `A newer version is available for download.`\n : pack.description;\n\n return (\n <div\n data-testid={`sample-pack-cta-${pack.packId}`}\n className=\"flex flex-col items-center justify-center py-12 px-6 text-center\"\n >\n <div className=\"text-sm uppercase tracking-wide text-sas-muted mb-2\">\n {status === 'stale' ? 'Update available' : 'Sample library not installed'}\n </div>\n <div className=\"text-base text-sas-text mb-1\">{headline}</div>\n <div className=\"text-xs text-sas-muted mb-6 max-w-md\">{sublabel}</div>\n <DownloadPackButton\n host={host}\n packId={pack.packId}\n displayName={pack.displayName}\n sizeBytes={pack.sizeBytes}\n variant=\"large\"\n onDownloadComplete={onDownloadComplete}\n />\n </div>\n );\n};\n\nexport default SamplePackCTACard;\n","/**\n * WaveformView — small canvas waveform for an audio file on disk.\n *\n * Reads bytes via `host.getAudioFileBytes`, decodes via\n * `AudioContext.decodeAudioData`, computes peaks, and renders to a\n * canvas. Suitable for take rows, sample previews, or any place a\n * decorative ~40px waveform makes sense.\n *\n * The component is self-contained: it owns the AudioContext and the\n * peak buffer, decodes once per `filePath` change, and tears down on\n * unmount. Failures (file missing, decode error) render as a silent\n * blank canvas — the caller can decide how to surface errors.\n */\n\nimport React, { useEffect, useRef, useState } from 'react';\nimport type { PluginHost } from '../types/plugin-sdk.types';\nimport { computePeaks, drawWaveform, type WaveformPeaks } from './waveform';\n\nexport interface WaveformViewProps {\n host: PluginHost;\n filePath: string;\n /** Number of bins to compute. Default 256 — plenty for ~40px tall rows. */\n bins?: number;\n /** Tailwind / inline className for sizing. Default: w-full h-10. */\n className?: string;\n /** Override the bar fill style (e.g., to match a track color). */\n fillStyle?: string;\n /**\n * If set, the bin range spans `targetSamples` instead of the file's\n * actual length. Bins beyond the audio render as flat silence — used\n * to align a partial recording inside a full-loop-width canvas so\n * every take row has the same time scale.\n */\n targetSamples?: number;\n}\n\nexport const WaveformView: React.FC<WaveformViewProps> = ({\n host,\n filePath,\n bins = 256,\n className,\n fillStyle,\n targetSamples,\n}) => {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const [peaks, setPeaks] = useState<WaveformPeaks | null>(null);\n\n // Decode + compute peaks whenever the file changes.\n useEffect(() => {\n let cancelled = false;\n let audioContext: AudioContext | null = null;\n\n (async () => {\n try {\n const bytes = await host.getAudioFileBytes(filePath);\n if (cancelled) return;\n\n // OfflineAudioContext would be cheaper but its constructor needs\n // sampleRate/length up front — we don't know them until decode.\n const ContextCtor: typeof AudioContext =\n (window as unknown as { AudioContext?: typeof AudioContext }).AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext!;\n audioContext = new ContextCtor();\n\n // decodeAudioData mutates / detaches the buffer in some impls,\n // so pass a copy.\n const audioBuffer = await audioContext.decodeAudioData(bytes.slice(0));\n if (cancelled) return;\n\n const computed = computePeaks(audioBuffer, bins, targetSamples);\n setPeaks(computed);\n } catch (err) {\n // Silent: the canvas stays blank. Caller can layer their own\n // error UI on top if needed.\n console.warn('[WaveformView] failed to decode', filePath, err);\n } finally {\n if (audioContext) {\n audioContext.close().catch(() => { /* ignore */ });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [host, filePath, bins, targetSamples]);\n\n // Repaint whenever peaks update — including layout-driven resizes via\n // ResizeObserver so the canvas stays crisp at the current CSS width.\n useEffect(() => {\n if (!peaks) return;\n const canvas = canvasRef.current;\n if (!canvas) return;\n drawWaveform(canvas, peaks, fillStyle ? { fillStyle } : undefined);\n\n const observer = new ResizeObserver(() => {\n drawWaveform(canvas, peaks, fillStyle ? { fillStyle } : undefined);\n });\n observer.observe(canvas);\n return () => observer.disconnect();\n }, [peaks, fillStyle]);\n\n return (\n <canvas\n ref={canvasRef}\n data-testid=\"waveform-view\"\n className={className ?? 'w-full h-10'}\n />\n );\n};\n\nexport default WaveformView;\n","/**\n * Shared waveform peaks + canvas drawer.\n *\n * Originally inlined in `stems/TrimEditorDrawer.tsx`; lifted to\n * this module so the recorder plugin's per-take rows can render the\n * same compact min/max display without duplicating the math.\n *\n * Design:\n * - `computePeaks` reduces an AudioBuffer to `bins` min/max pairs (mono\n * average across channels). Output layout is interleaved\n * `[min0, max0, min1, max1, ...]` so the renderer reads pairs\n * sequentially without index arithmetic.\n * - `drawWaveform` paints one 1px vertical bar per canvas column,\n * dpr-aware so it stays crisp on retina displays.\n *\n * No host or React dependencies — pure functions are safe to use from\n * tests, web workers, or non-React renderers.\n */\n\nexport interface WaveformPeaks {\n /** Sample rate of the source file (used to convert sample → seconds). */\n sampleRate: number;\n /** Total length of the raw file in samples. */\n totalSamples: number;\n /** Min/max pairs per bin (length = bins × 2). */\n peaks: Float32Array;\n}\n\n/**\n * Reduce an AudioBuffer to `bins` min/max pairs. Mono averages across\n * channels. The output buffer is fixed-size (`bins * 2`) for fast canvas\n * traversal.\n *\n * `targetSamples` (optional) extends the bin range to a fixed sample\n * count larger than the buffer's actual length — bins falling beyond\n * the buffer get (0, 0) pairs, which renders as a flat tail. Used by\n * the recorder so a partial last chunk's waveform sits at the start of\n * a full-loop-width canvas instead of being stretched to fill.\n */\nexport function computePeaks(\n audioBuffer: AudioBuffer,\n bins: number,\n targetSamples?: number\n): WaveformPeaks {\n const { length, numberOfChannels, sampleRate } = audioBuffer;\n const channels: Float32Array[] = [];\n for (let c = 0; c < numberOfChannels; c++) {\n channels.push(audioBuffer.getChannelData(c));\n }\n const totalForBinning =\n typeof targetSamples === 'number' && targetSamples > length ? targetSamples : length;\n const samplesPerBin = Math.max(1, Math.floor(totalForBinning / bins));\n const out = new Float32Array(bins * 2);\n for (let i = 0; i < bins; i++) {\n const startIdx = i * samplesPerBin;\n const endIdx = Math.min(length, startIdx + samplesPerBin);\n if (startIdx >= length) {\n // Bin falls entirely past the audio's end — render as silence.\n out[i * 2] = 0;\n out[i * 2 + 1] = 0;\n continue;\n }\n let mn = Infinity;\n let mx = -Infinity;\n for (let j = startIdx; j < endIdx; j++) {\n let v = 0;\n for (let c = 0; c < numberOfChannels; c++) {\n v += channels[c][j];\n }\n v /= numberOfChannels;\n if (v < mn) mn = v;\n if (v > mx) mx = v;\n }\n if (!Number.isFinite(mn)) mn = 0;\n if (!Number.isFinite(mx)) mx = 0;\n out[i * 2] = mn;\n out[i * 2 + 1] = mx;\n }\n return { sampleRate, totalSamples: totalForBinning, peaks: out };\n}\n\n/**\n * Draw min/max peaks to the given canvas. Resizes the canvas backing\n * store to CSS pixels × devicePixelRatio so the result is crisp on\n * retina. Caller controls CSS sizing via the `<canvas>` element's\n * className.\n */\nexport function drawWaveform(\n canvas: HTMLCanvasElement,\n peaks: WaveformPeaks,\n options: { fillStyle?: string } = {}\n): void {\n const dpr = window.devicePixelRatio || 1;\n const cssWidth = canvas.clientWidth;\n const cssHeight = canvas.clientHeight;\n if (cssWidth === 0 || cssHeight === 0) return;\n canvas.width = Math.floor(cssWidth * dpr);\n canvas.height = Math.floor(cssHeight * dpr);\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n ctx.scale(dpr, dpr);\n ctx.clearRect(0, 0, cssWidth, cssHeight);\n ctx.fillStyle = options.fillStyle ?? 'rgba(255, 255, 255, 0.4)';\n\n const bins = peaks.peaks.length / 2;\n const mid = cssHeight / 2;\n for (let x = 0; x < cssWidth; x++) {\n const binIdx = Math.floor((x / cssWidth) * bins);\n const mn = peaks.peaks[binIdx * 2];\n const mx = peaks.peaks[binIdx * 2 + 1];\n const yTop = mid - mx * mid;\n const yBot = mid - mn * mid;\n ctx.fillRect(x, yTop, 1, Math.max(1, yBot - yTop));\n }\n}\n","/**\n * ScrollingWaveform — live waveform during recording (Phase 8.10).\n *\n * Reads the platform's `peakDb` history and renders it as a horizontal\n * bar-graph that scrolls left as new samples arrive. Two halves: top\n * band shows positive amplitude, bottom band mirrors it (matches the\n * static waveform's min/max layout in `WaveformView`).\n *\n * The data source is a function the caller supplies — typically a ref\n * to the `inputLevelDb` value from `AudioRoutingContext` polled at\n * ~30Hz. The component samples that ref via requestAnimationFrame and\n * shifts a fixed-size float ring buffer one column per frame.\n *\n * Pure presentational + animation logic; no IPC. Stops animating\n * when `active` is false (engine isn't running the audio callback).\n */\n\nimport React, { useEffect, useRef } from 'react';\n\nexport interface ScrollingWaveformProps {\n /** Function returning the latest peak in dBFS. Called per RAF. */\n getPeakDb: () => number;\n /** True while the audio callback is running; false freezes the wave. */\n active: boolean;\n /** Number of horizontal columns in the ring buffer. */\n columns?: number;\n /** Optional className for sizing. */\n className?: string;\n /** Highlight color for the wave. */\n fillStyle?: string;\n}\n\nexport const ScrollingWaveform: React.FC<ScrollingWaveformProps> = ({\n getPeakDb,\n active,\n columns = 256,\n className,\n fillStyle,\n}) => {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const ringRef = useRef<Float32Array>(new Float32Array(columns));\n const writeIdxRef = useRef(0);\n const rafRef = useRef<number | null>(null);\n\n // Recreate the ring buffer if `columns` changes — preserve any data\n // that fits.\n useEffect(() => {\n if (ringRef.current.length !== columns) {\n const next = new Float32Array(columns);\n const prev = ringRef.current;\n const copyLen = Math.min(prev.length, columns);\n // Copy the tail of the previous buffer into the head of the new one.\n for (let i = 0; i < copyLen; i++) {\n next[i] = prev[i];\n }\n ringRef.current = next;\n writeIdxRef.current = writeIdxRef.current % columns;\n }\n }, [columns]);\n\n useEffect(() => {\n if (!active) {\n // Freeze the wave but leave the existing buffer on screen.\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n return;\n }\n\n const tick = (): void => {\n const peakDb = getPeakDb();\n // Map dBFS → normalised amplitude [0, 1]. -60dB → 0, 0dB → 1.\n const amp =\n peakDb <= -120\n ? 0\n : Math.max(0, Math.min(1, (peakDb + 60) / 60));\n const ring = ringRef.current;\n ring[writeIdxRef.current] = amp;\n writeIdxRef.current = (writeIdxRef.current + 1) % ring.length;\n\n // Draw.\n const canvas = canvasRef.current;\n if (canvas) {\n const dpr = window.devicePixelRatio || 1;\n const cssW = canvas.clientWidth;\n const cssH = canvas.clientHeight;\n if (cssW > 0 && cssH > 0) {\n if (canvas.width !== Math.floor(cssW * dpr) || canvas.height !== Math.floor(cssH * dpr)) {\n canvas.width = Math.floor(cssW * dpr);\n canvas.height = Math.floor(cssH * dpr);\n }\n const ctx = canvas.getContext('2d');\n if (ctx) {\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, cssW, cssH);\n ctx.fillStyle = fillStyle ?? '#6af2c5';\n const mid = cssH / 2;\n const cols = ring.length;\n const colW = cssW / cols;\n // Read the ring oldest → newest so the wave scrolls left.\n const start = writeIdxRef.current; // oldest sample\n for (let x = 0; x < cols; x++) {\n const ringIdx = (start + x) % cols;\n const a = ring[ringIdx];\n const half = a * mid;\n ctx.fillRect(x * colW, mid - half, Math.max(1, colW), Math.max(1, half * 2));\n }\n }\n }\n }\n rafRef.current = requestAnimationFrame(tick);\n };\n rafRef.current = requestAnimationFrame(tick);\n\n return () => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n };\n }, [active, getPeakDb, fillStyle]);\n\n return (\n <canvas\n ref={canvasRef}\n data-testid=\"scrolling-waveform\"\n className={className ?? 'w-full h-12'}\n />\n );\n};\n\nexport default ScrollingWaveform;\n","/**\n * OffsetScrubber — manual sample-offset slider for Lyria-generated audio.\n *\n * Renders a thin horizontal track with one tick per detected beat (tall\n * tick on the downbeat) and a draggable thumb. Drag distance maps to a\n * sample offset that is applied to the audio clip via\n * `host.setAudioOffsetSamples(trackId, n)`.\n *\n * Snap behavior:\n * - Default: snap to the nearest beat in `cuePoints.beats`.\n * - Hold Shift: bypass snap (free 1-sample resolution).\n * - Click on a tick mark: jump to that beat exactly.\n *\n * The visible range is one bar (= meter beats) on each side of bar 1.\n * For a 4-bar / 4/4 clip at 44100 Hz, one bar at 120 BPM is 88_200\n * samples — so the slider covers ±88_200 samples, ~2 s either way. That\n * matches the alignment errors we observe from Lyria detection misses\n * (typically <1 beat off).\n *\n * BPM mismatch chip: shown when `cuePoints.detected_bpm` is more than\n * 1 BPM away from the project BPM, since the beat ticks won't line up\n * with the project grid in that case.\n */\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { PluginCuePoints } from '../types/plugin-sdk.types';\n\nconst SLIDER_HEIGHT_PX = 28;\nconst TICK_HEIGHT_PX = 14;\nconst DOWNBEAT_TICK_HEIGHT_PX = 22;\nconst THUMB_WIDTH_PX = 4;\n\nexport interface OffsetScrubberProps {\n /** Detected beat positions + sample rate. Slider is disabled when null. */\n cuePoints: PluginCuePoints | null;\n /** Current offset, in samples (signed). */\n offsetSamples: number;\n /** Project BPM — used to compute the visible range and the mismatch chip. */\n projectBpm: number;\n /** Beats per bar, defaults to 4. */\n meter?: number;\n /** Called on drag-end with the resolved offset (already snapped). */\n onChange: (offsetSamples: number) => void;\n /** Disable interaction (e.g., during generation / split). */\n disabled?: boolean;\n}\n\nexport function OffsetScrubber({\n cuePoints,\n offsetSamples,\n projectBpm,\n meter = 4,\n onChange,\n disabled = false,\n}: OffsetScrubberProps): React.ReactElement {\n const trackRef = useRef<HTMLDivElement | null>(null);\n // Local optimistic offset during drag — committed on mouseup\n const [draftOffset, setDraftOffset] = useState<number>(offsetSamples);\n const [isDragging, setIsDragging] = useState(false);\n\n // Keep the draft synced with the parent prop when not dragging.\n useEffect(() => {\n if (!isDragging) setDraftOffset(offsetSamples);\n }, [offsetSamples, isDragging]);\n\n // Range is ±1 bar of samples around the downbeat.\n // beats are 60 / bpm seconds; bar = meter beats.\n const sampleRate = cuePoints?.sample_rate ?? 44100;\n const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;\n const beatsForRange = useMemo(() => {\n // Use the project BPM for the visible range so the slider scale\n // matches what the user is editing against in the timeline.\n return Math.round((60 / projectBpm) * sampleRate);\n }, [projectBpm, sampleRate]);\n const rangeSamples = beatsForRange * meter; // ±1 bar\n\n // Map a sample offset to a 0..1 position on the slider track.\n const sampleToFraction = useCallback(\n (sample: number): number => {\n const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));\n return (clamped + rangeSamples) / (2 * rangeSamples);\n },\n [rangeSamples],\n );\n\n const fractionToSample = useCallback(\n (fraction: number): number => {\n const clamped = Math.max(0, Math.min(1, fraction));\n return Math.round(clamped * 2 * rangeSamples - rangeSamples);\n },\n [rangeSamples],\n );\n\n // Snap a candidate sample to the nearest detected beat. Beats are\n // CuePoints.beats positions (relative to clip start). Offset slider\n // semantics: positive = shift clip later; we map offset onto the\n // beats array so the user lines up the desired beat with bar 1.\n //\n // Implementation: each beat[i] corresponds to a candidate offset\n // value of `beats[i] - beats[0]` (the relative distance the user has\n // shifted the clip). Snap to the nearest such candidate.\n const snapTargets = useMemo(() => {\n if (!cuePoints || cuePoints.beats.length === 0) return [];\n const downbeat = cuePoints.beats[0];\n // Snap candidates: differences between every beat and the downbeat\n // (positive shifts) plus their negation (negative shifts). De-dup +\n // sort so binary search is cheap if the array gets large.\n const positives = cuePoints.beats.map((b) => b - downbeat);\n const negatives = positives.slice(1).map((p) => -p); // skip 0 to avoid dupe\n return [...negatives, ...positives].sort((a, b) => a - b);\n }, [cuePoints]);\n\n const snapToBeat = useCallback(\n (sample: number): number => {\n if (snapTargets.length === 0) return sample;\n // Linear scan — beats[] is small (≤ 16 for v1). Switch to binary\n // search if we ever generate longer clips.\n let best = snapTargets[0];\n let bestDist = Math.abs(sample - best);\n for (const t of snapTargets) {\n const d = Math.abs(sample - t);\n if (d < bestDist) {\n best = t;\n bestDist = d;\n }\n }\n return best;\n },\n [snapTargets],\n );\n\n // Drag handler — pointer events let us track outside the element.\n const handlePointerDown = useCallback(\n (e: React.PointerEvent<HTMLDivElement>): void => {\n if (disabled || !cuePoints) return;\n e.preventDefault();\n const track = trackRef.current;\n if (!track) return;\n track.setPointerCapture(e.pointerId);\n setIsDragging(true);\n\n const updateFromEvent = (clientX: number, shiftHeld: boolean): number => {\n const rect = track.getBoundingClientRect();\n const fraction = (clientX - rect.left) / rect.width;\n const raw = fractionToSample(fraction);\n return shiftHeld ? raw : snapToBeat(raw);\n };\n\n // Apply the initial click position immediately.\n setDraftOffset(updateFromEvent(e.clientX, e.shiftKey));\n\n const onMove = (ev: PointerEvent): void => {\n setDraftOffset(updateFromEvent(ev.clientX, ev.shiftKey));\n };\n const onUp = (ev: PointerEvent): void => {\n const final = updateFromEvent(ev.clientX, ev.shiftKey);\n track.releasePointerCapture(e.pointerId);\n track.removeEventListener('pointermove', onMove);\n track.removeEventListener('pointerup', onUp);\n track.removeEventListener('pointercancel', onUp);\n setIsDragging(false);\n setDraftOffset(final);\n onChange(final);\n };\n\n track.addEventListener('pointermove', onMove);\n track.addEventListener('pointerup', onUp);\n track.addEventListener('pointercancel', onUp);\n },\n [disabled, cuePoints, fractionToSample, onChange, snapToBeat],\n );\n\n // Reset to 0 (downbeat-aligned) — handy \"snap to bar 1\" button.\n const handleResetToZero = useCallback((): void => {\n if (disabled) return;\n setDraftOffset(0);\n onChange(0);\n }, [disabled, onChange]);\n\n const thumbFraction = sampleToFraction(draftOffset);\n const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;\n\n // BPM mismatch — show a chip when detected BPM diverges from project.\n const bpmMismatch = cuePoints?.detected_bpm != null\n && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;\n\n // Render tick marks for each beat in the snap-target list. Convert\n // sample → fraction → percent for CSS positioning.\n const ticks = useMemo(() => {\n if (!cuePoints) return [];\n const downbeat = cuePoints.beats[0] ?? 0;\n return cuePoints.beats.map((b, i) => {\n const offsetCandidate = b - downbeat;\n const fraction = sampleToFraction(offsetCandidate);\n const isDownbeat = i === 0;\n return { i, fraction, isDownbeat };\n });\n }, [cuePoints, sampleToFraction]);\n\n const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;\n\n return (\n <div data-testid=\"offset-scrubber\" className=\"flex items-center gap-2 w-full\">\n <span className=\"text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0\">\n Align\n </span>\n <div\n ref={trackRef}\n data-testid=\"offset-scrubber-track\"\n onPointerDown={handlePointerDown}\n className={`relative flex-1 min-w-0 rounded-sm select-none ${\n isDisabled\n ? 'bg-sas-panel cursor-not-allowed opacity-40'\n : 'bg-sas-bg cursor-pointer'\n }`}\n style={{ height: SLIDER_HEIGHT_PX }}\n title={\n isDisabled\n ? 'Generate audio first to enable offset alignment'\n : 'Drag to align beat 1. Hold Shift for free, no-snap movement.'\n }\n role=\"slider\"\n aria-label=\"Audio offset alignment\"\n aria-valuemin={-rangeSamples}\n aria-valuemax={rangeSamples}\n aria-valuenow={draftOffset}\n aria-disabled={isDisabled}\n >\n {/* Center marker — bar 1 / beat 1 reference line */}\n <div\n aria-hidden=\"true\"\n className=\"absolute top-0 bottom-0 w-px bg-sas-accent/40\"\n style={{ left: '50%' }}\n />\n {/* Beat ticks */}\n {ticks.map((t) => (\n <div\n key={t.i}\n data-testid={t.isDownbeat ? 'offset-tick-downbeat' : 'offset-tick'}\n aria-hidden=\"true\"\n className={t.isDownbeat ? 'absolute bg-sas-accent' : 'absolute bg-sas-muted/50'}\n style={{\n left: `${(t.fraction * 100).toFixed(2)}%`,\n top: (SLIDER_HEIGHT_PX - (t.isDownbeat ? DOWNBEAT_TICK_HEIGHT_PX : TICK_HEIGHT_PX)) / 2,\n width: 1,\n height: t.isDownbeat ? DOWNBEAT_TICK_HEIGHT_PX : TICK_HEIGHT_PX,\n }}\n />\n ))}\n {/* Thumb */}\n <div\n data-testid=\"offset-scrubber-thumb\"\n aria-hidden=\"true\"\n className={`absolute top-0 bottom-0 rounded-sm ${\n isDragging ? 'bg-sas-accent' : 'bg-sas-accent/80'\n }`}\n style={{\n left: thumbLeftPct,\n width: THUMB_WIDTH_PX,\n transform: 'translateX(-50%)',\n pointerEvents: 'none',\n }}\n />\n </div>\n {/* Numeric readout — samples + millisecond equivalent */}\n <span\n data-testid=\"offset-scrubber-readout\"\n className=\"text-[10px] text-sas-muted/70 tabular-nums flex-shrink-0 min-w-[64px] text-right\"\n >\n {formatOffset(draftOffset, sampleRate)}\n </span>\n {/* Reset button (snap back to 0) */}\n <button\n type=\"button\"\n data-testid=\"offset-scrubber-reset\"\n onClick={handleResetToZero}\n disabled={isDisabled || draftOffset === 0}\n className={`text-[10px] px-1 py-0.5 rounded-sm border transition-colors flex-shrink-0 ${\n isDisabled || draftOffset === 0\n ? 'border-sas-border text-sas-muted/30 cursor-not-allowed'\n : 'border-sas-border text-sas-muted/70 hover:border-sas-accent hover:text-sas-accent'\n }`}\n title=\"Reset offset to 0 (bar 1)\"\n >\n ⌖\n </button>\n {bpmMismatch && (\n <span\n data-testid=\"offset-bpm-mismatch\"\n className=\"text-[9px] px-1 py-0.5 rounded-sm bg-amber-500/15 text-amber-400 border border-amber-500/30 flex-shrink-0\"\n title={`Detected ${detectedBpm.toFixed(1)} BPM — beats may not align with project ${projectBpm} BPM grid`}\n >\n BPM ≠\n </span>\n )}\n </div>\n );\n}\n\n/** Format an offset in samples as `+12345 spl (+279 ms)` for the readout. */\nfunction formatOffset(samples: number, sampleRate: number): string {\n const sign = samples > 0 ? '+' : samples < 0 ? '-' : '';\n const abs = Math.abs(samples);\n const ms = Math.round((abs / sampleRate) * 1000);\n return `${sign}${abs} spl (${sign}${ms} ms)`;\n}\n\nexport default OffsetScrubber;\n","/**\n * WAV peak analyzer (Phase 8.10).\n *\n * Reads a WAV file via the plugin host, decodes it via Web Audio,\n * scans every channel for the absolute maximum sample, and returns\n * peak dBFS + a clipped flag (true when the peak >= -1dBFS, matching\n * the engine's hard-limiter ceiling).\n *\n * Used by the recorder's take rows to surface \"this take peaked at\n * -8dB\" or \"this take CLIPPED\" without the user having to click play.\n */\n\nimport type { PluginHost } from '../types/plugin-sdk.types';\n\nexport interface PeakAnalysis {\n peakLinear: number;\n peakDb: number;\n clipped: boolean;\n}\n\n/** Threshold matching the engine's -1dBFS hard limiter ceiling. */\nconst CLIP_THRESHOLD_LINEAR = 0.891;\n\nexport async function analyzeWavPeak(\n host: PluginHost,\n filePath: string\n): Promise<PeakAnalysis> {\n const bytes = await host.getAudioFileBytes(filePath);\n const ContextCtor: typeof AudioContext =\n (window as unknown as { AudioContext?: typeof AudioContext }).AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext!;\n const audioContext = new ContextCtor();\n try {\n const audioBuffer = await audioContext.decodeAudioData(bytes.slice(0));\n let peak = 0;\n for (let c = 0; c < audioBuffer.numberOfChannels; c++) {\n const data = audioBuffer.getChannelData(c);\n for (let i = 0; i < data.length; i++) {\n const a = Math.abs(data[i]);\n if (a > peak) peak = a;\n }\n }\n const peakDb = peak > 1e-6 ? 20 * Math.log10(peak) : -120;\n return {\n peakLinear: peak,\n peakDb,\n clipped: peak >= CLIP_THRESHOLD_LINEAR - 0.005,\n };\n } finally {\n await audioContext.close().catch(() => { /* ignore */ });\n }\n}\n","/**\n * Synthesize a PluginCuePoints object from raw BPM/sample-rate inputs.\n *\n * The OffsetScrubber consumes PluginCuePoints — a beat grid plus\n * per-beat sample positions, normally produced by Lyria's onset\n * detector. The recorder doesn't have detected cue points (live\n * recordings have no detection pass), but it always knows the project\n * BPM, the engine sample rate, and the loop length in bars. That's\n * enough to construct a synthetic grid where every beat sits on a\n * regular interval — which is exactly what the scrubber needs to\n * provide tick marks + snap behavior for nudging the take's offset.\n */\n\nimport type { PluginCuePoints } from '../types/plugin-sdk.types';\n\nexport interface SynthesizeCuePointsOptions {\n bpm: number;\n sampleRate: number;\n /** Total bars in the clip (e.g. 4 for a 4-bar loop). */\n bars: number;\n /** Beats per bar. Defaults to 4 (4/4). */\n meter?: number;\n}\n\nexport function synthesizeCuePoints({\n bpm,\n sampleRate,\n bars,\n meter = 4,\n}: SynthesizeCuePointsOptions): PluginCuePoints {\n const safeBpm = bpm > 0 ? bpm : 120;\n const safeSampleRate = sampleRate > 0 ? sampleRate : 48000;\n const samplesPerBeat = Math.round((60 / safeBpm) * safeSampleRate);\n const totalBeats = Math.max(1, Math.round(bars * meter));\n const beats: number[] = [];\n for (let i = 0; i < totalBeats; i++) {\n beats.push(i * samplesPerBeat);\n }\n return {\n schema: 1,\n sample_rate: safeSampleRate,\n detected_bpm: safeBpm,\n downbeat_sample: 0,\n beats,\n detected_at: new Date().toISOString(),\n };\n}\n","/**\n * useSceneState — Scene-keyed state hook for plugin developers.\n *\n * Works like `useState`, but maintains separate state per scene.\n * When the user switches scenes, the previous scene's state is preserved\n * and restored when they switch back.\n *\n * Returns `[value, setForCurrentScene, setForScene]`:\n * - `value` — state for the currently active scene\n * - `setForCurrentScene(v)` — updates state for whatever scene is active at call time\n * - `setForScene(sceneId, v)` — updates state for a specific scene (for async callbacks)\n *\n * Both setters support the functional updater pattern: `prev => next`.\n *\n * **Important:** For object/array `initialValue`, hoist to a module-level constant\n * to keep the setter callbacks referentially stable:\n * ```ts\n * const EMPTY: string[] = [];\n * const [items, setItems, setItemsForScene] = useSceneState(activeSceneId, EMPTY);\n * ```\n */\n\nimport { useState, useCallback, useRef } from 'react';\n\ntype SetSceneState<T> = (value: T | ((prev: T) => T)) => void;\ntype SetSceneStateForScene<T> = (sceneId: string, value: T | ((prev: T) => T)) => void;\n\nexport function useSceneState<T>(\n activeSceneId: string | null,\n initialValue: T\n): [T, SetSceneState<T>, SetSceneStateForScene<T>] {\n const [stateMap, setStateMap] = useState<Map<string, T>>(() => new Map());\n const activeSceneIdRef = useRef(activeSceneId);\n activeSceneIdRef.current = activeSceneId;\n\n const currentValue = activeSceneId !== null && stateMap.has(activeSceneId)\n ? stateMap.get(activeSceneId)!\n : initialValue;\n\n const setForCurrentScene = useCallback((value: T | ((prev: T) => T)): void => {\n const sid = activeSceneIdRef.current;\n if (sid === null) return;\n setStateMap(prev => {\n const current = prev.has(sid) ? prev.get(sid)! : initialValue;\n const next = typeof value === 'function' ? (value as (prev: T) => T)(current) : value;\n const newMap = new Map(prev);\n newMap.set(sid, next);\n return newMap;\n });\n }, [initialValue]);\n\n const setForScene = useCallback((sceneId: string, value: T | ((prev: T) => T)): void => {\n setStateMap(prev => {\n const current = prev.has(sceneId) ? prev.get(sceneId)! : initialValue;\n const next = typeof value === 'function' ? (value as (prev: T) => T)(current) : value;\n const newMap = new Map(prev);\n newMap.set(sceneId, next);\n return newMap;\n });\n }, [initialValue]);\n\n return [currentValue, setForCurrentScene, setForScene];\n}\n","/**\n * useAnySolo — reactively reports whether ANY track in the project is soloed.\n *\n * Solo is cross-panel: when the user solos a track in ANY panel, the engine's\n * effective-mute model silences every non-soloed track. A panel uses this flag\n * to DIM its own non-soloed rows without lighting their Mute buttons:\n *\n * ```tsx\n * const anySolo = useAnySolo(host);\n * // ...\n * <TrackRow soloedOut={anySolo && !track.runtimeState.solo} ... />\n * ```\n *\n * Refreshes on mount and on every track-state change. `onTrackStateChange`\n * fires for tracks in ALL panels (not just this plugin's), so a solo toggled in\n * another panel updates this flag too.\n */\n\nimport { useEffect, useState } from 'react';\nimport type { PluginHost } from '../types/plugin-sdk.types';\n\nexport function useAnySolo(\n host: Pick<PluginHost, 'isAnySoloActive' | 'onTrackStateChange'>\n): boolean {\n const [anySolo, setAnySolo] = useState(false);\n\n useEffect(() => {\n let active = true;\n const refresh = (): void => {\n host\n .isAnySoloActive()\n .then((v) => {\n if (active) setAnySolo(v);\n })\n .catch(() => {\n /* engine unreachable — leave the flag as-is rather than flicker */\n });\n };\n refresh();\n const unsub = host.onTrackStateChange(() => refresh());\n return () => {\n active = false;\n unsub();\n };\n }, [host]);\n\n return anySolo;\n}\n","/**\n * useSoundHistory — generic, per-track \"what sounds has this track had?\" stack.\n *\n * Powers the drawer \"History\" tab: restore any earlier sound, star favorites,\n * and (via the host plugin) persist across project reopen. The SDK is ignorant\n * of WHAT a sound is — each plugin records an opaque `descriptor` (a drum sample\n * path / an instrument `{ displayName, zones }` / a synth Surge state blob) plus\n * a human `label`, and supplies `applySound` to re-apply a chosen descriptor.\n *\n * Persistence is the plugin's job: pass `opts.onChange` (called after every\n * mutation with the new state) to save, and call `restore()` on load to seed.\n * Favorited entries are never auto-evicted by the cap.\n *\n * Robustness: `applySound` + `onChange` are read through refs, so the returned\n * object is referentially STABLE regardless of whether the caller memoizes them.\n * Plugins list this object in `loadTracks` deps — an unstable return previously\n * caused a render loop, so keep it stable.\n *\n * @since SDK 2.13.0\n */\n\nimport { useCallback, useMemo, useRef, useState } from 'react';\nimport type { SoundHistoryEntry } from '../types/plugin-sdk.types';\n\nexport type { SoundHistoryEntry };\n\n/** A track's ordered sound history plus the index of the currently-applied sound. */\nexport interface TrackSoundHistory {\n entries: readonly SoundHistoryEntry[];\n /** Index into `entries` of the currently-applied sound; -1 when empty. */\n cursor: number;\n}\n\nexport interface UseSoundHistoryOptions {\n /** Max non-favorited entries kept per track (favorites are never evicted). Default 24. */\n max?: number;\n /**\n * Called after every mutation (record/undo/restoreTo/toggleFavorite/clear) with the\n * track's new state — use it to persist. NOT called by `restore()` (that's a load).\n */\n onChange?: (trackId: string, state: TrackSoundHistory) => void;\n}\n\nexport interface UseSoundHistoryResult {\n /** Remember a sound that was just applied (generation, scene-load, or shuffle). */\n record(trackId: string, descriptor: unknown, label: string): void;\n /** Re-apply the sound one step before the current one. Resolves true if it moved. */\n undo(trackId: string): Promise<boolean>;\n /** Re-apply a specific entry by index. Resolves true if it applied. */\n restoreTo(trackId: string, index: number): Promise<boolean>;\n /** The ordered history + cursor for a track (safe empty default). */\n list(trackId: string): TrackSoundHistory;\n /** Whether there is an earlier sound to step back to. */\n canUndo(trackId: string): boolean;\n /** Forget a track's history (e.g. on regenerate). Persists the cleared state. */\n clear(trackId: string): void;\n /** Forget ALL tracks' history in memory (e.g. before re-seeding on scene load). */\n reset(): void;\n /** Seed a track's full history (e.g. from persistence on load). Does NOT fire onChange. */\n restore(\n trackId: string,\n state: { entries?: readonly SoundHistoryEntry[]; cursor?: number } | null | undefined,\n ): void;\n /** Toggle the favorite flag on an entry (favorites survive cap eviction). */\n toggleFavorite(trackId: string, index: number): void;\n}\n\nconst EMPTY: TrackSoundHistory = { entries: [], cursor: -1 };\n\nfunction sameDescriptor(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n try {\n return JSON.stringify(a) === JSON.stringify(b);\n } catch {\n return false;\n }\n}\n\nexport function useSoundHistory(\n applySound: (trackId: string, descriptor: unknown) => Promise<void>,\n opts: UseSoundHistoryOptions = {},\n): UseSoundHistoryResult {\n const max = Math.max(2, opts.max ?? 24);\n\n // Read callbacks through refs so the returned API stays referentially stable\n // even if the caller passes a fresh closure each render.\n const applyRef = useRef(applySound);\n applyRef.current = applySound;\n const onChangeRef = useRef(opts.onChange);\n onChangeRef.current = opts.onChange;\n\n // Authoritative store in a ref (async callbacks read latest); version forces re-render.\n const dataRef = useRef<Record<string, TrackSoundHistory>>({});\n const [, setVersion] = useState(0);\n const bump = useCallback((): void => setVersion((v) => v + 1), []);\n\n // Single writer: update store, re-render, optionally notify for persistence.\n const commit = useCallback(\n (trackId: string, next: TrackSoundHistory, notify: boolean): void => {\n dataRef.current = { ...dataRef.current, [trackId]: next };\n bump();\n if (notify) onChangeRef.current?.(trackId, next);\n },\n [bump],\n );\n\n const record = useCallback(\n (trackId: string, descriptor: unknown, label: string): void => {\n const h = dataRef.current[trackId];\n const current = h && h.cursor >= 0 ? h.entries[h.cursor] : undefined;\n // Ignore re-applying the same sound (no-op shuffles, scene re-seeds).\n if (current && sameDescriptor(current.descriptor, descriptor)) return;\n const entries: SoundHistoryEntry[] = [...(h ? h.entries : []), { descriptor, label }];\n // Cap: evict the OLDEST NON-FAVORITED entry when over the limit (favorites survive).\n while (entries.length > max) {\n const victim = entries.findIndex((e) => !e.favorite);\n if (victim === -1) break; // everything is favorited — keep it all\n entries.splice(victim, 1);\n }\n commit(trackId, { entries, cursor: entries.length - 1 }, true);\n },\n [max, commit],\n );\n\n const restoreTo = useCallback(\n async (trackId: string, index: number): Promise<boolean> => {\n const h = dataRef.current[trackId];\n if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;\n await applyRef.current(trackId, h.entries[index].descriptor);\n commit(trackId, { entries: h.entries, cursor: index }, true);\n return true;\n },\n [commit],\n );\n\n const undo = useCallback(\n (trackId: string): Promise<boolean> => {\n const h = dataRef.current[trackId];\n if (!h || h.cursor <= 0) return Promise.resolve(false);\n return restoreTo(trackId, h.cursor - 1);\n },\n [restoreTo],\n );\n\n const toggleFavorite = useCallback(\n (trackId: string, index: number): void => {\n const h = dataRef.current[trackId];\n if (!h || index < 0 || index >= h.entries.length) return;\n const entries = h.entries.map((e, i) => (i === index ? { ...e, favorite: !e.favorite } : e));\n commit(trackId, { entries, cursor: h.cursor }, true);\n },\n [commit],\n );\n\n const restore = useCallback(\n (\n trackId: string,\n state: { entries?: readonly SoundHistoryEntry[]; cursor?: number } | null | undefined,\n ): void => {\n const entries: SoundHistoryEntry[] = Array.isArray(state?.entries) ? [...state!.entries] : [];\n const raw = typeof state?.cursor === 'number' ? state!.cursor : entries.length - 1;\n const cursor = entries.length === 0 ? -1 : Math.min(Math.max(raw, 0), entries.length - 1);\n commit(trackId, { entries, cursor }, false);\n },\n [commit],\n );\n\n const list = useCallback(\n (trackId: string): TrackSoundHistory => dataRef.current[trackId] ?? EMPTY,\n [],\n );\n\n const canUndo = useCallback((trackId: string): boolean => {\n const h = dataRef.current[trackId];\n return !!h && h.cursor > 0;\n }, []);\n\n const clear = useCallback(\n (trackId: string): void => {\n if (dataRef.current[trackId]) {\n const next = { ...dataRef.current };\n delete next[trackId];\n dataRef.current = next;\n bump();\n }\n onChangeRef.current?.(trackId, EMPTY); // persist the cleared state\n },\n [bump],\n );\n\n const reset = useCallback((): void => {\n dataRef.current = {};\n bump();\n }, [bump]);\n\n // Stable object so consumers can safely list it in useCallback/useEffect deps.\n return useMemo(\n () => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),\n [record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite],\n );\n}\n","/**\n * useTrackReorder — shared drag-and-drop row reordering for generator panels.\n *\n * One hook drives the whole flow so every panel (drums / instruments / synths)\n * behaves identically: HTML5 drag mechanics (zero dependencies), an optimistic\n * local reorder, persistence via {@link PluginHost.reorderTracks}, and an\n * automatic revert if persistence fails. Panels supply their track array + its\n * setter and spread the returned props onto each {@link TrackRow}; the grip\n * handle and drop-target visuals live in TrackRow.\n *\n * Persisted ids should be STABLE (use `getId: t => t.handle.dbId`) — engine\n * track ids are not stable across project reopen.\n */\nimport { useCallback, useRef, useState } from 'react';\nimport type { DragEvent, Dispatch, SetStateAction } from 'react';\nimport type { PluginHost } from '../types/plugin-sdk.types';\n\n/**\n * Props the reorder machinery hands to a single row. Spread `handleProps` on the\n * drag grip and `rowProps` on the row's outer element; `isDragging` /\n * `isDragTarget` drive the visual state.\n */\nexport interface TrackRowDragProps {\n handleProps: {\n draggable: true;\n onDragStart: (e: DragEvent<HTMLElement>) => void;\n onDragEnd: (e: DragEvent<HTMLElement>) => void;\n };\n rowProps: {\n onDragEnter: (e: DragEvent<HTMLElement>) => void;\n onDragOver: (e: DragEvent<HTMLElement>) => void;\n onDragLeave: (e: DragEvent<HTMLElement>) => void;\n onDrop: (e: DragEvent<HTMLElement>) => void;\n };\n /** This row is the one currently being dragged (dim it). */\n isDragging: boolean;\n /** This row is the current drop target (show an insertion accent). */\n isDragTarget: boolean;\n}\n\n/**\n * Pure helper: return a NEW array with the item at `from` moved to `to`.\n * Out-of-range or no-op moves return a shallow copy unchanged. Exported for\n * unit testing the index math without a DOM.\n */\nexport function moveItem<T>(arr: readonly T[], from: number, to: number): T[] {\n const next = arr.slice();\n if (\n from === to ||\n from < 0 ||\n to < 0 ||\n from >= next.length ||\n to >= next.length\n ) {\n return next;\n }\n const [moved] = next.splice(from, 1);\n next.splice(to, 0, moved);\n return next;\n}\n\nexport interface UseTrackReorderOptions<T> {\n /** Host (only {@link PluginHost.reorderTracks} is used). */\n host: Pick<PluginHost, 'reorderTracks'>;\n /** The panel's current track array (also the render order). */\n items: T[];\n /** The panel's state setter for `items` (used for optimistic update + revert). */\n setItems: Dispatch<SetStateAction<T[]>>;\n /** Stable id for persistence — use the track's dbId, not its engine id. */\n getId: (item: T) => string;\n /** Called if persistence fails, after the optimistic update is reverted. */\n onError?: (err: unknown) => void;\n}\n\nexport interface UseTrackReorderResult {\n /** Build the drag props for the row at `index`; spread onto its TrackRow. */\n dragPropsFor: (index: number) => TrackRowDragProps;\n /** Index of the row being dragged, or null. */\n draggingIndex: number | null;\n /** Index of the current drop-target row, or null. */\n dragOverIndex: number | null;\n}\n\n/**\n * Drag-and-drop reordering for a panel's track list. Dropping a row onto another\n * row moves it into that row's position (everything between shifts); the top and\n * bottom are reachable by dropping on the first/last row.\n */\nexport function useTrackReorder<T>({\n host,\n items,\n setItems,\n getId,\n onError,\n}: UseTrackReorderOptions<T>): UseTrackReorderResult {\n const [draggingIndex, setDraggingIndex] = useState<number | null>(null);\n const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);\n // Source index for the in-flight drag; a ref avoids stale-closure reads in the\n // drop handler. itemsRef keeps the freshest array without re-creating handlers.\n const fromRef = useRef<number | null>(null);\n const itemsRef = useRef(items);\n itemsRef.current = items;\n\n const dragPropsFor = useCallback(\n (index: number): TrackRowDragProps => ({\n handleProps: {\n draggable: true,\n onDragStart: (e) => {\n fromRef.current = index;\n setDraggingIndex(index);\n if (e.dataTransfer) {\n e.dataTransfer.effectAllowed = 'move';\n // Required by Firefox to start a drag; the value itself is unused.\n try {\n e.dataTransfer.setData('text/plain', String(index));\n } catch {\n /* some environments disallow setData — drag still works */\n }\n }\n },\n onDragEnd: () => {\n fromRef.current = null;\n setDraggingIndex(null);\n setDragOverIndex(null);\n },\n },\n rowProps: {\n onDragEnter: (e) => {\n if (fromRef.current === null) return;\n e.preventDefault();\n setDragOverIndex(index);\n },\n onDragOver: (e) => {\n if (fromRef.current === null) return;\n e.preventDefault(); // allow drop\n if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';\n setDragOverIndex((cur) => (cur === index ? cur : index));\n },\n onDragLeave: () => {\n setDragOverIndex((cur) => (cur === index ? null : cur));\n },\n onDrop: (e) => {\n e.preventDefault();\n const from = fromRef.current;\n fromRef.current = null;\n setDraggingIndex(null);\n setDragOverIndex(null);\n if (from === null || from === index) return;\n\n const prev = itemsRef.current;\n const next = moveItem(prev, from, index);\n setItems(next);\n const ids = next.map(getId);\n Promise.resolve(host.reorderTracks(ids)).catch((err) => {\n // Persistence failed — roll back to the pre-drag order.\n setItems(prev);\n onError?.(err);\n });\n },\n },\n isDragging: draggingIndex === index,\n isDragTarget: dragOverIndex === index && draggingIndex !== index,\n }),\n [host, setItems, getId, onError, draggingIndex, dragOverIndex]\n );\n\n return { dragPropsFor, draggingIndex, dragOverIndex };\n}\n","/**\n * Plugin SDK Version\n *\n * Semver version of the plugin SDK contract.\n * Plugins declare minSdkVersion in their manifest.\n * Registry checks semver.gte(PLUGIN_SDK_VERSION, manifest.minHostVersion)\n * during activation and marks incompatible plugins accordingly.\n */\nexport const PLUGIN_SDK_VERSION = '2.25.0';\n","/**\n * Format the cross-plugin concurrent-track context into a prose block\n * that's safe to drop straight into an LLM user-prompt. Both the synth\n * and drum builtin panels use this so the rendered prompt stays\n * consistent across generators — and so a single change here propagates\n * to every plugin that calls `host.getGenerationContext()`.\n *\n * Per-track payload follows the user's preferred shape (raw note JSON\n * grouped by chord) so the model sees velocity / start-beat /\n * duration / pitch verbatim and can reason about feel + harmony.\n *\n * Returns the empty string when there are no concurrent tracks — call\n * sites can `if (block) push(block)` rather than baking in a placeholder.\n */\n\nimport type {\n PluginGenerationContext,\n PluginChordSegment,\n PluginMidiNote,\n} from '../types/plugin-sdk.types';\n\nexport function formatConcurrentTracks(ctx: PluginGenerationContext): string {\n const tracks = ctx.concurrentTracks;\n if (!tracks || tracks.length === 0) return '';\n\n const lines: string[] = [`Concurrent tracks in scene (already generated):`];\n\n for (const track of tracks) {\n const promptStr = track.prompt\n ? ` prompt=\"${escapeQuotes(track.prompt)}\"`\n : '';\n lines.push(` - role=${track.role ?? 'unknown'}${promptStr}`);\n\n if (track.notesByChord.length === 0) {\n lines.push(` (no notes)`);\n } else {\n for (const segment of track.notesByChord) {\n if (segment.notes.length === 0) continue;\n lines.push(` ${formatChordSegment(segment)}`);\n }\n }\n\n if (track.truncated && typeof track.originalNoteCount === 'number') {\n const dropped = track.originalNoteCount - sumKeptNotes(track.notesByChord);\n if (dropped > 0) {\n lines.push(` … (${dropped} more notes truncated)`);\n }\n }\n }\n\n if (ctx.truncatedTrackCount && ctx.truncatedTrackCount > 0) {\n lines.push(\n ` … (${ctx.truncatedTrackCount} additional track${ctx.truncatedTrackCount === 1 ? '' : 's'} omitted to fit token budget)`,\n );\n }\n\n return lines.join('\\n');\n}\n\nfunction formatChordSegment(segment: PluginChordSegment): string {\n const [start, end] = segment.chordRangeQn;\n const notesJson = JSON.stringify(segment.notes.map(compactNote));\n return `${segment.chord} (beats ${start}-${end}): ${notesJson}`;\n}\n\n/**\n * Strip channel and other rarely-relevant fields so the LLM sees only\n * the four properties that drive perception: pitch, startBeat,\n * durationBeats, velocity.\n */\nfunction compactNote(n: PluginMidiNote): {\n pitch: number;\n startBeat: number;\n durationBeats: number;\n velocity: number;\n} {\n return {\n pitch: n.pitch,\n startBeat: n.startBeat,\n durationBeats: n.durationBeats,\n velocity: n.velocity,\n };\n}\n\nfunction escapeQuotes(s: string): string {\n return s.replace(/\"/g, '\\\\\"');\n}\n\nfunction sumKeptNotes(segments: PluginChordSegment[]): number {\n let total = 0;\n for (const s of segments) total += s.notes.length;\n return total;\n}\n","/**\n * Lightweight, dependency-free semantic matching for sample selection.\n *\n * Sample generators (drums, instruments) ship a short StableAudio text\n * prompt next to every sample (\"tight 909-style kick one shot, hard click\n * transient, short punchy body, dry, no hi hats, no loop\"). When the user\n * asks for \"a 1950s style boom bap kick\" we want to pick the sample whose\n * prompt is closest to that intent — instead of a uniform random draw —\n * while still preserving variety so a vague \"give me a kick\" doesn't return\n * the identical sample every time.\n *\n * Design notes:\n * - Pure functions, no I/O, no SDK-type dependencies → trivially unit\n * testable with an injected `rng`, and safe to call from either the\n * main or renderer process.\n * - Scoring is IDF-weighted query-coverage (a TF-IDF / BM25-lite). The\n * IDF is derived from the candidate pool itself, so it is STRUCTURAL —\n * no hand-maintained synonym tables. Rare, discriminating tokens in the\n * prompts (\"909\", \"dusty\", \"tube\") dominate; corpus-universal filler\n * (\"one\", \"shot\", \"dry\") washes out to ~zero IDF on its own.\n * - The near-universal negative clauses StableAudio prompts carry\n * (\"no hi hats\", \"no loop\", \"no melody\") are stripped before tokenizing;\n * they are pure noise for matching.\n * - Selection is softmax-weighted random among the top-k. Flat scores →\n * ~uniform (≈ the old random behavior); a clear winner → tight\n * convergence. The all-zero (no-signal) case is intentionally left to\n * the caller to fall back to its existing random path over the full\n * pool — see `scorePromptMatch`'s contract below.\n */\n\n/**\n * Function words + a few imperative-request fillers that should never count\n * as matchable intent. Kept deliberately SMALL — IDF already neutralizes\n * corpus-universal words, and query tokens that appear in no candidate are\n * dropped during scoring, so this list only needs the words that would\n * otherwise be both query-frequent AND coincidentally present in prompts.\n */\nconst STOP_WORDS: ReadonlySet<string> = new Set([\n 'a', 'an', 'the', 'and', 'or', 'but', 'with', 'for', 'to', 'of', 'in', 'on',\n 'at', 'by', 'is', 'it', 'this', 'that', 'i', 'my', 'me', 'make', 'please',\n 'give', 'want', 'need', 'some', 'like', 'get', 'something',\n]);\n\n/**\n * Tokenize a prompt or query into matchable lowercase tokens.\n *\n * 1. Drop comma-delimited negative clauses (\"no hi hats\", \"no loop\").\n * 2. Lowercase, split on any non-alphanumeric run.\n * 3. Drop stop-words and 1–2 digit numeric noise (\"01\", \"02\") while\n * keeping meaningful numerics (\"808\", \"909\", \"1950\").\n */\nexport function tokenizePrompt(text: string): string[] {\n if (!text) return [];\n const withoutNegatives = text\n .split(',')\n .map((clause) => clause.trim())\n .filter((clause) => clause.length > 0 && !/^no\\s/i.test(clause))\n .join(' ');\n\n return withoutNegatives\n .toLowerCase()\n .split(/[^a-z0-9]+/u)\n .filter((tok) => {\n if (!tok) return false;\n if (STOP_WORDS.has(tok)) return false;\n if (/^\\d{1,2}$/.test(tok)) return false; // \"01\", \"02\" — sequence noise\n return true;\n });\n}\n\n/**\n * Score each candidate prompt against the query, returning a parallel array\n * of scores in [0, 1] (1 = the candidate covers all of the query's\n * discriminating intent).\n *\n * Contract: a returned max of 0 means the query shares NO matchable token\n * with any candidate (no signal). Callers should treat that as \"fall back to\n * the existing uniform-random pick over the full pool\" so vague queries keep\n * today's variety rather than biasing toward an arbitrary top-k slice.\n */\nexport function scorePromptMatch(\n query: string,\n candidatePrompts: ReadonlyArray<string>,\n): number[] {\n const n = candidatePrompts.length;\n if (n === 0) return [];\n\n const queryTokens = Array.from(new Set(tokenizePrompt(query)));\n if (queryTokens.length === 0) return candidatePrompts.map(() => 0);\n\n const candidateTokenSets = candidatePrompts.map((p) => new Set(tokenizePrompt(p)));\n\n // IDF for each query token, derived from the candidate pool. Tokens that\n // appear in no candidate are unmatchable → excluded from both the score\n // numerator and the normalization denominator.\n const idf = new Map<string, number>();\n for (const token of queryTokens) {\n let df = 0;\n for (const set of candidateTokenSets) {\n if (set.has(token)) df += 1;\n }\n if (df > 0) idf.set(token, Math.log(1 + n / df));\n }\n\n let denominator = 0;\n for (const weight of idf.values()) denominator += weight;\n if (denominator === 0) return candidatePrompts.map(() => 0);\n\n return candidateTokenSets.map((set) => {\n let numerator = 0;\n for (const [token, weight] of idf) {\n if (set.has(token)) numerator += weight;\n }\n return numerator / denominator;\n });\n}\n\n/** One scored candidate. `key` (if present) is what `excludeKeys` matches on. */\nexport interface ScoredCandidate<T> {\n item: T;\n score: number;\n key?: string;\n}\n\nexport interface PickTopKOptions {\n /** Consider only the top-k by score (default 5). */\n k?: number;\n /**\n * Softmax temperature (default 0.3). Lower → sharper preference for the\n * top match; higher → flatter (more variety). Scores are in [0, 1].\n */\n temperature?: number;\n /** Candidate keys to exclude (e.g. shuffle history). */\n excludeKeys?: ReadonlySet<string>;\n /** Injectable RNG in [0, 1) for deterministic tests (default Math.random). */\n rng?: () => number;\n}\n\n/**\n * Pick one candidate via softmax-weighted random selection among the top-k\n * by score. Returns null only when the pool is empty after exclusion.\n *\n * Equal scores → equal weights → uniform pick among the top-k, so this\n * degrades gracefully toward random when the query gives no preference.\n */\nexport function pickTopKWeighted<T>(\n scored: ReadonlyArray<ScoredCandidate<T>>,\n options: PickTopKOptions = {},\n): T | null {\n const { k = 5, temperature = 0.3, excludeKeys, rng = Math.random } = options;\n\n let pool = scored;\n if (excludeKeys && excludeKeys.size > 0) {\n pool = pool.filter((c) => c.key === undefined || !excludeKeys.has(c.key));\n }\n if (pool.length === 0) return null;\n\n const sorted = [...pool].sort((a, b) => b.score - a.score);\n const top = sorted.slice(0, Math.max(1, k));\n\n // Softmax with a max-subtraction for numerical stability.\n const maxScore = top[0].score;\n const safeTemp = Math.max(1e-6, temperature);\n const weights = top.map((c) => Math.exp((c.score - maxScore) / safeTemp));\n const totalWeight = weights.reduce((sum, w) => sum + w, 0);\n\n let threshold = rng() * totalWeight;\n for (let i = 0; i < top.length; i += 1) {\n threshold -= weights[i];\n if (threshold <= 0) return top[i].item;\n }\n return top[top.length - 1].item;\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime","next","import_jsx_runtime","import_react","import_react","import_jsx_runtime","import_jsx_runtime","import_jsx_runtime","import_react","import_jsx_runtime","import_react","import_jsx_runtime","import_react","import_jsx_runtime","useDebouncedCallback","import_react","import_jsx_runtime","import_jsx_runtime","React","import_react","import_jsx_runtime","React","import_react","import_jsx_runtime","scenes","import_react","import_jsx_runtime","import_react","import_jsx_runtime","import_jsx_runtime","import_react","import_jsx_runtime","import_react","import_jsx_runtime","import_react","import_jsx_runtime","import_react","import_react","import_react","import_react","compactNote"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/types/plugin-sdk.types.ts","../src/types/fx-toggle.types.ts","../src/components/TrackRow.tsx","../src/components/TrackDrawer.tsx","../src/constants/fx-presets.ts","../src/components/FxToggleBar.tsx","../src/components/PianoRollEditor.tsx","../src/components/ConfirmDialog.tsx","../src/components/Modal.tsx","../src/components/LevelMeter.tsx","../src/hooks/useTrackLevels.ts","../src/components/TrackMeterStrip.tsx","../src/components/VolumeSlider.tsx","../src/utils/volume-conversion.ts","../src/components/PanSlider.tsx","../src/components/SorceryProgressBar.tsx","../src/components/CrossfadeTrackRow.tsx","../src/crossfade-meta.ts","../src/crossfade-inpaint.ts","../src/fade-meta.ts","../src/components/FadeTrackRow.tsx","../src/components/FadeModal.tsx","../src/components/ImportTrackModal.tsx","../src/components/CrossfadeModal.tsx","../src/components/DownloadPackButton.tsx","../src/components/SamplePackCTACard.tsx","../src/components/WaveformView.tsx","../src/components/waveform.ts","../src/components/ScrollingWaveform.tsx","../src/components/OffsetScrubber.tsx","../src/components/wavPeakAnalyzer.ts","../src/components/synthesizeCuePoints.ts","../src/hooks/useSceneState.ts","../src/hooks/useAnySolo.ts","../src/hooks/useSoundHistory.ts","../src/hooks/useTrackReorder.ts","../src/constants/sdk-version.ts","../src/utils/format-concurrent-tracks.ts","../src/utils/semantic-match.ts"],"sourcesContent":["/**\n * @sas/plugin-sdk — Public API\n *\n * Everything an external plugin author needs to build a generator plugin\n * for Signals & Sorcery.\n */\n\n// ============================================================================\n// Types — Core plugin contract\n// ============================================================================\n\nexport type {\n GeneratorType,\n InstrumentDescriptor,\n GeneratorPlugin,\n PluginUIProps,\n PluginHost,\n ExportedPluginData,\n CreateTrackOptions,\n PluginTrackHandle,\n PluginTrackInfo,\n ImportCandidateTrack,\n ImportCandidateScene,\n SceneFamilyTrack,\n TrackSoundSnapshot,\n ListImportableTracksOptions,\n PluginSynthInfo,\n PluginTrackRuntimeState,\n TrackStateChangeListener,\n PluginFxCategoryDetailState,\n PluginTrackFxDetailState,\n MidiClipData,\n PluginMidiNote,\n MidiWriteResult,\n ReadMidiClip,\n ReadMidiResult,\n ExportMidiBundleOptions,\n ExportMidiBundleResult,\n PostProcessOptions,\n MusicalContext,\n PluginChordTiming,\n PluginGenerationContext,\n PluginConcurrentTrackInfo,\n PluginChordSegment,\n TransportEvent,\n DeckBoundaryEvent,\n PluginTransportState,\n PluginTrackLevel,\n PluginSceneInfo,\n PluginSceneContext,\n BulkAddPlaceholderTrack,\n TransportEventListener,\n DeckBoundaryListener,\n SceneChangeListener,\n UnsubscribeFn,\n LLMGenerationRequest,\n LLMGenerationResult,\n // Tool-use LLM types — agentic plugins (chat panel, etc.) use these via\n // `host.generateWithLLMTools` to drive a Claude-Code-style loop. SDK 2.4.0+.\n LLMPart,\n LLMContent,\n LLMFunctionDeclaration,\n LLMTool,\n LLMGenerationConfig,\n LLMSystemInstruction,\n LLMToolUseRequest,\n LLMUsageMetadata,\n LLMCandidate,\n LLMToolUseResponse,\n PluginPresetData,\n ShufflePresetResult,\n SoundHistoryEntry,\n PluginSettingsSchema,\n SettingDefinition,\n PluginSettingsStore,\n // AI skill surface — lets plugins declare LLM-callable actions\n // registered as namespaced tools (plugin:<id>:<skill>). Required for\n // plugins that expose a `chat` or similar agent-delegation skill.\n PluginSkill,\n PluginSkillInputSchema,\n PluginErrorCode,\n PluginManifest,\n PluginCapabilities,\n PluginFileDialogOptions,\n PluginDownloadOptions,\n PluginHttpRequestOptions,\n PluginHttpResponse,\n PluginSampleFilter,\n PluginSampleInfo,\n PluginSampleImportResult,\n PluginSampleTrackInfo,\n PluginAudioTextureRequest,\n PluginAudioTextureResult,\n PluginCuePoints,\n PluginTrimWindow,\n ComposeSceneOptions,\n ComposeSceneResult,\n ComposeProgressListener,\n ComposeProgressEvent,\n PluginPresetInfo,\n SavePluginPresetOptions,\n PluginAppTool,\n PluginAppToolInputSchema,\n PluginAppToolResult,\n PluginStatus,\n PluginRegistration,\n StemType,\n PluginStemSplitResult,\n PluginStemTrackInfo,\n // Audio recording (since SDK 2.1.0)\n AudioInputDevice,\n RecordingTargetInfo,\n RecordingChunkFinalizedEvent,\n // Drum sampler (since SDK 1.2.0)\n DrumKit,\n // Pitched instrument sampler (since SDK 1.3.0)\n InstrumentZone,\n InstrumentSampler,\n ListAudioFilesOptions,\n} from './types/plugin-sdk.types';\n\nexport { PluginError } from './types/plugin-sdk.types';\n\n// ============================================================================\n// Types — FX toggle system\n// ============================================================================\n\nexport type {\n FxCategory,\n FxPreset,\n MixInterpolation,\n FxPresetConfig,\n FxCategoryDetailState,\n TrackFxDetailState,\n TrackFxState,\n FxPresetDataEntry,\n FxPresetData,\n} from './types/fx-toggle.types';\n\nexport {\n FX_CATEGORIES,\n FX_CHAIN_ORDER,\n FX_ENGINE_PLUGIN_NAMES,\n FX_DISPLAY_LABELS,\n EMPTY_FX_STATE,\n DEFAULT_FX_DRY_WET,\n DEFAULT_FX_CATEGORY_DETAIL,\n EMPTY_FX_DETAIL_STATE,\n} from './types/fx-toggle.types';\n\n// ============================================================================\n// Components\n// ============================================================================\n\nexport { TrackRow, type SDKTrackRowProps } from './components/TrackRow';\nexport {\n CrossfadeTrackRow,\n type CrossfadeTrackRowProps,\n type CrossfadeLayer,\n} from './components/CrossfadeTrackRow';\nexport {\n EQUAL_POWER_GAIN,\n parseCrossfadePairs,\n asCrossfadeMeta,\n buildCrossfadeVolumeCurves,\n type CrossfadeSlot,\n type CrossfadeMeta,\n type CrossfadePairMeta,\n type VolumeAutomationPoint,\n type CrossfadeVolumeCurves,\n} from './crossfade-meta';\nexport { buildCrossfadeInpaintPrompt, type CrossfadeInpaintInput } from './crossfade-inpaint';\nexport {\n parseFades,\n asFadeMeta,\n buildFadeVolumeCurve,\n defaultFadeGesture,\n TEXTURAL_ROLES,\n type FadeDirection,\n type FadeGesture,\n type FadeMeta,\n type FadeEntry,\n} from './fade-meta';\nexport { FadeTrackRow, type FadeTrackRowProps, type FadeLayer } from './components/FadeTrackRow';\nexport { FadeModal, type FadeModalProps, type FadeSelection } from './components/FadeModal';\nexport { ImportTrackModal, type ImportTrackModalProps } from './components/ImportTrackModal';\nexport {\n CrossfadeModal,\n type CrossfadeModalProps,\n type CrossfadeSelection,\n} from './components/CrossfadeModal';\nexport { ConfirmDialog, type ConfirmDialogProps } from './components/ConfirmDialog';\nexport { Modal, type ModalProps } from './components/Modal';\nexport {\n TrackDrawer,\n type TrackDrawerProps,\n type DrawerTab,\n // Backwards-compatible aliases — the drawer was `InstrumentDrawer` before it\n // grew an FX tab + Import tab and became the unified per-track drawer.\n InstrumentDrawer,\n type TrackDrawerProps as InstrumentDrawerProps,\n} from './components/TrackDrawer';\nexport {\n PianoRollEditor,\n type PianoRollEditorProps,\n PX_PER_BEAT,\n ROW_HEIGHT,\n GUTTER_W,\n DRAG_DEAD_ZONE,\n RESIZE_HANDLE_PX,\n pxToCell,\n cellToPx,\n resizeNoteDuration,\n centerScrollTop,\n transposeNotes,\n pitchToName,\n} from './components/PianoRollEditor';\nexport { VolumeSlider } from './components/VolumeSlider';\nexport { PanSlider } from './components/PanSlider';\nexport { FxToggleBar, type FxToggleBarProps } from './components/FxToggleBar';\nexport { SorceryProgressBar, calculateTimeBasedTarget } from './components/SorceryProgressBar';\nexport { DownloadPackButton, type DownloadPackButtonProps, type DownloadPackButtonVariant } from './components/DownloadPackButton';\nexport {\n SamplePackCTACard,\n type SamplePackCTACardProps,\n type SamplePackCTACardStatus,\n type SamplePackCardInfo,\n} from './components/SamplePackCTACard';\n\n// Waveform / audio-clip UI toolkit — shared by audio-oriented plugins (stems,\n// recorder). Promoted from the app's src/plugins/shared (W9 — so extracted\n// plugins reach it through the SDK, not a relative app path). Since 2.10.0.\nexport { WaveformView, type WaveformViewProps } from './components/WaveformView';\nexport { LevelMeter, type LevelMeterProps } from './components/LevelMeter';\nexport { TrackMeterStrip, type TrackMeterStripProps } from './components/TrackMeterStrip';\nexport { ScrollingWaveform, type ScrollingWaveformProps } from './components/ScrollingWaveform';\nexport { OffsetScrubber, type OffsetScrubberProps } from './components/OffsetScrubber';\nexport { computePeaks, drawWaveform, type WaveformPeaks } from './components/waveform';\nexport { analyzeWavPeak, type PeakAnalysis } from './components/wavPeakAnalyzer';\nexport { synthesizeCuePoints, type SynthesizeCuePointsOptions } from './components/synthesizeCuePoints';\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\nexport { useSceneState } from './hooks/useSceneState';\nexport { useAnySolo } from './hooks/useAnySolo';\nexport {\n useSoundHistory,\n type UseSoundHistoryResult,\n type UseSoundHistoryOptions,\n type TrackSoundHistory,\n} from './hooks/useSoundHistory';\nexport {\n useTrackReorder,\n moveItem,\n type UseTrackReorderOptions,\n type UseTrackReorderResult,\n type TrackRowDragProps,\n} from './hooks/useTrackReorder';\nexport {\n useTrackLevels,\n useTrackLevel,\n useTrackMeter,\n useTransportPlaying,\n type TrackLevelsHandle,\n type TrackMeterView,\n} from './hooks/useTrackLevels';\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n// VALID_INSTRUMENT_ROLES (SDK 1.x) removed in 2.0.0 — external plugins now\n// call `host.getValidRoles()` on PluginHost at runtime. The canonical list\n// lives in the assistant (src/music-engine/constants/instrument-classification.ts)\n// and is exposed via that accessor.\nexport { PLUGIN_SDK_VERSION } from './constants/sdk-version';\nexport { FX_PRESET_CONFIGS } from './constants/fx-presets';\n\n// ============================================================================\n// Utils\n// ============================================================================\n\nexport { sliderToDb, dbToSlider, SLIDER_UNITY, DB_MAX, DB_MIN } from './utils/volume-conversion';\nexport { formatConcurrentTracks } from './utils/format-concurrent-tracks';\n\n// Semantic sample matching — pick the closest sample to a text intent by\n// scoring against each sample's StableAudio prompt, with variety-preserving\n// top-k weighted selection. Shared by the drum + instrument resolvers. Since 2.11.0.\nexport {\n tokenizePrompt,\n scorePromptMatch,\n pickTopKWeighted,\n type ScoredCandidate,\n type PickTopKOptions,\n} from './utils/semantic-match';\n","/**\n * Plugin SDK Type Definitions\n *\n * Complete type system for the generator plugin architecture.\n * Plugins implement GeneratorPlugin and interact with the host via PluginHost.\n * All plugin output flows through TracktionEngine (MIDI or audio clips).\n */\n\nimport type { ComponentType, ReactNode } from 'react';\n\n// ============================================================================\n// Core Plugin Interface\n// ============================================================================\n\n/** What kind of Tracktion content this plugin creates */\nexport type GeneratorType = 'midi' | 'audio' | 'sample' | 'hybrid';\n\n/**\n * Drum-kit configuration for `host.setTrackDrumKit`. Prototype shape carries\n * a single sample; future multi-slot kits will extend this with a `notes`\n * map (`Record<midiNote, samplePath>`) for GM-style drum maps.\n */\nexport interface DrumKit {\n /** Absolute path to the sample (WAV, AIFF, FLAC). Triggered on every note-on. */\n samplePath: string;\n}\n\n/**\n * One key-mapped sample zone in a pitched, polyphonic instrument.\n * Used by `host.setTrackInstrumentSampler`.\n *\n * Zones in an InstrumentSampler MUST be disjoint and ordered low to\n * high by rootKey — the engine rejects overlap because Tracktion would\n * otherwise double-trigger every matching sound on each note-on.\n */\nexport interface InstrumentZone {\n /** Absolute path to the zone's sample (WAV, FLAC, AIFF). */\n samplePath: string;\n /** MIDI note this sample sounds at unshifted (0-127). */\n rootKey: number;\n /** Inclusive low end of the key range that triggers this zone (0-127). */\n minKey: number;\n /** Inclusive high end of the key range that triggers this zone (0-127). */\n maxKey: number;\n /**\n * If true, the sampler plays the sample for the duration the note is\n * held and stops on note-off (good for sustaining pads, organs, etc.,\n * whose source has been pre-trimmed to a steady-state region).\n * If false, the sampler plays the sample through to its end ignoring\n * note-off (good for plucks, mallets, percussion).\n */\n openEnded: boolean;\n}\n\n/**\n * Pitched instrument configuration for `host.setTrackInstrumentSampler`.\n * Parallel to `DrumKit` but multi-zone and pitch-aware. A manifest\n * authored by the pitched-sample pipeline reduces to one of these.\n *\n * NOTE: This is distinct from `host.setTrackInstrument(trackId, pluginId)`\n * which loads a VST3/AU synth plugin. `setTrackInstrumentSampler` loads\n * the built-in Tracktion sampler with N pre-rendered zones.\n */\nexport interface InstrumentSampler {\n /** Display name (e.g. \"Bright Warm Pluck\"). Used for diagnostics. */\n name: string;\n /** Disjoint zones, ordered low->high by rootKey. At least one required. */\n zones: ReadonlyArray<InstrumentZone>;\n}\n\n/** Options for `host.listAudioFiles`. */\nexport interface ListAudioFilesOptions {\n /**\n * File extensions to include (dot-prefixed, lowercase). Defaults to\n * `['.wav']`. Other audio formats (`.aif`, `.flac`, `.mp3`) are passed\n * through verbatim; the host does not transcode.\n */\n extensions?: string[];\n /** Walk subdirectories. Defaults to `false`. */\n recursive?: boolean;\n}\n\n/** Describes an available instrument plugin (VST3/AU synth) on the system. */\nexport interface InstrumentDescriptor {\n /** Stable plugin identifier for loading (VST3 TUID or AU component ID) */\n pluginId: string;\n /** Display name */\n name: string;\n /** Plugin manufacturer */\n manufacturer: string;\n /** Plugin format */\n type: 'vst3' | 'au' | 'vst' | 'internal';\n /** Plugin category (from scan) */\n category: string;\n /** Whether this plugin is currently installed/available */\n missing?: boolean;\n}\n\n/** Every generator plugin must implement this interface. */\nexport interface GeneratorPlugin {\n /** Unique ID, npm-style scope: '@sas/synth-generator', '@user/my-plugin' */\n readonly id: string;\n /** Human-readable name shown in accordion header */\n readonly displayName: string;\n /** Semver version string */\n readonly version: string;\n /** Short description for settings/marketplace */\n readonly description: string;\n /** 24x24 icon — data URL, relative path from plugin dir, or undefined */\n readonly icon?: string;\n /** What kind of Tracktion content this plugin creates */\n readonly generatorType: GeneratorType;\n /** Minimum host SDK version this plugin requires */\n readonly minHostVersion?: string;\n\n /**\n * Called once when plugin is loaded. Receives the PluginHost API.\n * If this throws, plugin is marked as failed and not rendered.\n */\n activate(host: PluginHost): Promise<void>;\n\n /**\n * Called when plugin is being unloaded (disable, uninstall, app quit).\n * Must complete within 5 seconds or host force-kills.\n */\n deactivate(): Promise<void>;\n\n /**\n * Return the React component rendered inside the accordion section.\n * Component receives PluginUIProps from the host.\n */\n getUIComponent(): ComponentType<PluginUIProps>;\n\n /**\n * Return JSON Schema for plugin-specific settings.\n * Host auto-renders a settings form. Return null if no settings.\n */\n getSettingsSchema(): PluginSettingsSchema | null;\n\n /**\n * Optional: Called when the active scene changes.\n */\n onSceneChanged?(sceneId: string | null): Promise<void>;\n\n /**\n * Optional: Called when the generation context changes\n * (chords updated, tracks added/removed, BPM changed).\n */\n onContextChanged?(context: MusicalContext): void;\n\n /**\n * Optional: Declare LLM-callable skills this plugin provides.\n * Skills are registered as namespaced tools (plugin:<pluginId>:<skillId>)\n * and become available to AI agents for orchestration.\n *\n * Example: the chat-panel plugin declares a `chat` skill so external\n * agents (Claude Code, OpenClaw) can delegate scene-scoped natural\n * language work to the in-app agent via a single call.\n */\n getSkills?(): PluginSkill[];\n}\n\n// ============================================================================\n// Plugin Skills (AI Harness)\n// ============================================================================\n\n/** An LLM-callable action declared by a plugin. */\nexport interface PluginSkill {\n /** Unique skill id within this plugin (e.g., 'chat', 'generate_bassline') */\n id: string;\n /** Human-readable description — drives LLM tool selection */\n description: string;\n /** JSON Schema for the skill's input parameters */\n inputSchema: PluginSkillInputSchema;\n /** Whether this skill only reads state (no mutations). Default: false */\n isReadOnly?: boolean;\n}\n\n/** JSON Schema shape for skill input parameters. */\nexport interface PluginSkillInputSchema {\n type: 'object';\n properties?: Record<string, unknown>;\n required?: string[];\n}\n\n// ============================================================================\n// Plugin UI Props\n// ============================================================================\n\n/** Props passed to every plugin's React component by the host */\nexport interface PluginUIProps {\n /** The scoped PluginHost API instance for this plugin */\n host: PluginHost;\n /** Currently active scene ID (null if none selected) */\n activeSceneId: string | null;\n /** Whether the user is authenticated (for LLM access) */\n isAuthenticated: boolean;\n /** Whether all systems are connected (engine, gateway) */\n isConnected: boolean;\n /** Which workstation deck this is rendered in */\n deckId?: 'left' | 'right';\n /** Plugin calls this to set/clear header buttons. Pass null to clear. */\n onHeaderContent?: (content: ReactNode | null) => void;\n /** Plugin calls this to show/hide the loading spinner in the header. */\n onLoading?: (loading: boolean) => void;\n /** Scene-level context: contract state, chords, BPM, etc. Null if no scene. */\n sceneContext?: PluginSceneContext | null;\n /** Callback to open the scene selector (Scenes accordion section). */\n onSelectScene?: (() => void) | null;\n /** Callback to open the contract/chords section (for \"Generate a Contract\" CTA). */\n onOpenContract?: (() => void) | null;\n /** Callback to expand this plugin's own accordion section. */\n onExpandSelf?: (() => void) | null;\n /**\n * Whether the host's accordion section for this plugin is currently expanded.\n * Plugin UIs can watch transitions to take focus, refresh data, etc. The host\n * keeps the plugin mounted across collapse/expand to preserve state, so this\n * prop (not mount/unmount) is the signal that the user is actively viewing.\n */\n isExpanded?: boolean;\n}\n\n// ============================================================================\n// PluginHost API\n// ============================================================================\n\n/**\n * Canonical display metadata for a distributable sample pack, sourced from the\n * HOST's pack registry (the same source it uses to download + version-check the\n * bundle). Returned by `host.getSamplePackInfo` so a plugin's download CTA can\n * show the live name / description / size instead of a hardcoded copy that\n * drifts when a new pack version ships. Structurally compatible with\n * `SamplePackCardInfo` (the CTA card prop).\n *\n * @since SDK 2.12.0\n */\nexport interface SamplePackPublicInfo {\n /** Stable pack identifier, e.g. `'sas-instrument-pack'`. */\n packId: string;\n /** Human-readable pack name for the CTA headline. */\n displayName: string;\n /** One-line description of the pack's contents. */\n description: string;\n /** Size in bytes of the default download variant. */\n sizeBytes: number;\n}\n\n/** Scoped API surface that plugins interact with. Plugins NEVER get direct TracktionEngine access. */\nexport interface PluginHost {\n // --- Track Management (ownership-scoped) ---\n\n /** Create a new track in the active scene. Host enforces ownership and scene routing. */\n createTrack(options: CreateTrackOptions): Promise<PluginTrackHandle>;\n\n /** Delete a track previously created by THIS plugin. */\n deleteTrack(trackId: string): Promise<void>;\n\n /** Get all tracks this plugin owns in the active scene. */\n getPluginTracks(): Promise<PluginTrackHandle[]>;\n\n /** Adopt unowned tracks in the active scene matching this plugin's generator type. */\n adoptSceneTracks(): Promise<PluginTrackHandle[]>;\n\n /** Get info about a specific owned track. */\n getTrackInfo(trackId: string): Promise<PluginTrackInfo>;\n\n /** Set track mute state. Only works on owned tracks. */\n setTrackMute(trackId: string, muted: boolean): Promise<void>;\n\n /** Set track volume (linear 0.0 - 1.0). Only works on owned tracks. */\n setTrackVolume(trackId: string, volume: number): Promise<void>;\n /**\n * Set/replace a time-based volume automation curve (a fade envelope) on a track,\n * or clear it with an empty array. Points are {time: seconds, db}; linear between\n * points. Used by crossfade tracks to fade origin↔target across the looped\n * transition. Optional — callers MUST null-check. @since SDK 2.25.0\n */\n setTrackVolumeAutomation?(trackId: string, points: Array<{ time: number; db: number }>): Promise<void>;\n\n /** Set track pan (-1.0 left to 1.0 right). Only works on owned tracks. */\n setTrackPan(trackId: string, pan: number): Promise<void>;\n\n /** Set track solo state. Only works on owned tracks. */\n setTrackSolo(trackId: string, solo: boolean): Promise<void>;\n\n /** Whether ANY track in the project is currently soloed (across all panels).\n * Lets a panel dim its non-soloed rows (the engine silences them via the\n * effective-mute model). Read-only; not ownership-scoped. */\n isAnySoloActive(): Promise<boolean>;\n\n /** Rename a track. Only works on owned tracks. */\n setTrackName(trackId: string, name: string): Promise<void>;\n\n /**\n * Persist a track's musical role to the `tracks.role` column. Call this\n * after an LLM generation classifies the track (e.g. `'bass'`, `'lead'`,\n * `'pad'`, `'fx'`, `'kicks'`) so downstream features — especially the v1\n * transition generator's layer classifier — can see the role.\n *\n * Canonical values understood by the transition classifier include\n * `bass`, `drums`, `lead`, `chords`, `pad`, `arp`, `fx`, `kicks`,\n * `snares`, `hats`, `clap`, `perc`, `riser`, `impact`. Anything else is\n * stored verbatim but won't match the neutral-role set.\n *\n * Only works on owned tracks.\n */\n setTrackRole(trackId: string, role: string): Promise<void>;\n\n /** Shuffle preset: keep MIDI, apply a random preset from the same category. Only works on owned tracks. */\n /**\n * Shuffle preset: keep MIDI, apply a random preset from the same category.\n * `excludeNames` (since SDK 1.5.0) filters preset names out of the random\n * pool; the current preset is always implicitly excluded. Use this to\n * implement a \"no-repeat until full cycle\" shuffle: the panel accumulates\n * the history and resets when shufflePreset throws \"no presets available\".\n */\n shufflePreset(trackId: string, excludeNames?: readonly string[]): Promise<ShufflePresetResult>;\n\n /** Duplicate track: copy MIDI + role to a new track with a different preset. Only works on owned tracks. */\n duplicateTrack(trackId: string): Promise<PluginTrackHandle>;\n\n /**\n * Persist this plugin's track row order for the active scene. Pass the stable\n * track dbIds ({@link PluginTrackHandle.dbId}) in the desired top-to-bottom\n * order. Reload-safe — {@link getPluginTracks} returns tracks in this order\n * across scene switches and project reopen.\n *\n * Per-panel and decoupled from the engine-synced global track order, so\n * reordering one panel never disturbs other plugins' tracks. Tracks omitted\n * from the list (e.g. newly added or duplicated) keep their natural order at\n * the end. Pairs with the {@link useTrackReorder} hook, which drives the\n * drag-and-drop UI and calls this on drop.\n *\n * @since SDK 2.16.0\n */\n reorderTracks(orderedTrackIds: readonly string[]): Promise<void>;\n\n /**\n * Return the canonical list of valid role tokens that the host's\n * classifier and UI understand. Plugins should use this list when\n * building LLM prompts or validating role values before calling\n * {@link setTrackRole}.\n *\n * The assistant owns the canonical taxonomy — plugins MUST NOT ship\n * their own hardcoded list, which would drift from the host. Pair with\n * {@link setTrackRole} to persist a classified role.\n *\n * @since SDK 2.0.0\n */\n getValidRoles(): readonly string[];\n\n // --- FX Operations (ownership-scoped) ---\n\n /** Get detailed FX state for a track (enabled, preset, dry/wet per category). */\n getTrackFxState(trackId: string): Promise<PluginTrackFxDetailState>;\n\n /** Toggle an FX category on/off for a track. */\n toggleTrackFx(trackId: string, category: string, enabled: boolean): Promise<void>;\n\n /** Set FX preset for a track. Returns the new dry/wet value if applicable. */\n setTrackFxPreset(trackId: string, category: string, presetIndex: number): Promise<{ dryWet?: number }>;\n\n /** Set FX dry/wet level for a track. */\n setTrackFxDryWet(trackId: string, category: string, value: number): Promise<void>;\n\n // --- Real-time Track State ---\n\n /** Subscribe to real-time track state changes (mute, solo, volume, pan). Returns unsubscribe fn. */\n onTrackStateChange(listener: TrackStateChangeListener): UnsubscribeFn;\n\n // --- MIDI Operations ---\n\n /** Write MIDI notes to a track this plugin owns. Replaces existing MIDI. */\n writeMidiClip(trackId: string, clip: MidiClipData): Promise<MidiWriteResult>;\n\n /** Clear all MIDI from a track this plugin owns. */\n clearMidi(trackId: string): Promise<void>;\n\n /**\n * Export all tracks owned by this plugin in the active scene as a ZIP bundle\n * of Standard MIDI Files (one .mid per track, named after each track with\n * collision-avoidance suffixes). Prompts the user for a save location.\n *\n * Tracks with no MIDI data are skipped. Returns the path written, or\n * `{ canceled: true }` if the user dismissed the save dialog.\n *\n * @since SDK 1.1.0\n */\n exportTracksAsMidiBundle(\n options?: ExportMidiBundleOptions\n ): Promise<ExportMidiBundleResult>;\n\n /**\n * Run the host's MIDI post-processing pipeline on raw notes.\n * Wraps MidiProcessor: quantize -> swing -> scale -> register -> overlaps -> humanize.\n */\n postProcessMidi(notes: PluginMidiNote[], options: PostProcessOptions): Promise<PluginMidiNote[]>;\n\n /**\n * Read a track's current MIDI notes for in-place editing (e.g. a piano\n * roll). Returns the track's clips with beat-based notes; an empty `clips`\n * array means the track has no MIDI. Reads LIVE engine state (NOT the DB),\n * so it reflects unsaved generator output too and needs no project_id\n * scoping — do not \"fix\" this into a DB query.\n *\n * Ownership-gated like {@link writeMidiClip}. Optional so a plugin built\n * against this SDK still loads on an older host — callers MUST null-check.\n * @since SDK 2.15.0\n */\n readMidiNotes?(trackId: string): Promise<ReadMidiResult>;\n\n // --- Audio Operations ---\n\n /** Place an audio file on a track this plugin owns. */\n writeAudioClip(trackId: string, filePath: string, position?: number): Promise<void>;\n\n /**\n * Render a single track to a temporary WAV file and return its path.\n * Only works on owned tracks. For MIDI/synth tracks the host mutes siblings\n * and renders the scene. For single-clip audio tracks the host MAY take a\n * copy-source fast path.\n * @since SDK 1.2.0\n */\n exportTrackAudio?(trackId: string): Promise<ExportTrackAudioResult>;\n\n /**\n * Run a chain of audio operations on an input WAV via the bundled\n * sas-audio-processor binary. Unsupported ops throw NOT_IMPLEMENTED.\n * @since SDK 1.2.0\n */\n processAudio?(\n inputPath: string,\n operations: AudioProcessingOp[]\n ): Promise<ProcessAudioResult>;\n\n /**\n * Replace a track's audio content. For audio tracks, clears clips and\n * adds the new audio. For MIDI/synth tracks, the original row is stashed\n * in plugin_data and a new audio_tracks row is inserted (MIDI is lost).\n * @since SDK 1.2.0\n */\n replaceTrackAudio?(trackId: string, audioPath: string): Promise<void>;\n\n // --- Plugin/Synth Operations ---\n\n /** Load a VST3/AU plugin onto a track this plugin owns. */\n loadSynthPlugin(trackId: string, pluginName: string): Promise<number>;\n\n /** Set plugin state (base64-encoded preset data). */\n setPluginState(trackId: string, pluginIndex: number, stateBase64: string): Promise<void>;\n\n /** Get current plugin state (base64-encoded). */\n getPluginState(trackId: string, pluginIndex: number): Promise<string>;\n\n /**\n * Set a plugin's RAW VST3/AU state — the plugin's own getStateInformation\n * format, bypassing Tracktion's ValueTree wrapper. Use for third-party\n * instruments (u-he Diva, Serum, …) whose patches the ValueTree round-trip\n * does not faithfully preserve. Default Surge XT presets use setPluginState.\n * @since SDK 2.15.0\n */\n setRawPluginState(trackId: string, pluginIndex: number, stateBase64: string): Promise<void>;\n\n /** Get a plugin's RAW VST3/AU state (see setRawPluginState). @since SDK 2.15.0 */\n getRawPluginState(trackId: string, pluginIndex: number): Promise<string>;\n\n /** List plugins currently loaded on a track. */\n getTrackPlugins(trackId: string): Promise<PluginSynthInfo[]>;\n\n /** Remove a plugin from a track. */\n removePlugin(trackId: string, pluginIndex: number): Promise<void>;\n\n /** Check if a specific VST/AU plugin is available on the system. */\n isPluginAvailable(pluginName: string): Promise<boolean>;\n\n // --- Instrument Plugin Selection ---\n\n /** Get available instrument plugins (VST3/AU synths) scanned by the engine. */\n getAvailableInstruments(): Promise<InstrumentDescriptor[]>;\n\n /** Get the instrument plugin currently loaded on a track. Null = default (Surge XT). */\n getTrackInstrument(trackId: string): Promise<InstrumentDescriptor | null>;\n\n /** Change the instrument plugin on a track. Preserves MIDI data. */\n setTrackInstrument(trackId: string, pluginId: string): Promise<void>;\n\n /** Open the instrument plugin's native editor GUI as a floating window. */\n showInstrumentEditor(trackId: string): Promise<void>;\n\n /** Close the instrument plugin's editor window. */\n hideInstrumentEditor(trackId: string): Promise<void>;\n\n // --- Drum Sampler ---\n\n /**\n * Load the engine's built-in sampler on the track (if not already\n * present) and configure it with a single one-shot sound. Every MIDI\n * note triggers the loaded sample regardless of pitch — used by the\n * drum-generator plugin where the LLM's emitted pitch is advisory.\n *\n * Idempotent: calling repeatedly on the same track swaps the loaded\n * sample without stacking more sampler instances. The sampler counts\n * as the track's instrument; mixing it with `setTrackInstrument` on\n * the same track is undefined behaviour for now.\n *\n * @since SDK 1.2.0\n */\n setTrackDrumKit(trackId: string, kit: DrumKit): Promise<void>;\n\n /**\n * Load the engine's built-in sampler on the track (if not already\n * present) and configure it with a pitched, polyphonic, multi-zone\n * instrument. Each MIDI note triggers the zone whose [minKey,maxKey]\n * range contains it; the zone is played back pitch-shifted relative\n * to its rootKey. Polyphony is handled by the Tracktion sampler's\n * voice allocator.\n *\n * Used by the instrument-generator plugin to load a pre-rendered\n * pitched-sample manifest. Mutually exclusive with `setTrackDrumKit`\n * on the same track (both occupy the sampler slot) and with\n * `setTrackInstrument(pluginId)` (which loads a VST synth instead).\n *\n * Idempotent: calling repeatedly on the same track swaps the loaded\n * zones without stacking sampler instances.\n *\n * @since SDK 1.3.0\n */\n setTrackInstrumentSampler(trackId: string, instrument: InstrumentSampler): Promise<void>;\n\n // --- Filesystem (sample library scanning) ---\n\n /**\n * List audio files (by default `.wav`) under `rootPath`. Returns\n * absolute file paths. `recursive` defaults to false; pass `true` to\n * walk subdirectories. The drum-generator plugin uses this to\n * lazily discover available samples without round-tripping each\n * folder through `getSamples`.\n *\n * Plugins MUST NOT use this to read paths outside their declared\n * sample roots — the host may add path validation in a later release.\n *\n * @since SDK 1.2.0\n */\n listAudioFiles(rootPath: string, options?: ListAudioFilesOptions): Promise<string[]>;\n\n /**\n * Read a text file's contents from the host filesystem (UTF-8). Returns\n * `null` on any read error (missing file, permission, etc.) — the\n * caller does not need to wrap the call in try/catch.\n *\n * Intended for plugin sample-library metadata: instrument manifest\n * JSON (`<instrument-id>/manifest.json`) and prompt-sibling text\n * (`<id>.txt`). Plugins parse the returned string themselves so the\n * host stays content-agnostic.\n *\n * Plugins MUST NOT use this to read paths outside their declared\n * sample roots — the host may add path validation in a later release.\n *\n * @since SDK 1.4.0\n */\n readTextFile(absolutePath: string): Promise<string | null>;\n\n // --- Scene Context (read-only) ---\n\n /** Get the FULL generation context for the active scene. */\n getGenerationContext(excludeTrackId?: string): Promise<PluginGenerationContext>;\n\n /** Get lightweight musical context (no concurrent track MIDI data). */\n getMusicalContext(): Promise<MusicalContext>;\n\n /** Get the active scene ID. Null if no scene is active. */\n getActiveSceneId(): string | null;\n\n /**\n * Get the bound project's DB id. Null when no project is bound.\n * Optional — older hosts and the renderer-side host proxy may omit it;\n * callers MUST feature-check. Used e.g. to detect project switches for\n * per-project conversation persistence.\n * @since SDK 2.18.0\n */\n getProjectId?(): string | null;\n\n /** Get list of all scenes in the project. */\n getSceneList(): Promise<PluginSceneInfo[]>;\n\n /**\n * Enumerate importable track candidates from OTHER scenes, scoped to this\n * plugin's track type (derived from the plugin id). Each candidate is\n * annotated with `importable` + `disabledReason` — the host computes the\n * harmonic/length/tempo gate so the UI only renders it. By default the active\n * scene is excluded; pass `includeSameScene` to also surface the active\n * scene's MIDI tracks owned by OTHER panels (the cross-panel re-sound source).\n * Scenes with no candidate of this type are omitted.\n *\n * Optional so a plugin built against this SDK still loads on an older host —\n * callers MUST null-check and hide the affordance when absent.\n * @since SDK 2.13.0\n */\n listImportableTracks?(opts?: ListImportableTracksOptions): Promise<ImportCandidateScene[]>;\n\n /**\n * Import a source track (from another scene) into the active scene as a\n * faithful, independent copy, delegating to the `import_track_from_scene`\n * tool. Returns the new track's handle so the panel can append a row.\n * Throws on a gate violation — call only for candidates with `importable`.\n * Optional — callers MUST null-check (see `listImportableTracks`).\n * @since SDK 2.13.0\n */\n importTrack?(opts: { sourceSceneId: string; sourceTrackId: string }): Promise<PluginTrackHandle>;\n\n /**\n * Read a source track's CURRENT sound — sample path (drums), sampler zones\n * (instruments), or Surge preset state (synths) — so a panel can copy just\n * the sound onto another track, IGNORING the contract gate that `importTrack`\n * enforces (\"different contract, same preset\"). Read-only: applies nothing.\n * The selector is the source track's DB row id (`ImportCandidateTrack.dbId`).\n * Returns null when the track has no stored sound. Optional — callers MUST\n * null-check (see `listImportableTracks`).\n * @since SDK 2.14.0\n */\n getTrackSound?(sourceTrackDbId: string): Promise<TrackSoundSnapshot | null>;\n\n /**\n * Read a source track's persisted MIDI by its DB row id — the cross-panel\n * READ half of \"re-sound a part on a different instrument\". Unlike\n * `readMidiNotes` (engine-read, ownership-gated), this reads the DB and is\n * NOT ownership-gated, so a panel can pull a part out of a track owned by a\n * DIFFERENT panel in the same scene (the selector is\n * `ImportCandidateTrack.dbId`, e.g. a `sameScene` candidate). Notes are\n * beat-based, identical shape to `readMidiNotes`; the loop span comes from the\n * source scene. Returns `{ clips: [] }` when the track has no MIDI. Optional —\n * callers MUST null-check (see `listImportableTracks`).\n * @since SDK 2.20.0\n */\n readImportableTrackMidi?(sourceTrackDbId: string): Promise<ReadMidiResult>;\n\n /**\n * List THIS panel's family tracks in a specific scene (by DB id), WITHOUT the\n * import key/length/tempo gate that `listImportableTracks` applies. Powers the\n * crossfade picker: the origin (from) and target (to) scenes of a transition\n * deliberately differ in key, so gating would wrongly hide valid candidates.\n * Project-scoped, read-only. Returns [] for an unknown/empty scene. Optional —\n * callers MUST null-check (see `listImportableTracks`).\n * @since SDK 2.22.0\n */\n listSceneFamilyTracks?(sceneDbId: string): Promise<SceneFamilyTrack[]>;\n\n /**\n * Read a specific scene's musical key (tonic + mode) by db id. Labels the\n * SOURCE keys of a crossfade's origin/target patterns — the active-scene\n * musical context only carries the transition scene's key. Optional — callers\n * MUST null-check. @since SDK 2.24.0\n */\n getSceneKey?(sceneDbId: string): Promise<{ key: string; mode: string } | null>;\n /**\n * Read a scene's human display name by db id (for labelling a crossfade's\n * origin/target scenes). Optional — callers MUST null-check. @since SDK 2.26.0\n */\n getSceneName?(sceneDbId: string): Promise<string | null>;\n\n // --- Transport & Playback Events ---\n\n /** Subscribe to transport state changes. Returns unsubscribe function. */\n onTransportEvent(listener: TransportEventListener): UnsubscribeFn;\n\n /** Subscribe to deck boundary events. Returns unsubscribe function. */\n onDeckBoundary(listener: DeckBoundaryListener): UnsubscribeFn;\n\n /** Subscribe to scene change events. Returns unsubscribe function. */\n onSceneChange(listener: SceneChangeListener): UnsubscribeFn;\n\n /** Get current transport state (one-shot). */\n getTransportState(): Promise<PluginTransportState>;\n\n /**\n * One-shot mono peak level for every track this plugin owns. Drives the\n * cosmetic per-track strip meters; poll at ~30Hz while the transport is\n * playing. The host scopes the result to this plugin's tracks and coalesces\n * the underlying engine read, so a busy engine yields a STALE meter rather\n * than a backlog (playback always wins over the GUI). Optional: guard with\n * `typeof host.getTrackLevels === 'function'` for older hosts.\n * @since SDK 2.21.0\n */\n getTrackLevels?(): Promise<PluginTrackLevel[]>;\n\n // --- LLM Access (metered, authenticated) ---\n\n /** Generate text/JSON via the host's authenticated LLM service. */\n generateWithLLM(request: LLMGenerationRequest): Promise<LLMGenerationResult>;\n\n /**\n * Generate with native tool-use (function calling). Used by agentic plugins\n * (chat panel, etc.) to drive an iterative loop where the model calls tools,\n * observes results, and decides next steps — same loop class as Claude Code\n * or VS Code agent mode.\n *\n * Shape mirrors Gemini's `generateContent` REST surface; the host forwards\n * verbatim to the gateway's Gemini-native passthrough endpoint, which adds\n * the central Google API key. Plugins never see provider credentials.\n *\n * Available since SDK 2.4.0.\n */\n generateWithLLMTools(request: LLMToolUseRequest): Promise<LLMToolUseResponse>;\n\n /**\n * Resolve absolute paths for spawning the bundled `sas` CLI as a subprocess.\n * Used by agentic plugins that drive the CLI as their tool surface (chat\n * panel, etc.). Returns `null` when called from a renderer-side host or\n * when the CLI isn't accessible.\n *\n * Available since SDK 2.4.0.\n */\n getCliPaths(): { appExe: string; cliEntry: string } | null;\n\n /**\n * Resolve the absolute path to a bundled resource directory shipped with\n * the app via `extraResources` (e.g. `'drum-samples'`,\n * `'tracktion-presets'`). In dev, resolves to\n * `<projectRoot>/resources/<name>`. In packaged builds, resolves to\n * `<process.resourcesPath>/<name>`.\n *\n * Returns `null` if the host cannot resolve paths in this context\n * (e.g. Electron mocked out in unit tests). Plugins MUST null-check and\n * either degrade gracefully or fall back to a known dev path.\n *\n * Async by design: the renderer-side host proxy round-trips through IPC.\n *\n * @since SDK 2.7.0\n */\n getBundledResourcePath(name: string): Promise<string | null>;\n\n /** Check if LLM access is available (user authenticated + gateway reachable). */\n isLLMAvailable(): Promise<boolean>;\n\n // --- App Tool Bridge ---\n\n /**\n * List the host's registered app tools. Used by plugins (e.g. the chat\n * panel) that want to expose the same surface external AI agents have.\n *\n * `opts.scope` filters by scope tag — scene-scoped consumers pass\n * `'scene'` to hide project-level tools they shouldn't call. When omitted,\n * every tool regardless of scope is returned.\n *\n * `opts.includeDeferred` (since SDK 2.18.0) opts in to tools flagged with\n * `deferLoading` (progressive disclosure). Default `false` mirrors\n * `/api/v1/actions` — the curated core surface. Used by curation layers\n * that promote specific deferred/project tools onto an agent's default\n * declaration set.\n *\n * @since SDK 1.2.0\n */\n listAppTools(opts?: {\n scope?: 'scene' | 'project';\n includeDeferred?: boolean;\n }): Promise<PluginAppTool[]>;\n\n /**\n * Execute a host app tool by name. Delegates to the in-process\n * ToolRegistry — every call (including this one) broadcasts to the\n * UI's `mutations:tool-executed` channel so renderer state stays\n * fresh whether the call mutates or is read-only. Read-only callers\n * pay zero extra cost since the renderer debounces and skips\n * redundant reloads.\n *\n * For scene-scoped tools tagged with `autoBindSceneId`, the host\n * overrides the caller's `sceneId` param with the currently-active\n * scene. That keeps a scene-bound caller from accidentally targeting\n * another scene.\n *\n * `opts.provenance` (since SDK 2.18.0) stamps the originating actor onto\n * every domain event this call emits — pass `'agent'` from autonomous\n * agent loops so the UI orchestrator can gate auto-navigation, `'user'`\n * when proxying a direct user gesture. Omitted = `'system'`.\n *\n * @since SDK 1.2.0\n */\n executeAppTool(\n name: string,\n params: Record<string, unknown>,\n opts?: { provenance?: 'agent' | 'user' }\n ): Promise<PluginAppToolResult>;\n\n /**\n * Monotonic counter that increments on every state mutation\n * (`broadcastMutation('tool-executed', ...)`). Use as a cache key for\n * derived state that depends on the project: when the counter changes,\n * something mutated; when it doesn't, your cache is still valid.\n *\n * Mostly aimed at performance-sensitive callers like ambient-context\n * builders that want to skip re-querying state when nothing has\n * changed. The counter is process-local — it resets on app restart\n * and is not durable across sessions.\n *\n * Implementation detail: the counter is bumped by `mutation-broadcaster`\n * before the broadcaster fires, so a synchronous `getMutationSeq()`\n * call from inside a mutation listener will see the post-bump value.\n *\n * @since SDK 2.6.0\n */\n getMutationSeq(): number;\n\n // --- Preset System ---\n\n /** Get available preset categories for a synth plugin. */\n getPresetCategories(pluginName: string): Promise<string[]>;\n\n /** Get a random preset from a category. */\n getRandomPreset(category: string): Promise<PluginPresetData | null>;\n\n /** Get a specific preset by name from a category. */\n getPresetByName(category: string, name: string): Promise<PluginPresetData | null>;\n\n /** Use LLM to classify a text description into a preset category. */\n classifyPresetCategory(description: string): Promise<string>;\n\n // --- Storage & Settings ---\n\n /** Get absolute path to this plugin's isolated data directory. */\n getDataDirectory(): string;\n\n /** Persisted key-value settings store. */\n settings: PluginSettingsStore;\n\n // --- Sample Pack Distribution ---\n\n /**\n * Return the absolute path to an installed sample pack's root directory,\n * or `null` if the pack is missing OR its installed version doesn't match\n * what the current app build expects.\n *\n * Plugins should treat `null` as \"show the download CTA\"; do NOT fall back\n * to a hardcoded path. The host owns where samples live (currently\n * `<userData>/samples/<installSubdir>/`).\n *\n * Stable packIds: `'sas-drum-pack'`, `'sas-instrument-pack'`. Both packs\n * are downloaded on demand via the host's pack-download flow; see\n * `host.isSamplePackCurrent` and the renderer-side `DownloadPackButton`.\n *\n * @since SDK 2.7.0\n */\n getSamplePackRoot(packId: string): Promise<string | null>;\n\n /**\n * True if the installed version of `packId` matches the version this app\n * build expects. False if the pack is missing OR the installed version\n * differs (older or newer).\n *\n * Plugins call this on activate to decide between rendering their normal\n * UI vs the \"Sample library not installed / Update available\" CTA.\n *\n * @since SDK 2.7.0\n */\n isSamplePackCurrent(packId: string): Promise<boolean>;\n\n /**\n * Return the currently-installed version string for `packId` (e.g. `'1'`,\n * `'2'`), or `null` if the pack is not installed at all. Reads the\n * `_pack-version.json` marker inside the pack's install dir.\n *\n * Useful for distinguishing the \"missing\" CTA from the \"stale, update\n * available\" CTA — plugins can call this when `isSamplePackCurrent`\n * returns false to pick the right empty-state message.\n *\n * @since SDK 2.7.0\n */\n getSamplePackInstalledVersion(packId: string): Promise<string | null>;\n\n /**\n * Trigger a download + install of `packId` via the host's pack system (the\n * same flow `getSamplePackRoot` / `isSamplePackCurrent` report on). Resolves\n * when the install completes or fails. Plugins call this from a \"download\n * library\" CTA instead of reaching into the app's IPC (`window.electronAPI`)\n * directly.\n *\n * @since SDK 2.8.0\n */\n startSamplePackDownload(\n packId: string\n ): Promise<{ success: boolean; error?: string }>;\n\n /**\n * Subscribe to download/install progress for `packId`. Returns an unsubscribe\n * fn. `status` mirrors the host's pack-download states (e.g. `'downloading' |\n * 'extracting' | 'installing' | 'complete' | 'error'`); `progress` is 0-100.\n *\n * @since SDK 2.8.0\n */\n onSamplePackProgress(\n packId: string,\n listener: (progress: {\n packId?: string;\n status: string;\n progress: number;\n message?: string;\n }) => void\n ): UnsubscribeFn;\n\n /**\n * Return the canonical display metadata (`displayName`, `description`,\n * `sizeBytes`) for `packId` from the host's pack registry — the SAME source\n * the host uses to download + version-check the pack. A plugin's download CTA\n * should prefer this over a hardcoded copy so the size/description stay in\n * sync with whatever bundle the host actually ships (no per-version drift).\n * Resolves `null` for an unknown packId.\n *\n * Optional so a plugin built against this SDK still runs on an older host:\n * callers should fall back to their own static copy when it is absent or\n * returns `null`.\n *\n * @since SDK 2.12.0\n */\n getSamplePackInfo?(packId: string): Promise<SamplePackPublicInfo | null>;\n\n /**\n * Per-pack roots of the USER's imported sample packs for `kind`. Each root\n * is laid out exactly like the corresponding stock pack (drums:\n * `<root>/<role>/<file>.wav` + `.txt` sidecars; instruments:\n * `<root>/<category>/<id>/manifest.json`), so resolvers scan them as\n * additional roots alongside `getSamplePackRoot`. `[]` when nothing is\n * imported. User content lives under `<userData>/user-samples/` — strictly\n * separate on disk; stock pack installs never touch it.\n *\n * Optional for older-host compat: feature-check\n * (`host.getUserSampleRoots?.(...)`) and treat absence as `[]`.\n *\n * @since SDK 2.20.0\n */\n getUserSampleRoots?(kind: 'drums' | 'instruments'): Promise<string[]>;\n\n /**\n * Ask the host app to open its sample-import wizard targeting `kind`.\n * Fire-and-forget; renderer-hosted plugins only (the wizard is an app-level\n * modal — the main-process host no-ops). Library changes land as\n * `onSamplePackProgress` events with packId `user:<kind>` and\n * `status: 'complete'`, so subscribe to that to refresh.\n *\n * @since SDK 2.20.0\n */\n openSampleImportWizard?(kind: 'drums' | 'instruments'): void;\n\n // --- Deck playback ---\n //\n // The two playback decks: `'loop-a'` (composition / cue, headphones) and\n // `'loop-b'` (performance / main). These route through the SAME host path\n // the workstation UI uses, so the deck mutual-exclusivity rules\n // (PlaybackRuleEngine) are enforced identically — a plugin cannot bypass\n // them. Used by playback-driven plugins (e.g. the recorder, which starts\n // loop-a so a take has a backing loop). Available on renderer-hosted plugins.\n\n /**\n * Start a deck playing the given scene/transition. Mirrors the workstation's\n * transport play. `contentType` defaults to `'scene'`.\n *\n * @since SDK 2.9.0\n */\n deckPlay(\n deckId: string,\n contentId?: string,\n contentType?: 'scene' | 'transition'\n ): Promise<{ success: boolean; error?: string; code?: string }>;\n\n /**\n * Stop a deck.\n *\n * @since SDK 2.9.0\n */\n deckStop(deckId: string): Promise<{ success: boolean; error?: string }>;\n\n /**\n * Subscribe to per-deck state changes. Each event carries the `deckId`, the\n * `property` that changed (e.g. `'playing'`), and its new `value`. Returns an\n * unsubscribe fn.\n *\n * @since SDK 2.9.0\n */\n onDeckStateChanged(\n listener: (event: { deckId: string; property: string; value: unknown }) => void\n ): UnsubscribeFn;\n\n /**\n * Subscribe to the \"all decks stopped\" engine event (e.g. global transport\n * stop). Returns an unsubscribe fn.\n *\n * @since SDK 2.9.0\n */\n onAllDecksStopped(listener: () => void): UnsubscribeFn;\n\n // --- Scoped Data API ---\n\n /** Get a value from scene-scoped plugin data. */\n getSceneData<T = unknown>(sceneId: string, key: string): Promise<T | null>;\n\n /** Set a value in scene-scoped plugin data. */\n setSceneData(sceneId: string, key: string, value: unknown): Promise<void>;\n\n /** Get all key-value pairs for a scene. */\n getAllSceneData(sceneId: string): Promise<Record<string, unknown>>;\n\n /** Delete a key from scene-scoped plugin data. */\n deleteSceneData(sceneId: string, key: string): Promise<void>;\n\n /** Get the full project-scoped state object. */\n getProjectData<T = unknown>(key: string): Promise<T | null>;\n\n /** Set a project-scoped data value. */\n setProjectData(key: string, value: unknown): Promise<void>;\n\n // --- Notifications & Progress ---\n\n /** Show a toast notification to the user. */\n showToast(type: 'info' | 'success' | 'warning' | 'error', title: string, message?: string): void;\n\n /** Set progress indicator on a specific track. -1 to hide. */\n setProgress(trackId: string, progress: number): void;\n\n /** Set a global status message in the plugin's accordion header. */\n setStatusMessage(message: string | null): void;\n\n /** Request user confirmation via a modal dialog. */\n confirmAction(title: string, message: string): Promise<boolean>;\n\n // --- File System (Phase 2) ---\n\n /** Show a native file open dialog. Requires 'fileDialog' capability. */\n showOpenDialog(options: PluginFileDialogOptions): Promise<string[] | null>;\n\n /** Show a native file save dialog. Requires 'fileDialog' capability. */\n showSaveDialog(options: PluginFileDialogOptions): Promise<string | null>;\n\n /** Download a file to the plugin's data directory. */\n downloadFile(url: string, filename: string, options?: PluginDownloadOptions): Promise<string>;\n\n /** Copy a file into the plugin's data directory. */\n importFile(sourcePath: string, destFilename: string): Promise<string>;\n\n // --- Network (Phase 2, capability-gated) ---\n\n /** Make an HTTP request. Requires 'network' capability with allowedHosts. */\n httpRequest(options: PluginHttpRequestOptions): Promise<PluginHttpResponse>;\n\n // --- Secure Storage (Phase 2) ---\n\n /** Store a secret in the OS keychain (plugin-scoped). */\n storeSecret(key: string, value: string): Promise<void>;\n\n /** Retrieve a secret from the OS keychain (plugin-scoped). */\n getSecret(key: string): Promise<string | null>;\n\n /** Delete a secret from the OS keychain (plugin-scoped). */\n deleteSecret(key: string): Promise<void>;\n\n // --- Sample Library (Phase 2) ---\n\n /** Query the sample library with optional filters. */\n getSamples(filter?: PluginSampleFilter): Promise<PluginSampleInfo[]>;\n\n /** Get a single sample by ID. */\n getSampleById(id: string): Promise<PluginSampleInfo | null>;\n\n /** Import audio files into the sample library. */\n importSamples(filePaths: string[]): Promise<PluginSampleImportResult>;\n\n /** Create a sample track in the active scene. */\n createSampleTrack(sampleId: string, options?: { name?: string }): Promise<PluginTrackHandle>;\n\n /** Delete a sample track. */\n deleteSampleTrack(trackId: string): Promise<void>;\n\n /** Get all sample tracks in the active scene. Re-establishes ownership. */\n getPluginSampleTracks(): Promise<PluginSampleTrackInfo[]>;\n\n /** Time-stretch a sample to a target BPM. Returns the new sample info. */\n timeStretchSample(sampleId: string, targetBpm: number): Promise<PluginSampleInfo>;\n\n /**\n * Fit a sample to the active scene's `(bpm, length_bars)`. Composes:\n * 1. Time-stretch to scene BPM (no-op if already matching).\n * 2. Chop / loop-stitch / passthrough so the resulting clip's duration\n * equals exactly `length_bars × 4 × (60 / bpm)` seconds.\n *\n * Required because the deck loops the clip at the scene's bar boundary —\n * a 4-bar sample dropped into a 2-bar scene used to over-run; a 4-bar\n * sample dropped into an 8-bar scene used to leave 4 bars of silence.\n *\n * The fitted sample is cached in the library by content hash, so\n * subsequent calls for the same `(sample, bpm, bars)` return instantly.\n */\n fitSampleToScene(sampleId: string): Promise<PluginSampleInfo>;\n\n /**\n * Lightweight one-shot sample audition through the cue (headphone) output.\n *\n * Plays the file via a dedicated SimpleLoopPlayer instance in the audio\n * engine — no Tracktion track or clip is created, no BPM matching, no\n * sync. Calling previewSample again with a different file replaces the\n * current preview cleanly. Independent of loop-b: starting/stopping a\n * preview never affects the performance deck and vice versa.\n */\n previewSample(filePath: string): Promise<void>;\n\n /**\n * Stop any in-flight sample preview started by previewSample(). Safe to\n * call when no preview is active — never throws.\n */\n stopPreview(): Promise<void>;\n\n // --- Audio Generation (Phase 2) ---\n\n /** Invoke the host's audio texture generation pipeline. */\n generateAudioTexture(request: PluginAudioTextureRequest): Promise<PluginAudioTextureResult>;\n\n // --- Audio Cue Points + Offset (Migration 060) ---\n\n /**\n * Persist cue points (detected beat positions) for an audio track.\n * Called once after `writeAudioClip` to remember the trim metadata so the\n * UI can later draw beat ticks and snap-to-beat the manual offset.\n *\n * Pass `null` to clear cue points. Throws OWNERSHIP_VIOLATION if the\n * track wasn't created by this plugin.\n */\n setCuePoints(trackId: string, cues: PluginCuePoints | null): Promise<void>;\n\n /** Read cue points previously written by `setCuePoints`. Returns null when none stored. */\n getCuePoints(trackId: string): Promise<PluginCuePoints | null>;\n\n /**\n * Set the manual sample-offset applied to the track's audio clip during\n * playback. Positive shifts later, negative shifts earlier with head\n * silence. Throws OWNERSHIP_VIOLATION if not owned by this plugin.\n */\n setAudioOffsetSamples(trackId: string, offsetSamples: number): Promise<void>;\n\n /** Read the current manual offset (0 if never set). */\n getAudioOffsetSamples(trackId: string): Promise<number>;\n\n // --- Raw / pre-trim audio metadata (stems trim editor) ---\n\n /**\n * Read raw bytes of an audio file written by the host. The path may be\n * `~app/`-relative or project-relative — the host resolves it using the\n * same logic as `writeAudioClip`. Throws FILE_NOT_FOUND if the path\n * can't be resolved or doesn't exist on disk.\n */\n getAudioFileBytes(filePath: string): Promise<ArrayBuffer>;\n\n /** Persist the original (raw, un-trimmed) audio file path for a track. */\n setRawAudioFilePath(trackId: string, filePath: string | null): Promise<void>;\n\n /** Read the raw audio file path persisted via `setRawAudioFilePath`. */\n getRawAudioFilePath(trackId: string): Promise<string | null>;\n\n /**\n * Persist the cue-points detected in the raw (un-trimmed) audio file.\n * Sample positions are in input-file coordinates.\n */\n setRawCuePoints(trackId: string, cues: PluginCuePoints | null): Promise<void>;\n\n /** Read raw-domain cue points persisted via `setRawCuePoints`. */\n getRawCuePoints(trackId: string): Promise<PluginCuePoints | null>;\n\n /** Persist the current trim window inside the raw audio file. */\n setTrimWindow(trackId: string, window: PluginTrimWindow | null): Promise<void>;\n\n /** Read the current trim window persisted via `setTrimWindow`. */\n getTrimWindow(trackId: string): Promise<PluginTrimWindow | null>;\n\n /**\n * Re-trim the raw audio file at the given sample offset and replace the\n * track's audio clip with the new slice. Persists updated trimmed-domain\n * cue points and the new trim window.\n */\n commitTrimWindow(\n trackId: string,\n startSample: number,\n durationSamples: number,\n ): Promise<{ filePath: string; cuePoints: PluginCuePoints | null }>;\n\n // --- Scene Composition ---\n\n /** Trigger bulk composition for the active scene (LLM plans arrangement, creates tracks, generates MIDI). */\n composeScene(options: ComposeSceneOptions): Promise<ComposeSceneResult>;\n\n /** Subscribe to composition progress events (planning, generating, complete, error). */\n onComposeProgress(listener: ComposeProgressListener): UnsubscribeFn;\n\n /** Subscribe to engine ready events (fires when the engine finishes loading tracks after a scene change). */\n onEngineReady(listener: () => void): UnsubscribeFn;\n\n /**\n * Subscribe to external state mutations (CLI, MCP, or HTTP-API tool calls\n * that bypass plugin-host methods). Fires after such a tool finishes,\n * signalling that scene/track DB state may have changed underneath the\n * plugin's local cache. Use it to refresh state that the plugin doesn't\n * own — e.g. re-running adoptSceneTracks() so AI-created tracks become\n * visible without requiring the user to switch scenes.\n *\n * Optional: only the renderer-side host implements this. Main-side\n * plugins should subscribe to the typed domain-event bus instead.\n */\n onAfterAgentMutation?(listener: () => void): UnsubscribeFn;\n\n // --- MIDI Extensions (Phase 2) ---\n\n /** Audition a single note on a track (fire-and-forget preview). */\n auditionNote(trackId: string, pitch: number, velocity: number, durationMs: number): Promise<void>;\n\n // --- Plugin Presets (Phase 2) ---\n\n /** Get presets for this plugin, optionally filtered by category. */\n getPluginPresets(category?: string): Promise<PluginPresetInfo[]>;\n\n /** Save a new preset for this plugin. */\n savePluginPreset(options: SavePluginPresetOptions): Promise<PluginPresetInfo>;\n\n /** Delete a plugin preset by ID. */\n deletePluginPreset(id: string): Promise<void>;\n\n // --- Performance / Logging (Phase 2) ---\n\n /** Log a performance metric. */\n logMetric(name: string, durationMs: number, metadata?: Record<string, unknown>): void;\n\n /** Start a timer. Returns a stop function that logs the duration. */\n startTimer(name: string): () => void;\n\n // --- Stem Splitting ---\n\n /** Split an audio track into stems (vocals, drums, bass, other). Creates new muted tracks. */\n splitStems(trackId: string): Promise<PluginStemSplitResult>;\n\n /** Check if the stem splitter binary is available. */\n isStemSplitterAvailable(): Promise<boolean>;\n\n // --- Audio Recording (capability-gated, since SDK 2.1.0) ---\n\n /**\n * Enumerate audio input devices visible to the engine. Empty list means\n * no input device is available (or the OS denied permission). Requires\n * `audioCapture` capability.\n * @since SDK 2.1.0\n */\n getAudioInputDevices(): Promise<AudioInputDevice[]>;\n\n /**\n * Snapshot of engine state needed to start a recording session. Reads\n * the engine sample rate, the active scene id, the transition-render\n * lock state, and current BPM/bars. Requires `audioCapture`.\n * @since SDK 2.1.0\n */\n getRecordingTargetInfo(): Promise<RecordingTargetInfo>;\n\n /**\n * Begin a recording session. Engine writes integer-PCM WAV chunks to\n * disk; one chunk per call to `markRecordingChunkBoundary`. Each\n * finalized chunk fires a `RecordingChunkFinalizedEvent` to\n * subscribers of `onRecordingChunkFinalized`. Throws\n * AUDIO_CAPTURE_DENIED on permission failure or if no device is\n * available.\n *\n * Pass `deviceId` to override the platform-configured input (rare —\n * only useful for tests or workflows that need a specific device).\n * Omit it to use the platform's selected input from\n * `AudioRoutingConfig.inputDeviceId` — this is the recommended path\n * for plugins post-SDK-2.2.0.\n *\n * @since SDK 2.1.0 (deviceId required) — 2.2.0 made it optional.\n */\n startTrackRecording(deviceId?: string): Promise<void>;\n\n /**\n * Mark the boundary between two recording chunks. The engine closes the\n * currently-open WAV writer and opens a new one; the closed file fires\n * a `RecordingChunkFinalizedEvent` once flush completes. No-op if no\n * recording session is active.\n *\n * Pass `boundaryHostTimeNs` from `DeckBoundaryEvent.boundaryHostTimeNs`\n * for sample-perfect take alignment (Path 2). The engine then splits\n * the chunk at the EXACT recorder-sample that corresponds to that\n * host-time, eliminating the ~5–50 ms of jitter introduced by the\n * legacy \"split wherever the writer is\" path. Required for any\n * workflow that overlays multiple takes (vocalist comping, layered\n * dubs); optional for single-take captures.\n *\n * @since SDK 2.1.0 — 2.4.0 added optional boundaryHostTimeNs.\n */\n markRecordingChunkBoundary(boundaryHostTimeNs?: number): Promise<void>;\n\n /**\n * Stop the active recording session. The final chunk is closed and\n * finalized; its `RecordingChunkFinalizedEvent` fires before this\n * promise resolves. Returns the path of the final chunk (also delivered\n * via the event for symmetry).\n * @since SDK 2.1.0\n */\n stopTrackRecording(): Promise<{ finalChunkPath: string; durationMs: number }>;\n\n /**\n * Subscribe to chunk-finalized events for this plugin's active recording\n * session. Auto-unsubscribed on `deactivate`. Returns unsubscribe fn.\n * @since SDK 2.1.0\n */\n onRecordingChunkFinalized(\n listener: (event: RecordingChunkFinalizedEvent) => void\n ): UnsubscribeFn;\n\n /**\n * Get the platform-configured audio input device, or null when no\n * device is set. Read-only; configured via the assistant's\n * AudioRoutingPanel. Plugins use this to display the current input\n * to the user without exposing their own picker.\n *\n * @since SDK 2.2.0\n */\n getCurrentInputDevice(): Promise<AudioInputDevice | null>;\n\n /**\n * Subscribe to input-device changes (user picks a new mic in the\n * Audio Routing panel). Listeners should refetch via\n * `getCurrentInputDevice()`. Returns an unsubscribe fn.\n *\n * @since SDK 2.4.0\n */\n onInputDeviceChange(listener: () => void): UnsubscribeFn;\n\n /**\n * Get the platform's mic-to-output round-trip latency offset in\n * samples. 0 = uncalibrated. Plugins recording audio apply this via\n * `setAudioOffsetSamples` so takes line up with the source loop.\n *\n * @since SDK 2.2.0\n */\n getRecordingLatencyOffsetSamples(): Promise<number>;\n\n /**\n * Snapshot of the input level for the most recent audio block.\n * Renderer polls at ~30Hz to drive a level meter / scrolling\n * waveform without an event-channel subscription.\n *\n * @since SDK 2.3.0\n */\n getRecordingInputLevel(): Promise<{\n peakDb: number;\n peakLinear: number;\n clipped: boolean;\n active: boolean;\n }>;\n\n /**\n * Reset the latched clip indicator. Safe regardless of whether\n * monitoring or recording is active.\n *\n * @since SDK 2.3.0\n */\n clearRecordingInputClipIndicator(): Promise<void>;\n}\n\n// ============================================================================\n// Stem Splitting Types\n// ============================================================================\n\n/** Stem type identifiers */\nexport type StemType = 'vocals' | 'drums' | 'bass' | 'other';\n\n/** Result of splitting an audio track into stems */\nexport interface PluginStemSplitResult {\n /** Created stem tracks with audio loaded (all auto-muted) */\n stems: PluginStemTrackInfo[];\n}\n\n/** Information about a single stem track created by stem splitting */\nexport interface PluginStemTrackInfo {\n /** The stem type (vocals, drums, bass, other) */\n stemType: StemType;\n /** Track handle for the new stem track */\n track: PluginTrackHandle;\n}\n\n// ============================================================================\n// Exported Plugin Data Types (for .sasproj portability)\n// ============================================================================\n\nexport interface ExportedPluginData {\n pluginId: string;\n scope: 'project' | 'scene' | 'global';\n scopeId: string | null;\n key: string;\n value: string; // JSON-serialized\n}\n\n// ============================================================================\n// Track Types\n// ============================================================================\n\nexport interface CreateTrackOptions {\n /** Display name for the track. Auto-generated if omitted. */\n name?: string;\n /** Musical role hint: 'bass', 'drums', 'lead', 'chords', 'pad', 'arp', 'fx' */\n role?: string;\n /** Load a synth plugin immediately (default: false) */\n loadSynth?: boolean;\n /** Which synth to load (default: 'Surge XT'). Ignored if loadSynth=false. */\n synthName?: string;\n /**\n * Stable plugin identifier for a custom instrument (VST3 TUID or AU component ID).\n * If provided with loadSynth=true, loads this plugin instead of synthName.\n * Null/undefined = use default (Surge XT).\n */\n instrumentPluginId?: string | null;\n /** Metadata stored in DB. Plugins can use this for plugin-specific data. */\n metadata?: Record<string, unknown>;\n}\n\nexport interface PluginTrackHandle {\n /** Tracktion engine track ID (stable, GUID-based) */\n id: string;\n /** Display name */\n name: string;\n /** Database row ID */\n dbId: string;\n /** Musical role (if set) */\n role?: string;\n /** Prompt from tracks table (fallback when plugin_data not yet populated) */\n prompt?: string;\n /** Custom instrument plugin ID (null = default Surge XT) */\n instrumentPluginId?: string | null;\n /** Custom instrument display name (null = Surge XT) */\n instrumentName?: string | null;\n}\n\n/**\n * One source track offered by `listImportableTracks`, already filtered to the\n * calling panel's type. The host computes the gate; the UI only renders it.\n * @since SDK 2.13.0\n */\nexport interface ImportCandidateTrack {\n /** Source track's engine track id (the selector passed back to importTrack). */\n trackId: string;\n /** Source track's DB row id (globally unique; good React key). */\n dbId: string;\n /** Display name shown in the modal row. */\n name: string;\n /** Musical role if set (drives the row icon). */\n role?: string;\n /** True when this track can be copied into the active scene as-is. */\n importable: boolean;\n /** Why the track is disabled (shown as a tooltip). Present iff `!importable`. */\n disabledReason?: string;\n}\n\n/**\n * One track in a specific scene, returned by `host.listSceneFamilyTracks`,\n * already narrowed to the calling panel's family. Unlike `ImportCandidateTrack`\n * it carries NO import gate — the crossfade picker lists every same-family track\n * in the origin/target scene regardless of key/length. @since SDK 2.22.0\n */\nexport interface SceneFamilyTrack {\n /** Track's DB row id — the selector for getTrackSound + crossfade metadata. */\n dbId: string;\n /** Display name shown in the picker. */\n name: string;\n /** Musical role if set — used to enforce same-role crossfade pairing. */\n role?: string;\n /** Generation prompt, when the track was AI-generated. The crossfade picker\n * shows this as the primary label (users recognise tracks by prompt, not id).\n * @since SDK 2.27.0 */\n prompt?: string;\n}\n\n/**\n * One OTHER scene and its candidate tracks (already type-filtered). Scenes with\n * zero candidates of the panel's type are omitted by the host.\n * @since SDK 2.13.0\n */\nexport interface ImportCandidateScene {\n /** Source scene's engine scene id. */\n sceneId: string;\n /** Source scene's display name. */\n sceneName: string;\n /** Candidate tracks of this panel's type (may include disabled ones). */\n tracks: ImportCandidateTrack[];\n /**\n * True for the synthetic \"this scene — other panels\" entry: the ACTIVE\n * scene's MIDI tracks owned by OTHER panels. Importing one re-sounds the part\n * on the calling panel's instrument (via `readImportableTrackMidi` +\n * `writeMidiClip`) rather than faithfully copying it. Absent/false for\n * ordinary cross-scene entries. @since SDK 2.20.0\n */\n sameScene?: boolean;\n}\n\n/**\n * A source track's current sound, as returned by `host.getTrackSound`. The\n * discriminant matches the panel that reads it: drums → 'sample', instruments →\n * 'instrument', synths → 'preset'. `label` is the human name for the History row.\n * @since SDK 2.14.0\n */\n/**\n * How a synth `state` blob is serialized. `valuetree` is Tracktion's wrapped\n * format (default Surge XT presets); `raw` is the plugin's own\n * getStateInformation format (third-party instruments). Absent ⇒ `valuetree`,\n * for backward compatibility with history recorded before SDK 2.15.0.\n * @since SDK 2.15.0\n */\nexport type SynthStateType = 'raw' | 'valuetree';\n\nexport type TrackSoundSnapshot =\n | { kind: 'sample'; samplePath: string; label: string }\n | { kind: 'instrument'; displayName: string; instrumentId: string | null; zones: InstrumentZone[]; label: string }\n | { kind: 'preset'; state: string; label: string; stateType?: SynthStateType };\n\n/** Options for `PluginHost.listImportableTracks`. @since SDK 2.13.0 */\nexport interface ListImportableTracksOptions {\n /**\n * Coarse content family. 'midi' = synth/drum/instrument, 'audio' = stems,\n * 'sample' = loops. Defaults are derived from the calling plugin id, so\n * panels normally pass nothing.\n */\n family?: 'midi' | 'audio' | 'sample';\n /**\n * When true, prepend the active scene's MIDI tracks owned by OTHER panels as a\n * `sameScene` entry (the cross-panel re-sound source). Off by default so the\n * plain cross-scene import is unchanged. MIDI panels only. @since SDK 2.20.0\n */\n includeSameScene?: boolean;\n}\n\nexport interface PluginTrackInfo extends PluginTrackHandle {\n /** Is track muted? */\n muted: boolean;\n /** Is track soloed? */\n soloed: boolean;\n /** Volume (linear 0-1) */\n volume: number;\n /** Pan (-1 to 1) */\n pan: number;\n /** Loaded plugins on this track */\n plugins: PluginSynthInfo[];\n /** Has MIDI clips? */\n hasMidi: boolean;\n /** Has audio clips? */\n hasAudio: boolean;\n}\n\nexport interface PluginSynthInfo {\n index: number;\n name: string;\n type: string; // 'VST3' | 'AudioUnit' | 'Internal'\n enabled: boolean;\n}\n\n// ============================================================================\n// Real-time Track State Types\n// ============================================================================\n\n/** Real-time runtime state of a track (pushed from engine) */\nexport interface PluginTrackRuntimeState {\n id: string;\n muted: boolean;\n solo: boolean;\n volume: number;\n pan: number;\n}\n\n/** Listener for real-time track state changes */\nexport type TrackStateChangeListener = (trackId: string, state: PluginTrackRuntimeState) => void;\n\n// ============================================================================\n// FX Detail Types (SDK-friendly re-export)\n// ============================================================================\n\n/** Per-category FX detail state */\nexport interface PluginFxCategoryDetailState {\n enabled: boolean;\n presetIndex: number; // 0-4\n dryWet: number; // 0.0-1.0\n}\n\n/** Full FX detail state for a track — one entry per FX category */\nexport type PluginTrackFxDetailState = Record<string, PluginFxCategoryDetailState>;\n\n// ============================================================================\n// MIDI Types\n// ============================================================================\n\nexport interface MidiClipData {\n /** Start time in seconds */\n startTime: number;\n /** End time in seconds */\n endTime: number;\n /** BPM for beat<->time conversion */\n tempo: number;\n /** MIDI notes */\n notes: PluginMidiNote[];\n}\n\nexport interface PluginMidiNote {\n /** MIDI pitch 0-127 */\n pitch: number;\n /** Start position in quarter-note beats (0 = beginning of clip) */\n startBeat: number;\n /** Duration in quarter-note beats */\n durationBeats: number;\n /** Velocity 1-127 */\n velocity: number;\n /** MIDI channel 0-15 (default: 0) */\n channel?: number;\n}\n\nexport interface MidiWriteResult {\n /** Number of notes written */\n notesInserted: number;\n /** Actual bars covered */\n bars: number;\n}\n\n/**\n * One clip returned by {@link PluginHost.readMidiNotes}. `endTime - startTime`\n * (seconds) is the clip's loop span; round-trip it back into\n * {@link MidiClipData} on save so an edit never changes the clip length.\n * @since SDK 2.15.0\n */\nexport interface ReadMidiClip {\n /** Clip start in seconds (engine timeline). */\n startTime: number;\n /** Clip end in seconds. Loop span = endTime - startTime. */\n endTime: number;\n /** Beat-based notes, identical shape to {@link MidiClipData.notes}. */\n notes: PluginMidiNote[];\n}\n\n/**\n * Result of {@link PluginHost.readMidiNotes}: every clip on the track. Drum /\n * instrument / synth tracks are single-clip, so callers normally use\n * `clips[0]`; the array form mirrors the engine and is future-proof.\n * @since SDK 2.15.0\n */\nexport interface ReadMidiResult {\n clips: ReadMidiClip[];\n}\n\n/**\n * Options for {@link PluginHost.exportTracksAsMidiBundle}.\n * @since SDK 1.1.0\n */\nexport interface ExportMidiBundleOptions {\n /** Default ZIP filename suggested in the save dialog (without extension). */\n defaultName?: string;\n}\n\n/**\n * Result of {@link PluginHost.exportTracksAsMidiBundle}.\n * @since SDK 1.1.0\n */\nexport type ExportMidiBundleResult =\n | { success: true; filePath: string; trackCount: number; skippedCount: number }\n | { success: false; canceled: true }\n | { success: false; canceled?: false; error: string };\n\n// ============================================================================\n// Audio Processing Bridge (SDK 1.2.0 — see ai-orchestration-design.md §16)\n// ============================================================================\n\n/** @since SDK 1.2.0 */\nexport interface ExportTrackAudioResult {\n path: string;\n bpm: number;\n durationMs: number;\n fromCopyFastPath?: boolean;\n}\n\n/** @since SDK 1.2.0 */\nexport interface ProcessAudioResult {\n outputPath: string;\n}\n\n/** @since SDK 1.2.0 */\nexport type AudioProcessingOp =\n | { tool: 'normalize' }\n | { tool: 'compress'; params?: { threshold?: number; ratio?: number } }\n | { tool: 'eq'; params?: { low_gain?: number; mid_gain?: number; high_gain?: number } }\n | { tool: 'reverb'; params?: { room_size?: number; dry_wet?: number } }\n | { tool: 'pitch-shift'; params: { semitones: number } }\n | { tool: 'time-stretch'; params: { target_bpm: number } }\n | { tool: 'filter'; params: { type: 'lowpass' | 'highpass'; cutoff: number } }\n | { tool: 'gain'; params: { db: number } }\n | { tool: 'limit' }\n | { tool: 'trim'; params?: { start?: number; end?: number } };\n\nexport interface PostProcessOptions {\n /** Snap notes to grid (default: true) */\n quantize?: boolean;\n /** Grid size: '1/4', '1/8', '1/16', '1/32', '1/8T', '1/16T' (default: '1/16') */\n quantizeGrid?: string;\n /** Quantize strength 0-100 (default: 75) */\n quantizeStrength?: number;\n /** Swing amount 0-100 (default: 0) */\n swing?: number;\n /** Humanize timing/velocity variation 0-100 (default: 0) */\n humanize?: number;\n /** Enforce diatonic scale (default: false). Uses scene key/mode. */\n enforceScale?: boolean;\n /** Clamp notes to pitch range [low, high] */\n clampRegister?: [number, number];\n /** Remove overlapping notes on same pitch/channel (default: true) */\n removeOverlaps?: boolean;\n}\n\n// ============================================================================\n// Context Types\n// ============================================================================\n\nexport interface MusicalContext {\n key: string; // 'C', 'D', 'Eb', 'F#', etc.\n mode: string; // 'major', 'minor', 'dorian', 'mixolydian', etc.\n bpm: number; // 20-960\n bars: number; // Scene length in bars\n genre: string | null; // 'Drum & Bass', 'Lo-fi Hip Hop', etc.\n timeSignature: string; // '4/4', '3/4', '6/8'\n chordProgression: PluginChordTiming[];\n /**\n * The scene's natural-language contract prompt (e.g. \"dark psytrance,\n * driving 130 BPM, claustrophobic\"). Null when the scene has no\n * contract set yet. Auto-prefixed to the LLM by `host.generateWithLLM`\n * so every per-track generation sees the scene-level intent without\n * each plugin having to plumb it through manually.\n * @since SDK 1.2.0\n */\n contractPrompt: string | null;\n}\n\nexport interface PluginChordTiming {\n /** Chord symbol: 'Cm7', 'G', 'Fmaj7', etc. */\n symbol: string;\n /** Start position in quarter notes */\n startQn: number;\n /** End position in quarter notes */\n endQn: number;\n}\n\n/** Full generation context — includes concurrent track MIDI data */\nexport interface PluginGenerationContext {\n chordProgression: {\n key: { tonic: string; mode: string };\n chordsWithTiming: PluginChordTiming[];\n genre: string | null;\n };\n concurrentTracks: PluginConcurrentTrackInfo[];\n /**\n * Count of tracks the host had to drop entirely from `concurrentTracks`\n * because their notes pushed the running total past the cross-track\n * budget. Panels should disclose this to the LLM (e.g. \"… N additional\n * tracks omitted to fit token budget\") so the model knows it is\n * working with partial context.\n * @since SDK 1.2.0\n */\n truncatedTrackCount?: number;\n}\n\nexport interface PluginConcurrentTrackInfo {\n trackId: string;\n role: string | undefined;\n presetCategory: string | null;\n /** Notes organized by which chord they fall under */\n notesByChord: PluginChordSegment[];\n /**\n * The user-typed prompt that produced this track's MIDI (from\n * `tracks.prompt`). Lets the LLM see *intent* alongside the notes —\n * \"punchy 909 kick\" carries more meaning than the kick MIDI alone.\n * @since SDK 1.2.0\n */\n prompt?: string;\n /**\n * True when the host capped this track's notes (per-track budget).\n * The `notesByChord` payload is a prefix of the real content; the\n * total dropped count is `originalNoteCount - sum(notesByChord.notes.length)`.\n * @since SDK 1.2.0\n */\n truncated?: boolean;\n /** The track's full note count before per-track truncation. */\n originalNoteCount?: number;\n}\n\nexport interface PluginChordSegment {\n chord: string;\n chordRangeQn: [number, number];\n notes: PluginMidiNote[];\n}\n\n// ============================================================================\n// Transport Types\n// ============================================================================\n\nexport interface TransportEvent {\n type: 'play' | 'stop' | 'pause' | 'bpmChange' | 'positionChange';\n bpm?: number;\n position?: number; // in seconds\n isPlaying?: boolean;\n}\n\nexport interface DeckBoundaryEvent {\n deckId: string; // 'loop-a', 'loop-b'\n bar: number; // Current bar number (1-based)\n beat: number; // Current beat within bar (1-based)\n loopCount: number; // How many loops completed\n /**\n * Stream-time sample index at which the loop wrap was detected in the\n * audio thread (engine's AudioBoundaryProbe). Undefined when the\n * audio-thread anchor was unavailable. @since SDK 2.4.0\n */\n boundaryAudioSamplePosition?: number;\n /**\n * Monotonic host-time (nanoseconds) at the audio block in which the\n * loop wrap was detected. Same clock as\n * `juce::AudioIODeviceCallbackContext::hostTimeNs`. Pair with\n * `markRecordingChunkBoundary(boundaryHostTimeNs)` for sample-perfect\n * take alignment. @since SDK 2.4.0\n */\n boundaryHostTimeNs?: number;\n}\n\nexport interface PluginTransportState {\n isPlaying: boolean;\n isPaused: boolean;\n bpm: number;\n position: number; // in seconds\n timeSignature: string;\n}\n\n/**\n * Mono peak level for a single track, as reported by `getTrackLevels()`.\n * Drives the cosmetic per-track strip meters. `peakDb` is the max of the\n * L/R channels, floored at -120 (the \"no signal\" sentinel).\n * @since SDK 2.21.0\n */\nexport interface PluginTrackLevel {\n /** Tracktion engine track id — matches `PluginTrackHandle.id`. */\n trackId: string;\n /** Mono peak in dBFS (max of L/R), floored at -120. */\n peakDb: number;\n /** Latched overload since the last poll. */\n clipped: boolean;\n}\n\nexport interface PluginSceneInfo {\n id: string;\n name: string;\n isMuted: boolean;\n}\n\n/** Scene-level contract/context state passed to plugin UIs as a prop */\nexport interface PluginSceneContext {\n /** Whether a contract has been generated (genre or contractPrompt exists AND chords exist) */\n hasContract: boolean;\n /** Original user prompt text (e.g., \"dark psytrance\"). Null if none. */\n contractPrompt: string | null;\n /** Extracted genre. Null if none. */\n genre: string | null;\n /** Musical key. Null if no chord progression. */\n key: { tonic: string; mode: string } | null;\n /** Chord symbols (e.g., [\"Cm\", \"Fm\", \"G\"]). Empty if no chords. */\n chords: string[];\n /** BPM from project tempo */\n bpm: number;\n /** Scene length in bars */\n bars: number;\n /** Whether any synth tracks exist in this scene */\n hasTracks: boolean;\n /** Whether bulk generation is currently in progress */\n isBulkGenerating: boolean;\n /**\n * Scene kind. A 'transition' scene bridges two other scenes (the\n * transition-as-scene feature) and unlocks the crossfade-track UI in the\n * instrument panels; ordinary scenes are 'scene'. Absent on older hosts.\n * @since SDK 2.22.0\n */\n sceneType?: 'scene' | 'transition';\n /** For a transition scene, the DB id of the scene it bridges FROM (origin). Null otherwise. @since SDK 2.22.0 */\n transitionFromSceneId?: string | null;\n /** For a transition scene, the DB id of the scene it bridges TO (target). Null otherwise. @since SDK 2.22.0 */\n transitionToSceneId?: string | null;\n}\n\n/** Placeholder track state for the progressive bulk-add UX */\nexport interface BulkAddPlaceholderTrack {\n id: string;\n planIndex: number;\n role: string;\n description: string;\n status: 'planned' | 'creating' | 'completed' | 'failed';\n error?: string;\n}\n\nexport type TransportEventListener = (event: TransportEvent) => void;\nexport type DeckBoundaryListener = (event: DeckBoundaryEvent) => void;\nexport type SceneChangeListener = (sceneId: string | null) => void;\nexport type UnsubscribeFn = () => void;\n\n// ============================================================================\n// LLM Types\n// ============================================================================\n\nexport interface LLMGenerationRequest {\n /** System prompt (instructions, role, output format) */\n system: string;\n /** User prompt (the actual request) */\n user: string;\n /** Max tokens for response (host may cap this) */\n maxTokens?: number;\n /** Expected response format hint */\n responseFormat?: 'text' | 'json';\n /**\n * If true, the host will NOT auto-prefix the user prompt with musical\n * context (key, BPM, chords, genre, etc.). Default: false (context IS\n * prefixed automatically).\n */\n skipContextPrefix?: boolean;\n}\n\nexport interface LLMGenerationResult {\n /** Raw response text */\n content: string;\n /** Tokens consumed */\n tokensUsed: number;\n /** Model that generated the response */\n model: string;\n}\n\n// ----------------------------------------------------------------------------\n// Tool-use LLM types (Gemini-native shape, since SDK 2.4.0)\n// ----------------------------------------------------------------------------\n//\n// Plugins that want a Claude-Code / VS-Code-agent-mode loop call\n// `host.generateWithLLMTools(...)` with these shapes. The host forwards to\n// the gateway's Gemini-native passthrough endpoint, where Google's API key\n// is added centrally — plugins never see the raw key. Token usage is\n// tracked by the gateway just like `generateWithLLM`.\n//\n// Shapes mirror Gemini's REST `generateContent` surface deliberately. We do\n// not pull in `@google/genai` as a dependency: with the gateway as a\n// passthrough and the host owning auth, an SDK adds no value over typed\n// JSON, and we keep tighter control of breaking changes.\n\n/** A single part of a Gemini-style content block. */\nexport interface LLMPart {\n /** Plain text. Mutually exclusive with functionCall / functionResponse. */\n text?: string;\n /** A tool/function the model is asking the host to invoke. */\n functionCall?: {\n name: string;\n args: Record<string, unknown>;\n /**\n * Opaque signature returned by Gemini 3+ tool-use models. Must be echoed\n * verbatim when the assistant turn is replayed on a later iteration, or\n * the API rejects the request with a 400 (\"Function call is missing a\n * thought_signature in functionCall parts.\"). Pre-Gemini-3 models leave\n * this undefined; preserving it round-trip is safe across families.\n */\n thoughtSignature?: string;\n };\n /** The result of a tool call, fed back into the loop on the next turn. */\n functionResponse?: {\n name: string;\n response: Record<string, unknown>;\n };\n}\n\nexport interface LLMContent {\n /** 'user' = user/tool-result; 'model' = assistant. */\n role: 'user' | 'model';\n parts: LLMPart[];\n}\n\nexport interface LLMFunctionDeclaration {\n name: string;\n description: string;\n /** JSON Schema. Use `type: 'object'` with `properties` for any tool. */\n parameters: {\n type: 'object';\n properties?: Record<string, unknown>;\n required?: string[];\n };\n}\n\nexport interface LLMTool {\n functionDeclarations: LLMFunctionDeclaration[];\n}\n\nexport interface LLMGenerationConfig {\n temperature?: number;\n topP?: number;\n topK?: number;\n maxOutputTokens?: number;\n}\n\nexport interface LLMSystemInstruction {\n parts: { text: string }[];\n}\n\nexport interface LLMToolUseRequest {\n /** Gemini model id (e.g. 'gemini-2.5-flash'). */\n model: string;\n /** Conversation so far, including any tool-result turns. */\n contents: LLMContent[];\n /** System prompt as Gemini-native systemInstruction. */\n systemInstruction?: LLMSystemInstruction;\n /** Tool declarations the model may call. */\n tools?: LLMTool[];\n /** Optional tool-call mode override. */\n toolConfig?: {\n functionCallingConfig?: {\n mode?: 'AUTO' | 'ANY' | 'NONE';\n allowedFunctionNames?: string[];\n };\n };\n generationConfig?: LLMGenerationConfig;\n}\n\nexport interface LLMUsageMetadata {\n promptTokenCount: number;\n candidatesTokenCount: number;\n totalTokenCount: number;\n}\n\nexport interface LLMCandidate {\n content: LLMContent;\n finishReason?: string;\n index?: number;\n}\n\nexport interface LLMToolUseResponse {\n candidates: LLMCandidate[];\n usageMetadata?: LLMUsageMetadata;\n}\n\n// ============================================================================\n// Preset Types\n// ============================================================================\n\nexport interface PluginPresetData {\n name: string;\n category: string;\n /** Base64-encoded plugin state — pass to setPluginState() */\n state: string;\n}\n\n/** Result of shufflePreset() — the new preset that was applied */\nexport interface ShufflePresetResult {\n presetName: string;\n presetCategory: string;\n}\n\n/**\n * One entry in a track's in-session \"sound history\" — the data behind the\n * TrackRow ↩ back-arrow and the drawer \"History\" tab (see `useSoundHistory`).\n *\n * `descriptor` is opaque to the SDK: each generator plugin defines its own shape\n * (a drum sample path string, an instrument `{ displayName, zones }`, a synth\n * `{ pluginIndex, stateBase64 }`) and is the value handed back to the plugin's\n * `applySound` callback to re-apply the sound.\n */\nexport interface SoundHistoryEntry {\n /** Human-readable label shown in the History list (filename, preset/instrument name). */\n label: string;\n /** Opaque, plugin-defined value used to re-apply this sound. */\n descriptor: unknown;\n /** User-starred. Favorited entries are never auto-evicted by the history cap. */\n favorite?: boolean;\n}\n\n// ============================================================================\n// Settings Types\n// ============================================================================\n\nexport interface PluginSettingsSchema {\n type: 'object';\n properties: Record<string, SettingDefinition>;\n}\n\nexport interface SettingDefinition {\n type: 'string' | 'number' | 'boolean' | 'select';\n label: string;\n description?: string;\n default?: unknown;\n /** For 'select' type */\n options?: Array<{ label: string; value: string }>;\n /** For 'number' type */\n min?: number;\n max?: number;\n}\n\nexport interface PluginSettingsStore {\n get<T>(key: string, defaultValue: T): T;\n set(key: string, value: unknown): void;\n getAll(): Record<string, unknown>;\n /** Subscribe to settings changes. Returns unsubscribe fn. */\n onChange(listener: (key: string, value: unknown) => void): UnsubscribeFn;\n}\n\n// ============================================================================\n// Error Types\n// ============================================================================\n\nexport type PluginErrorCode =\n | 'NOT_OWNED' // Tried to modify a track not owned by this plugin\n | 'TRACK_NOT_FOUND' // Track ID doesn't exist in engine\n | 'TRACK_LIMIT_EXCEEDED' // Plugin has too many tracks\n | 'NO_ACTIVE_SCENE' // No scene selected\n | 'ENGINE_ERROR' // Tracktion engine call failed\n | 'INVALID_MIDI' // Malformed MIDI data\n | 'FILE_NOT_FOUND' // Audio file doesn't exist\n | 'INVALID_FORMAT' // Unsupported audio format\n | 'PLUGIN_NOT_FOUND' // VST/AU plugin not installed\n | 'LLM_BUDGET_EXCEEDED' // Over token limit\n | 'LLM_UNAVAILABLE' // Gateway unreachable\n | 'NOT_AUTHENTICATED' // User not logged in\n | 'TIMEOUT' // Operation timed out\n | 'CANCELLED' // User cancelled the operation\n | 'INCOMPATIBLE' // Plugin requires newer SDK version\n | 'CAPABILITY_DENIED' // Plugin lacks required capability\n | 'SECRET_NOT_FOUND' // Secret key doesn't exist\n | 'VALIDATION_ERROR' // Inputs failed schema/format validation\n | 'AUDIO_CAPTURE_DENIED'; // OS-level mic permission denied or input device unavailable\n\nexport class PluginError extends Error {\n public readonly code: PluginErrorCode;\n public readonly details?: Record<string, unknown>;\n\n constructor(\n code: PluginErrorCode,\n message: string,\n details?: Record<string, unknown>\n ) {\n super(message);\n this.name = 'PluginError';\n this.code = code;\n this.details = details;\n }\n}\n\n// ============================================================================\n// Plugin Manifest (on-disk plugin.json)\n// ============================================================================\n\nexport interface PluginManifest {\n id: string;\n displayName: string;\n version: string;\n description: string;\n generatorType: GeneratorType;\n main: string; // e.g., 'dist/index.js'\n renderer?: string; // e.g., 'dist/ui.bundle.js' (UMD bundle for renderer)\n icon?: string; // e.g., 'assets/icon.svg'\n author?: string;\n license?: string;\n minHostVersion?: string;\n capabilities?: PluginCapabilities;\n settings?: Record<string, SettingDefinition>;\n builtIn?: boolean;\n repository?: string; // e.g., 'https://github.com/user/my-plugin'\n}\n\nexport interface PluginCapabilities {\n requiresLLM?: boolean;\n requiresSurgeXT?: boolean;\n requiresNetwork?: boolean;\n /** Allowed network hosts for httpRequest (e.g., ['api.splice.com']) */\n network?: { allowedHosts?: string[] };\n /** Plugin needs native file dialog access */\n fileDialog?: boolean;\n /**\n * Plugin needs microphone / line-in capture. Gates the recording host\n * methods (getAudioInputDevices, startTrackRecording, etc).\n * @since SDK 2.1.0\n */\n audioCapture?: boolean;\n}\n\n// ============================================================================\n// Audio Recording (since SDK 2.1.0)\n// ============================================================================\n\n/**\n * Audio input device exposed by the audio engine. The `deviceId` is the\n * stable identifier returned by JUCE's AudioDeviceManager and accepted as\n * the device argument to `startTrackRecording`.\n * @since SDK 2.1.0\n */\nexport interface AudioInputDevice {\n /** Stable device identifier — passed back to startTrackRecording. */\n deviceId: string;\n /** Human-readable device name (e.g., \"MacBook Pro Microphone\", \"USB Mic\"). */\n label: string;\n /** True if this is the system default input device. */\n isDefault: boolean;\n /** Number of input channels the device supports (1 = mono, 2 = stereo). */\n channelCount: number;\n}\n\n/**\n * Engine state snapshot that an audio-recording plugin needs before\n * starting a session.\n * @since SDK 2.1.0\n */\nexport interface RecordingTargetInfo {\n /** Engine device sample rate, e.g. 44100 or 48000. */\n engineSampleRate: number;\n /** Active scene id, or null when no scene is selected. */\n sceneId: string | null;\n /** True when a transition render lock is held — recorder must refuse. */\n isRenderLocked: boolean;\n /** Current project BPM. */\n bpm: number;\n /** Active scene length in bars (4/4 assumed), or null when no scene. */\n bars: number | null;\n /**\n * Sample-perfect-recording compatibility (Path 2 gate). When false,\n * the recorder must refuse to start a session and surface\n * `recordingCompatibilityReason` to the user — input + output\n * devices cannot be sample-aligned.\n * @since SDK 2.4.0\n */\n canRecordSamplePerfect?: boolean;\n recordingCompatibilityReason?: string;\n}\n\n/**\n * Event payload fired when the engine finalizes a recording chunk WAV\n * file (either at a boundary mark or at session stop).\n * @since SDK 2.1.0\n */\nexport interface RecordingChunkFinalizedEvent {\n /** Absolute path to the finalized WAV file on disk. */\n filePath: string;\n /** Zero-based chunk index within the active session. */\n chunkIndex: number;\n /** Duration of this chunk in milliseconds. */\n durationMs: number;\n /** WAV sample rate. */\n sampleRate: number;\n /** WAV channel count. */\n channels: number;\n /**\n * Sample-perfect-recording metadata (Path 2). When the chunk was\n * closed via a host-time-anchored `markRecordingChunkBoundary` call,\n * carries recorder-local sample positions plus the host-time at\n * which the boundary fired. Undefined / -1 means the boundary\n * lacked a host-time anchor (legacy or stop-driven finalize).\n * @since SDK 2.4.0\n */\n recorderSampleStart?: number;\n recorderSampleEnd?: number;\n boundaryHostTimeNs?: number;\n}\n\n// ============================================================================\n// Phase 2: File System Types\n// ============================================================================\n\nexport interface PluginFileDialogOptions {\n title?: string;\n defaultPath?: string;\n filters?: Array<{ name: string; extensions: string[] }>;\n /** For open dialog: allow selecting multiple files */\n multiSelections?: boolean;\n /** For open dialog: allow selecting directories */\n directories?: boolean;\n}\n\nexport interface PluginDownloadOptions {\n /** HTTP headers to include */\n headers?: Record<string, string>;\n /** Overwrite if file exists (default: false) */\n overwrite?: boolean;\n}\n\n// ============================================================================\n// Phase 2: Network Types\n// ============================================================================\n\nexport interface PluginHttpRequestOptions {\n url: string;\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n headers?: Record<string, string>;\n body?: string | Record<string, unknown>;\n /** Timeout in milliseconds (default: 30000) */\n timeoutMs?: number;\n}\n\nexport interface PluginHttpResponse {\n status: number;\n statusText: string;\n headers: Record<string, string>;\n body: string;\n}\n\n// ============================================================================\n// Phase 2: Sample Library Types\n// ============================================================================\n\nexport interface PluginSampleFilter {\n bpm?: number;\n key?: { tonic: string; mode?: string };\n category?: string;\n searchQuery?: string;\n}\n\nexport interface PluginSampleInfo {\n id: string;\n filename: string;\n filePath: string;\n category: string | null;\n bpm: number | null;\n keyTonic: string | null;\n keyMode: string | null;\n durationSeconds: number | null;\n fileSizeBytes: number | null;\n tags: string[] | null;\n}\n\nexport interface PluginSampleImportResult {\n imported: number;\n skipped: number;\n errors: string[];\n}\n\n/** Sample track with associated sample metadata (returned by getPluginSampleTracks) */\nexport interface PluginSampleTrackInfo {\n track: PluginTrackHandle;\n sample: PluginSampleInfo;\n volume: number;\n pan: number;\n}\n\n// ============================================================================\n// Phase 2: Audio Generation Types\n// ============================================================================\n\nexport interface PluginAudioTextureRequest {\n /** Text prompt describing the audio texture */\n prompt: string;\n /** Duration in seconds (default: scene length) */\n durationSeconds?: number;\n /** Target BPM (default: project BPM) */\n bpm?: number;\n}\n\nexport interface PluginAudioTextureResult {\n /** Path to the generated audio file */\n filePath: string;\n /** Duration of the generated audio in seconds */\n durationSeconds: number;\n /**\n * Beat positions inside the generated audio file plus the detected BPM.\n * Sample positions are relative to the file at `filePath`. Null when the\n * audio-processor did not surface detection data (older binary, fallback\n * path, or processing failed). Persist via `host.setCuePoints` after the\n * clip is written so the OffsetScrubber UI can read them later.\n */\n cuePoints: PluginCuePoints | null;\n /**\n * Path to the un-trimmed (raw) Lyria output. Used by the stems\n * trim editor to draw the full waveform. Persist via\n * `host.setRawAudioFilePath`. Null when no raw file is available.\n */\n rawFilePath?: string | null;\n /** Same beats as `cuePoints` in raw-file sample coordinates. */\n rawCuePoints?: PluginCuePoints | null;\n /**\n * Auto-detected start of the trim window inside the raw file (sample\n * offset). Null when detection was skipped.\n */\n inputStartSample?: number | null;\n}\n\n/**\n * Cue-points sidecar surfaced by the audio-processor `trim` command —\n * sample positions for each detected beat inside the generated WAV.\n * Mirrors the canonical `CuePoints` shape from the assistant; duplicated\n * here so external plugins don't reach into sas-app internals.\n */\nexport interface PluginCuePoints {\n /** Schema version (currently 1). */\n schema: 1;\n /** Sample rate the beat positions are expressed in. */\n sample_rate: number;\n /** Detected BPM (may differ from project BPM). Null when detection failed. */\n detected_bpm: number | null;\n /** Sample position of bar 1 / beat 1 inside the clip. */\n downbeat_sample: number;\n /** Monotone-increasing array of beat positions in samples. */\n beats: number[];\n /** ISO-8601 timestamp of when detection ran. */\n detected_at: string;\n}\n\n/**\n * A trim window inside a raw (un-trimmed) audio file. `start_sample` is\n * the offset from the start of the raw file; `duration_samples` is the\n * length of the trimmed slice. Both are in raw-file sample coordinates.\n */\nexport interface PluginTrimWindow {\n start_sample: number;\n duration_samples: number;\n}\n\n// ============================================================================\n// Scene Composition Types\n// ============================================================================\n\n/** Options for composing a full scene arrangement via LLM. */\nexport interface ComposeSceneOptions {\n /** The contract prompt / musical direction for the arrangement. */\n contractPrompt: string;\n /** Genre hint (e.g. 'techno', 'jazz'). Optional. */\n genre?: string | null;\n}\n\n/** Result from a scene composition. */\nexport interface ComposeSceneResult {\n /** Whether the composition completed successfully. */\n success: boolean;\n /** Number of tracks created. */\n tracksCreated: number;\n /** Error message if not successful. */\n error?: string;\n}\n\n/** Listener for composition progress events. */\nexport type ComposeProgressListener = (event: ComposeProgressEvent) => void;\n\n/** Progress event emitted during scene composition. */\nexport interface ComposeProgressEvent {\n /** Current phase: 'planning' (LLM deciding tracks), 'generating' (creating MIDI), 'complete', 'error'. */\n phase: 'planning' | 'generating' | 'complete' | 'error';\n /** Per-track placeholder state (available once planning is done). */\n placeholders?: BulkAddPlaceholderTrack[];\n /** Error message when phase is 'error'. */\n error?: string;\n /** Scene ID this compose event belongs to (for scene-keyed UI state). */\n sceneId?: string;\n}\n\n// ============================================================================\n// Phase 2: Plugin Preset Types\n// ============================================================================\n\nexport interface PluginPresetInfo {\n id: string;\n name: string;\n category: string | null;\n isBuiltIn: boolean;\n data: Record<string, unknown>;\n}\n\nexport interface SavePluginPresetOptions {\n name: string;\n category?: string;\n data: Record<string, unknown>;\n}\n\n// ============================================================================\n// App Tool Bridge (since SDK 1.2.0)\n// ============================================================================\n\n/** JSON Schema shape for a tool's input params. */\nexport interface PluginAppToolInputSchema {\n type: 'object';\n properties?: Record<string, unknown>;\n required?: string[];\n}\n\n/** Lightweight descriptor returned by `PluginHost.listAppTools`. */\nexport interface PluginAppTool {\n name: string;\n description: string;\n inputSchema: PluginAppToolInputSchema;\n /** `'scene'` = safe for scene-scoped callers. `'project'` = cross-scene. */\n scope?: 'scene' | 'project';\n /**\n * `true` = the operation cannot be undone via the host's checkpoint/undo\n * system (project delete, disk overwrite, external export, …). The host\n * gates such calls behind a user-approval flow when invoked with agent\n * provenance; agent UIs may also surface the flag (e.g. ⚠ in a tool list).\n * @since SDK 2.18.0\n */\n irreversible?: boolean;\n}\n\n/** Result shape returned by `PluginHost.executeAppTool`. */\nexport interface PluginAppToolResult {\n success: boolean;\n action: string;\n message?: string;\n error?: string;\n /**\n * Tool-specific payload. Concrete shape depends on the tool — callers\n * should treat this as opaque unless they know the tool.\n */\n data?: unknown;\n}\n\n// ============================================================================\n// Plugin Registry Types (used by host internals)\n// ============================================================================\n\nexport type PluginStatus = 'pending' | 'active' | 'failed' | 'disabled' | 'incompatible';\n\nexport interface PluginRegistration {\n /** The loaded plugin instance */\n plugin: GeneratorPlugin;\n /** Current status */\n status: PluginStatus;\n /** Resolved manifest from disk */\n manifest: PluginManifest;\n /** The scoped PluginHost instance for this plugin */\n host: PluginHost | null;\n /** Sort order for accordion display */\n sortOrder: number;\n /** Whether the plugin is enabled */\n enabled: boolean;\n /** Error message if status is 'failed' */\n error?: string;\n}\n","/**\n * FX Toggle Types\n *\n * Types and constants for per-track FX toggle buttons.\n * Each track can enable/disable 6 FX categories independently.\n * The engine is the source of truth — no database persistence needed.\n */\n\n/** Available FX categories in signal chain order */\nexport type FxCategory = 'eq' | 'compressor' | 'chorus' | 'phaser' | 'delay' | 'reverb';\n\n/** All FX categories in signal chain order */\nexport const FX_CATEGORIES: readonly FxCategory[] = [\n 'eq',\n 'compressor',\n 'chorus',\n 'phaser',\n 'delay',\n 'reverb',\n] as const;\n\n/** Position in the signal chain (lower = earlier) */\nexport const FX_CHAIN_ORDER: Record<FxCategory, number> = {\n eq: 0,\n compressor: 1,\n chorus: 2,\n phaser: 3,\n delay: 4,\n reverb: 5,\n};\n\n/** Map from FxCategory to Tracktion Engine built-in plugin xmlTypeName */\nexport const FX_ENGINE_PLUGIN_NAMES: Record<FxCategory, string> = {\n eq: '4bandEq',\n compressor: 'compressor',\n chorus: 'chorus',\n phaser: 'phaser',\n delay: 'delay',\n reverb: 'reverb',\n};\n\n/** Display labels for UI buttons */\nexport const FX_DISPLAY_LABELS: Record<FxCategory, string> = {\n eq: 'EQ',\n compressor: 'Comp',\n chorus: 'Chorus',\n phaser: 'Phaser',\n delay: 'Delay',\n reverb: 'Reverb',\n};\n\n/** Per-track FX state: which categories are active */\nexport interface TrackFxState {\n eq: boolean;\n compressor: boolean;\n chorus: boolean;\n phaser: boolean;\n delay: boolean;\n reverb: boolean;\n}\n\n/** Default state: all FX disabled */\nexport const EMPTY_FX_STATE: TrackFxState = {\n eq: false,\n compressor: false,\n chorus: false,\n phaser: false,\n delay: false,\n reverb: false,\n};\n\n// ============================================================================\n// Preset Types\n// ============================================================================\n\n/** A single FX preset definition */\nexport interface FxPreset {\n /** Display name (e.g. \"Room\", \"Hall\") */\n name: string;\n /** Short label for button (e.g. \"RM\", \"HL\") */\n shortLabel: string;\n /** Map from automatable parameter name -> value (set via setPluginParameter) */\n params: Record<string, number>;\n /** CachedValue params set via XML state (getPluginState/setPluginState) */\n xmlStateParams?: Record<string, number>;\n /** BPM-relative delay time multiplier (1.0 = quarter note). When set, Delay Time is computed at apply time. */\n noteMultiplier?: number;\n /** Fixed delay time in ms (non-BPM-synced). Mutually exclusive with noteMultiplier. */\n fixedLengthMs?: number;\n}\n\n/** How dry/wet is applied to the plugin */\nexport type MixInterpolation = 'direct' | 'gain-scale' | 'ratio-scale';\n\n/** Preset configuration for an FX category */\nexport interface FxPresetConfig {\n /** Exactly 5 presets */\n presets: [FxPreset, FxPreset, FxPreset, FxPreset, FxPreset];\n /** Name of the native mix/wet parameter, or null if no native dry/wet */\n mixParamName: string | null;\n /** XML attribute name for dry/wet control (for plugins with no automatable mix param, e.g. chorus/phaser) */\n mixXmlAttr?: string;\n /** How to apply dry/wet (defaults to 'direct') */\n mixInterpolation: MixInterpolation;\n}\n\n/** Per-category detail state for a single FX on a track */\nexport interface FxCategoryDetailState {\n enabled: boolean;\n presetIndex: number; // 0-4\n dryWet: number; // 0.0-1.0\n}\n\n/** Extended FX state per track with preset and dry/wet info */\nexport type TrackFxDetailState = Record<FxCategory, FxCategoryDetailState>;\n\n/** Default dry/wet mix level (33% — musically useful for most effects) */\nexport const DEFAULT_FX_DRY_WET = 0.33;\n\n/** Default detail state for a single category */\nexport const DEFAULT_FX_CATEGORY_DETAIL: FxCategoryDetailState = {\n enabled: false,\n presetIndex: 0,\n dryWet: DEFAULT_FX_DRY_WET,\n};\n\n/** Default detail state: all FX disabled, preset 0, full wet */\nexport const EMPTY_FX_DETAIL_STATE: TrackFxDetailState = {\n eq: { ...DEFAULT_FX_CATEGORY_DETAIL },\n compressor: { ...DEFAULT_FX_CATEGORY_DETAIL },\n chorus: { ...DEFAULT_FX_CATEGORY_DETAIL },\n phaser: { ...DEFAULT_FX_CATEGORY_DETAIL },\n delay: { ...DEFAULT_FX_CATEGORY_DETAIL },\n reverb: { ...DEFAULT_FX_CATEGORY_DETAIL },\n};\n\n/** Persisted FX data for a single category (stored as JSON in database) */\nexport interface FxPresetDataEntry {\n presetIndex: number;\n dryWet: number;\n enabled: boolean;\n}\n\n/** Persisted FX data format (stored as JSON in database) */\nexport type FxPresetData = Partial<Record<FxCategory, FxPresetDataEntry>>;\n","/**\n * SDK TrackRow — Reusable track row component for generator plugins.\n *\n * Renders a complete track UI with prompt input, generation controls,\n * shuffle/copy, volume/pan, mute/solo, FX drawer, and visual states\n * (amber pulse for \"needs generation\", progress overlay, error indicator).\n *\n * Layout matches TrackInput (main branch) for visual parity.\n *\n * Depends only on PluginHost types + existing shared renderer components.\n */\n\nimport React from 'react';\nimport { AlertCircle, ChevronDown, GripVertical } from 'lucide-react';\nimport { TrackDrawer, type DrawerTab } from './TrackDrawer';\nimport { ConfirmDialog } from './ConfirmDialog';\nimport { TrackMeterStrip } from './TrackMeterStrip';\nimport type { TrackLevelsHandle } from '../hooks/useTrackLevels';\nimport type { InstrumentDescriptor, SoundHistoryEntry, PluginMidiNote } from '../types/plugin-sdk.types';\nimport type { TrackRowDragProps } from '../hooks/useTrackReorder';\nimport { VolumeSlider } from './VolumeSlider';\nimport { PanSlider } from './PanSlider';\nimport { SorceryProgressBar } from './SorceryProgressBar';\nimport type { TrackFxDetailState, FxCategory } from '../types/fx-toggle.types';\n\n// ============================================================================\n// Props\n// ============================================================================\n\nexport interface SDKTrackRowProps {\n /** Track identity */\n track: { id: string; name: string; role?: string };\n /** Current prompt text (optional — omit when using contentSlot) */\n prompt?: string;\n /** Playback state */\n runtimeState: { muted: boolean; solo: boolean; volume: number; pan: number };\n /** True when ANOTHER track is soloed, so this (non-soloed) track is currently\n * silenced. Renders the row dimmed while leaving its Mute button UNLIT — the\n * engine's effective-mute model silences it without touching user-mute. Purely\n * visual; does not change mute/solo state. */\n soloedOut?: boolean;\n /** FX category states */\n fxDetailState: TrackFxDetailState;\n /** Whether the unified track drawer is open. */\n drawerOpen: boolean;\n /** Which tab the drawer is showing. */\n drawerTab: DrawerTab;\n /** Switch the active drawer tab (tab-strip clicks). Omit for single-tab panels (e.g. loops = FX only). */\n onTabChange?: (tab: DrawerTab) => void;\n /** Generation in progress */\n isGenerating?: boolean;\n /** Auth state */\n isAuthenticated?: boolean;\n /** Error from last generation */\n error?: string | null;\n /** Enables shuffle/copy buttons */\n hasMidi?: boolean;\n /** Progress % (for persistence across scene switches) */\n generationProgress?: number;\n /** For progress bar pacing */\n estimatedGenerationMs?: number;\n /** Prompt edit (optional — omit to hide prompt input) */\n onPromptChange?: (prompt: string) => void;\n /** \"Create\" button / Enter key (optional — omit to hide Create button) */\n onGenerate?: () => void;\n /** Shuffle preset (optional — omit to hide Shuffle button) */\n onShuffle?: () => void;\n /** Duplicate track (optional — omit to hide Copy button) */\n onCopy?: () => void;\n /** Delete track. Optional — omit to hide the delete button (e.g. a composite\n * like CrossfadeTrackRow owns a single delete for the whole pair). */\n onDelete?: () => void;\n /** Custom content replacing the prompt input (e.g., sample info display) */\n contentSlot?: React.ReactNode;\n /** Toggle mute */\n onMuteToggle: () => void;\n /** Toggle solo */\n onSoloToggle: () => void;\n /** Volume slider */\n onVolumeChange: (vol: number) => void;\n /** Pan slider */\n onPanChange: (pan: number) => void;\n /** FX category toggle (optional — omit to hide FX button) */\n onFxToggle?: (cat: FxCategory, enabled: boolean) => void;\n /** FX preset select */\n onFxPresetChange?: (cat: FxCategory, idx: number) => void;\n /** FX dry/wet */\n onFxDryWetChange?: (cat: FxCategory, val: number) => void;\n /** Open/close FX (optional — omit to hide FX button) */\n onToggleFxDrawer?: () => void;\n /** Progress persistence callback */\n onProgressChange?: (pct: number) => void;\n /** Left border accent color */\n accentColor?: string;\n // --- Instrument Plugin Selection ---\n /** Current instrument display name (null/undefined = Surge XT default) */\n instrumentName?: string | null;\n /** Whether the current instrument plugin is missing from the system */\n instrumentMissing?: boolean;\n /** Open/close the drawer to a non-FX tab (the ▾ button). Omit to hide it. */\n onToggleDrawer?: () => void;\n /** Available instrument plugins for the drawer */\n availableInstruments?: InstrumentDescriptor[];\n /** Currently loaded instrument plugin ID */\n currentInstrumentPluginId?: string | null;\n /** Called when user selects an instrument from the drawer */\n onInstrumentSelect?: (pluginId: string) => void;\n /** Whether instrument scan is loading */\n instrumentsLoading?: boolean;\n /** Re-scan for instruments */\n onRefreshInstruments?: () => void;\n // --- Instrument Editor (Stage 2) ---\n /** Pick-tab sub-view: native plugin editor instead of the instrument grid. */\n editorStage?: boolean;\n /** Called when user clicks \"Open Editor\" */\n onShowEditor?: () => void;\n /** Called when user wants to go back from editor view */\n onBackToInstruments?: () => void;\n // --- Sound History (drawer \"History\" tab) ---\n /** Ordered list of sounds this track has had this session. */\n soundHistory?: readonly SoundHistoryEntry[];\n /** Index into soundHistory of the currently-applied sound. */\n soundHistoryCursor?: number;\n /** Restore a sound from the History tab by index. */\n onRestoreSound?: (index: number) => void;\n /** Toggle the favorite (⭐) flag on a history entry. */\n onToggleFavorite?: (index: number) => void;\n /** Open the drawer's sound-import picker; omit to hide the button. */\n onImportSound?: () => void;\n /** Sound-import button label (\"Import Sample\" / \"Import Preset\"). */\n importSoundLabel?: string;\n // --- Edit tab (piano-roll MIDI editor) ---\n /** Current MIDI notes for the piano-roll editor (the 'edit' tab). */\n editNotes?: readonly PluginMidiNote[];\n /** Persist edited notes; PRESENCE of this callback enables the Edit tab. */\n onNotesChange?: (notes: PluginMidiNote[]) => void;\n /** Scene length in bars (piano-roll grid width). */\n editBars?: number;\n /** Scene BPM (piano-roll audition timing). */\n editBpm?: number;\n /** Snap step in quarter notes for the piano roll (default 0.25). */\n editSnap?: number;\n /** Optional single-note preview when the user adds a note. */\n onAuditionNote?: (pitch: number, velocity: number, durationMs: number) => void;\n // --- Drag-to-reorder ---\n /** Drag props from {@link useTrackReorder}. When present, renders the grip\n * handle and makes the row a drop target. Omit for non-reorderable lists. */\n drag?: TrackRowDragProps;\n // --- Per-track peak meter (cosmetic) ---\n /** Shared meter handle from `useTrackLevels(host, isPlaying)`. When present,\n * a thin peak meter welds to the bottom of the row. Omit to hide it. */\n levels?: TrackLevelsHandle;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function TrackRow({\n track,\n prompt,\n runtimeState,\n soloedOut = false,\n fxDetailState,\n drawerOpen,\n drawerTab,\n onTabChange,\n isGenerating = false,\n isAuthenticated = false,\n error,\n hasMidi = false,\n generationProgress = 0,\n estimatedGenerationMs = 15000,\n onPromptChange,\n onGenerate,\n onShuffle,\n onCopy,\n onDelete,\n contentSlot,\n onMuteToggle,\n onSoloToggle,\n onVolumeChange,\n onPanChange,\n onFxToggle,\n onFxPresetChange,\n onFxDryWetChange,\n onToggleFxDrawer,\n onProgressChange,\n accentColor = '#A78BFA',\n instrumentName,\n instrumentMissing,\n onToggleDrawer,\n availableInstruments,\n currentInstrumentPluginId,\n onInstrumentSelect,\n instrumentsLoading,\n onRefreshInstruments,\n editorStage,\n onShowEditor,\n onBackToInstruments,\n soundHistory,\n soundHistoryCursor,\n onRestoreSound,\n onToggleFavorite,\n onImportSound,\n importSoundLabel,\n editNotes,\n onNotesChange,\n editBars,\n editBpm,\n editSnap,\n onAuditionNote,\n drag,\n levels,\n}: SDKTrackRowProps): React.ReactElement {\n const { muted: isMuted, solo: isSoloed, volume: currentVolume, pan: currentPan } = runtimeState;\n\n // Guard the (irreversible) delete behind a confirmation modal — the bare \"x\"\n // was one stray click away from losing a track's MIDI + sound.\n const [confirmDelete, setConfirmDelete] = React.useState(false);\n\n // \"Needs generation\" = has prompt, no MIDI yet, not currently generating\n const needsGeneration = !!(prompt?.trim() && !hasMidi && !isGenerating);\n\n const hasFxActive = Object.values(fxDetailState).some(\n (d: { enabled: boolean }) => d.enabled\n );\n\n // The two row buttons open the SAME unified drawer to different tabs:\n // FX → the 'fx' tab; ▾ → a non-FX tab (History/Pick/Import).\n const fxTabOpen = drawerOpen && drawerTab === 'fx';\n const soundTabOpen = drawerOpen && drawerTab !== 'fx';\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {\n if (e.key === 'Enter' && !e.shiftKey && onGenerate) {\n e.preventDefault();\n onGenerate();\n }\n };\n\n // Amber pulse class for \"needs generation\" state\n const borderColorStyle = needsGeneration\n ? undefined // handled by className animation\n : accentColor;\n\n const borderClass = needsGeneration\n ? 'border-amber-400 animate-pulse'\n : 'border-sas-border';\n\n return (\n <div data-testid=\"sdk-track-row-wrapper\" className=\"w-full\" {...(drag?.rowProps ?? {})}>\n <div\n data-testid=\"sdk-track-row\"\n className={`relative flex items-stretch gap-1 p-2 ${levels ? 'rounded-t-sm' : 'rounded-sm'} border w-full overflow-hidden ${borderClass} bg-sas-panel-alt ${drag?.isDragging ? 'opacity-40' : ''} ${drag?.isDragTarget ? 'ring-2 ring-sas-accent ring-inset' : ''}`}\n style={{\n borderLeftColor: needsGeneration ? '#f59e0b' : borderColorStyle,\n borderLeftWidth: '3px',\n }}\n >\n {/* Drag-to-reorder grip — only when reorder is enabled. z-30 keeps it\n above the generating overlay; only the grip is draggable, so the\n row's inputs and sliders stay interactive. */}\n {drag && (\n <div\n data-testid=\"sdk-drag-handle\"\n {...drag.handleProps}\n className=\"flex-shrink-0 self-stretch flex items-center -ml-0.5 pr-0.5 text-sas-muted/40 hover:text-sas-muted cursor-grab active:cursor-grabbing relative z-30\"\n title=\"Drag to reorder\"\n aria-label=\"Drag to reorder track\"\n >\n <GripVertical className=\"w-3.5 h-3.5\" strokeWidth={2} />\n </div>\n )}\n\n {/* Generating progress overlay - stops before buttons (right-44) */}\n {isGenerating && (\n <div className=\"absolute left-0 top-0 bottom-0 right-44 z-20\">\n <SorceryProgressBar\n isLoading={true}\n statusText=\"CONJURING MIDI...\"\n heightClass=\"h-full\"\n initialProgress={generationProgress}\n onProgressChange={onProgressChange}\n estimatedDurationMs={estimatedGenerationMs}\n />\n </div>\n )}\n\n {/* Left: Content area (prompt input or custom content slot) with track name, volume, and pan underneath.\n Dimmed when soloed-out (silenced by another track's solo); the Mute/Solo\n buttons below stay full-opacity and interactive so the user can un-solo. */}\n <div\n data-testid=\"sdk-track-content\"\n className={`flex flex-col flex-1 min-w-0 relative z-10 transition-opacity ${soloedOut ? 'opacity-40' : ''}`}\n title={soloedOut ? 'Silenced — another track is soloed' : undefined}\n >\n {contentSlot ? contentSlot : onPromptChange ? (\n <input\n type=\"text\"\n data-testid=\"sdk-prompt-input\"\n value={prompt ?? ''}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => onPromptChange(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"Describe your part...\"\n disabled={isGenerating}\n className=\"sas-input w-full px-2 py-1 text-xs disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n ) : null}\n {/* Track name, volume slider, and pan slider in horizontal row */}\n <div className=\"flex items-center gap-2 mt-1\">\n {track.name && (\n <span className=\"text-[10px] text-sas-muted/60 truncate pl-2 flex-shrink-0 max-w-[80px]\" title={track.name}>\n {track.name}\n </span>\n )}\n <span className=\"text-[9px] text-sas-muted/50 flex-shrink-0\">vol:</span>\n <VolumeSlider\n value={currentVolume}\n onChange={onVolumeChange}\n disabled={isGenerating}\n className=\"flex-1 min-w-[40px]\"\n />\n <span className=\"text-[9px] text-sas-muted/50 flex-shrink-0\">pan:</span>\n <PanSlider\n value={currentPan}\n onChange={onPanChange}\n disabled={isGenerating}\n className=\"w-10 flex-shrink-0\"\n />\n </div>\n </div>\n\n {/* Error indicator - shows when generation failed */}\n {error && (\n <div\n data-testid=\"sdk-error-indicator\"\n className=\"flex-shrink-0 relative z-10 self-stretch flex items-center px-1 group cursor-help\"\n title={error}\n >\n <div className=\"relative\">\n <AlertCircle\n className=\"w-5 h-5 text-red-500 animate-pulse\"\n strokeWidth={2.5}\n />\n {/* Tooltip - appears on hover */}\n <div className=\"absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-red-900/95 text-red-100 text-xs rounded shadow-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 max-w-[200px] truncate\">\n {error}\n </div>\n </div>\n </div>\n )}\n\n {/* Right: Button grid (2 rows) - z-30 to stay above generating overlay */}\n <div className=\"flex flex-col gap-0.5 flex-shrink-0 relative z-30 justify-center\">\n {/* Top row: [Create] [Copy] M x — Create/Copy only shown when handlers provided */}\n <div className=\"flex gap-1 items-center\">\n {onGenerate && (\n <button\n data-testid=\"sdk-generate-button\"\n onClick={onGenerate}\n disabled={!isAuthenticated || isGenerating || !prompt?.trim()}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n !isAuthenticated || isGenerating\n ? 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n : needsGeneration\n ? 'bg-amber-500/30 border-amber-500 text-amber-400 hover:bg-amber-500 hover:text-sas-bg animate-pulse'\n : prompt?.trim()\n ? 'bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg'\n : 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n }`}\n title={!isAuthenticated ? 'Please log in' : isGenerating ? 'Generating...' : 'Generate MIDI'}\n >\n Create\n </button>\n )}\n {onCopy && (\n <button\n data-testid=\"sdk-copy-button\"\n onClick={onCopy}\n disabled={!hasMidi || isGenerating}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n !hasMidi || isGenerating\n ? 'bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed'\n : 'bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={hasMidi ? 'Duplicate track with different preset' : 'Generate MIDI first'}\n >\n Copy\n </button>\n )}\n <button\n data-testid=\"sdk-mute-button\"\n onClick={onMuteToggle}\n disabled={isGenerating}\n className={`px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${\n isGenerating\n ? 'bg-sas-panel text-sas-muted/50 cursor-not-allowed'\n : isMuted\n ? 'bg-red-600 text-white'\n : 'bg-sas-panel-alt text-sas-muted hover:bg-sas-border'\n }`}\n title={isMuted ? 'Unmute track' : 'Mute track'}\n >\n M\n </button>\n {onDelete && (\n <button\n data-testid=\"sdk-delete-button\"\n onClick={() => setConfirmDelete(true)}\n className=\"text-sas-danger/70 hover:text-sas-danger px-1 py-0.5 transition-colors text-sm\"\n title=\"Delete track\"\n >\n x\n </button>\n )}\n </div>\n {/* Bottom row: [Shuffle] [FX] Solo [▾] */}\n <div className=\"flex gap-1 items-center\">\n {onShuffle && (\n <button\n data-testid=\"sdk-shuffle-button\"\n onClick={onShuffle}\n disabled={!hasMidi || isGenerating || !!currentInstrumentPluginId}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n !hasMidi || isGenerating || !!currentInstrumentPluginId\n ? 'bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed'\n : 'bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={\n currentInstrumentPluginId\n ? 'Shuffle only works with default Surge XT'\n : hasMidi\n ? 'Re-roll sound (keep MIDI)'\n : 'Generate MIDI first'\n }\n >\n Shuffle\n </button>\n )}\n {onToggleFxDrawer && (\n <button\n data-testid=\"sdk-fx-button\"\n onClick={onToggleFxDrawer}\n disabled={isGenerating}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n isGenerating\n ? 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n : fxTabOpen\n ? 'bg-sas-accent border-sas-accent text-sas-bg'\n : hasFxActive\n ? 'bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg'\n : 'bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={fxTabOpen ? 'Hide FX controls' : 'Show FX controls'}\n >\n FX\n </button>\n )}\n <button\n data-testid=\"sdk-solo-button\"\n onClick={onSoloToggle}\n disabled={isGenerating}\n className={`px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${\n isGenerating\n ? 'bg-sas-panel text-sas-muted/50 cursor-not-allowed'\n : isSoloed\n ? 'bg-yellow-500 text-black'\n : 'bg-sas-panel-alt text-sas-muted hover:bg-sas-border'\n }`}\n title={isSoloed ? 'Unsolo track' : 'Solo track'}\n >\n S\n </button>\n {onToggleDrawer && (\n <button\n data-testid=\"sdk-plugin-button\"\n onClick={onToggleDrawer}\n disabled={isGenerating}\n className={`px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${\n isGenerating\n ? 'bg-sas-panel text-sas-muted/50 cursor-not-allowed'\n : soundTabOpen\n ? 'bg-sas-accent border-sas-accent text-sas-bg'\n : instrumentMissing\n ? 'bg-amber-500/20 text-amber-400 hover:bg-amber-500/40'\n : 'bg-sas-panel-alt text-sas-muted hover:bg-sas-border'\n }`}\n title={`Sound — presets & history${instrumentMissing ? ' (instrument missing)' : ''}`}\n >\n <ChevronDown className=\"w-3 h-3\" strokeWidth={2.5} />\n </button>\n )}\n </div>\n </div>\n </div>\n\n {/* Thin per-track peak meter, welded to the bottom of the row (cosmetic).\n Isolated in TrackMeterStrip so its ~30Hz updates re-render only the\n strip, never this whole row. Squared bottom when a drawer welds below. */}\n {levels && (\n <TrackMeterStrip levels={levels} trackId={track.id} roundBottom={!drawerOpen} />\n )}\n\n {/* Unified track drawer — one drawer, contextual tabs (FX / Pick / History / Import).\n The FX button opens it to 'fx'; the ▾ button to a non-FX tab. Which tabs\n appear is computed inside TrackDrawer from the callbacks provided. */}\n {drawerOpen && (\n <div\n data-testid=\"sdk-track-drawer\"\n className=\"border border-t-0 border-sas-border bg-sas-bg rounded-b-sm px-3 py-2 max-h-[260px] overflow-y-auto\"\n >\n <TrackDrawer\n activeTab={drawerTab}\n onTabChange={onTabChange}\n trackId={track.id}\n fxState={fxDetailState}\n onFxToggle={onFxToggle}\n onFxPresetChange={onFxPresetChange}\n onFxDryWetChange={onFxDryWetChange}\n fxDisabled={isGenerating}\n instruments={availableInstruments}\n currentPluginId={currentInstrumentPluginId ?? null}\n isLoading={instrumentsLoading ?? false}\n onSelect={onInstrumentSelect}\n onRefresh={onRefreshInstruments}\n editorStage={editorStage}\n onShowEditor={onShowEditor}\n onBackToInstruments={onBackToInstruments}\n selectedInstrumentName={instrumentName}\n soundHistory={soundHistory}\n soundHistoryCursor={soundHistoryCursor}\n onRestoreSound={onRestoreSound}\n onToggleFavorite={onToggleFavorite}\n onImportSound={onImportSound}\n importSoundLabel={importSoundLabel}\n editNotes={editNotes}\n onNotesChange={onNotesChange}\n editBars={editBars}\n editBpm={editBpm}\n editSnap={editSnap}\n onAuditionNote={onAuditionNote}\n />\n </div>\n )}\n\n <ConfirmDialog\n open={confirmDelete}\n title=\"Delete track?\"\n message={\n <>\n <span className=\"text-sas-text\">{track.name?.trim() || 'This track'}</span> will be\n permanently removed from this scene. This cannot be undone.\n </>\n }\n confirmLabel=\"Delete\"\n onConfirm={() => {\n setConfirmDelete(false);\n onDelete?.();\n }}\n onCancel={() => setConfirmDelete(false)}\n testIdPrefix=\"track-delete-confirm\"\n />\n </div>\n );\n}\n\nexport default TrackRow;\n","/**\n * TrackDrawer — the unified per-track drawer body.\n *\n * ONE drawer with a flat contextual tab strip. Which tabs appear is computed\n * from which callbacks the host panel provides:\n * - FX (onFxToggle) — the 6-category FX toggle bar\n * - Pick (onSelect) — instrument-plugin picker (+ native editor stage)\n * - History (onRestoreSound) — sounds this track has had (restore / favorite)\n * - Import (onImportSound) — copy a sound from a matching track in another scene\n *\n * The active tab is CONTROLLED by the host (activeTab / onTabChange) so the\n * track row's FX button and ▾ button can open the SAME drawer to a chosen tab.\n * When only one tab is enabled (e.g. loops = FX only) the strip is hidden and\n * that single view renders directly.\n *\n * (Was `InstrumentDrawer` — renamed once it grew an FX tab + Import tab. A\n * `TrackDrawer as InstrumentDrawer` alias is exported from the barrel for\n * backwards compatibility.)\n */\n\nimport React, { useState, useMemo } from 'react';\nimport type { InstrumentDescriptor, SoundHistoryEntry, PluginMidiNote } from '../types/plugin-sdk.types';\nimport type { FxCategory, TrackFxDetailState } from '../types/fx-toggle.types';\nimport { FxToggleBar } from './FxToggleBar';\nimport { PianoRollEditor } from './PianoRollEditor';\n\n// ============================================================================\n// Tabs\n// ============================================================================\n\n/** The contextual tabs a track drawer can show, in display order. */\nexport type DrawerTab = 'fx' | 'pick' | 'history' | 'import' | 'edit';\n\nconst TAB_LABELS: Record<DrawerTab, string> = {\n fx: 'FX',\n pick: 'Pick',\n history: 'History',\n import: 'Import',\n edit: 'Edit',\n};\n\n// ============================================================================\n// Props\n// ============================================================================\n\nexport interface TrackDrawerProps {\n /** Which tab is active (controlled by the host TrackRow). */\n activeTab: DrawerTab;\n /** Switch tabs (strip clicks). */\n onTabChange?: (tab: DrawerTab) => void;\n\n // --- FX tab (enabled when onFxToggle is provided) ---\n trackId: string;\n fxState: TrackFxDetailState;\n onFxToggle?: (category: FxCategory, enabled: boolean) => void;\n onFxPresetChange?: (category: FxCategory, presetIndex: number) => void;\n onFxDryWetChange?: (category: FxCategory, value: number) => void;\n /** Disable FX controls (e.g. while the track is generating). */\n fxDisabled?: boolean;\n\n // --- Pick tab (enabled when onSelect is provided) ---\n /** Available instrument plugins from engine scan. */\n instruments?: InstrumentDescriptor[];\n /** Currently loaded instrument plugin ID (null = default Surge XT). */\n currentPluginId?: string | null;\n /** Whether the instrument scan is still in progress. */\n isLoading?: boolean;\n /** Called when user selects an instrument (presence enables the Pick tab). */\n onSelect?: (pluginId: string) => void;\n /** Re-scan plugins. */\n onRefresh?: () => void;\n /** Pick-tab sub-view: show the native plugin editor instead of the grid. */\n editorStage?: boolean;\n /** Called when user clicks \"Open Plugin Editor\". */\n onShowEditor?: () => void;\n /** Called when user goes back from the editor to the instrument grid. */\n onBackToInstruments?: () => void;\n /** Name of the selected instrument (shown in the editor header). */\n selectedInstrumentName?: string | null;\n\n // --- History tab (enabled when onRestoreSound is provided) ---\n soundHistory?: readonly SoundHistoryEntry[];\n soundHistoryCursor?: number;\n /** Restore a sound by index; presence enables the History tab. */\n onRestoreSound?: (index: number) => void;\n /** Toggle the favorite (⭐) flag on a history entry; omit to hide the star. */\n onToggleFavorite?: (index: number) => void;\n\n // --- Import tab (enabled when onImportSound is provided) ---\n /** Open the sound-import picker; presence enables the Import tab. */\n onImportSound?: () => void;\n /** Button label, e.g. \"Import Sample\" (drums/instruments) or \"Import Preset\" (synths). */\n importSoundLabel?: string;\n\n // --- Edit tab (enabled when onNotesChange is provided) ---\n /** Current MIDI notes for the piano-roll editor. */\n editNotes?: readonly PluginMidiNote[];\n /** Persist edited notes; PRESENCE of this callback enables the Edit tab. */\n onNotesChange?: (notes: PluginMidiNote[]) => void;\n /** Scene length in bars (piano-roll grid width). Default 4. */\n editBars?: number;\n /** Scene BPM (piano-roll audition timing). Default 120. */\n editBpm?: number;\n /** Snap step in quarter notes for the piano roll (default 0.25). */\n editSnap?: number;\n /** Optional single-note preview when the user adds a note. */\n onAuditionNote?: (pitch: number, velocity: number, durationMs: number) => void;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function TrackDrawer({\n activeTab,\n onTabChange,\n trackId,\n fxState,\n onFxToggle,\n onFxPresetChange,\n onFxDryWetChange,\n fxDisabled = false,\n instruments = [],\n currentPluginId = null,\n isLoading = false,\n onSelect,\n onRefresh,\n editorStage = false,\n onShowEditor,\n onBackToInstruments,\n selectedInstrumentName,\n soundHistory,\n soundHistoryCursor = -1,\n onRestoreSound,\n onToggleFavorite,\n onImportSound,\n importSoundLabel,\n editNotes,\n onNotesChange,\n editBars,\n editBpm,\n editSnap,\n onAuditionNote,\n}: TrackDrawerProps): React.ReactElement {\n // --- Hooks (MUST stay above every early return) ---\n const [search, setSearch] = useState('');\n\n const fxEnabled = !!onFxToggle;\n const pickEnabled = !!onSelect;\n const historyEnabled = !!onRestoreSound;\n const importEnabled = !!onImportSound;\n const editEnabled = !!onNotesChange;\n\n const enabledTabs = useMemo((): DrawerTab[] => {\n const tabs: DrawerTab[] = [];\n if (fxEnabled) tabs.push('fx');\n if (pickEnabled) tabs.push('pick');\n if (historyEnabled) tabs.push('history');\n if (importEnabled) tabs.push('import');\n if (editEnabled) tabs.push('edit');\n return tabs;\n }, [fxEnabled, pickEnabled, historyEnabled, importEnabled, editEnabled]);\n\n /** Sentinel pluginId for the default Surge XT entry */\n const SURGE_XT_DEFAULT_ID = 'Surge XT';\n\n // Filter instruments by search query, with selected instrument always first.\n // Computed unconditionally so the hook order is stable across tab switches.\n const filtered = useMemo((): InstrumentDescriptor[] => {\n let all = instruments.filter((i: InstrumentDescriptor) => i.name !== 'Surge XT');\n if (search.trim()) {\n const q = search.toLowerCase();\n all = all.filter(\n (i: InstrumentDescriptor) =>\n i.name.toLowerCase().includes(q) || i.manufacturer.toLowerCase().includes(q),\n );\n }\n if (currentPluginId) {\n const selectedIdx = all.findIndex((i: InstrumentDescriptor) => i.pluginId === currentPluginId);\n if (selectedIdx > 0) {\n const [selected] = all.splice(selectedIdx, 1);\n all.unshift(selected);\n }\n }\n return all;\n }, [instruments, search, currentPluginId]);\n\n // --- Derived (non-hook) values ---\n const history = soundHistory ?? [];\n const effectiveTab: DrawerTab = enabledTabs.includes(activeTab)\n ? activeTab\n : enabledTabs[0] ?? 'fx';\n\n const tabClass = (active: boolean): string =>\n `px-2 py-0.5 text-xs rounded-sm transition-colors ${\n active ? 'bg-sas-accent/20 text-sas-accent font-medium' : 'text-sas-muted hover:text-sas-accent'\n }`;\n\n // The tab strip replaces the old \"Sound\" title. Hidden when only one tab is\n // enabled (e.g. loops = FX only) — that single view renders directly.\n const strip =\n enabledTabs.length > 1 ? (\n <div\n className=\"flex items-center gap-1 border-b border-sas-border pb-1\"\n data-testid=\"sdk-drawer-tabs\"\n >\n {enabledTabs.map((tab: DrawerTab) => (\n <button\n key={tab}\n type=\"button\"\n data-testid={`sdk-drawer-tab-${tab}`}\n onClick={() => onTabChange?.(tab)}\n className={tabClass(effectiveTab === tab)}\n >\n {tab === 'history' && history.length > 0\n ? `History (${history.length})`\n : TAB_LABELS[tab]}\n </button>\n ))}\n </div>\n ) : null;\n\n // Subtle current-sound hint (the \"Sound\" title was removed in favour of tabs).\n const currentSound =\n soundHistoryCursor >= 0 && soundHistoryCursor < history.length\n ? history[soundHistoryCursor].label\n : null;\n\n const header =\n strip || currentSound ? (\n <div className=\"flex flex-col gap-1\" data-testid=\"sdk-drawer-header\">\n {strip}\n {currentSound && (\n <span\n className=\"text-[10px] text-sas-muted/60 truncate px-0.5\"\n title={currentSound}\n >\n {currentSound}\n </span>\n )}\n </div>\n ) : null;\n\n // ---- Edit tab (piano-roll MIDI editor) ----\n if (effectiveTab === 'edit') {\n return (\n <div className=\"flex flex-col gap-2\" data-testid=\"sdk-drawer-edit\">\n {header}\n <PianoRollEditor\n notes={editNotes ?? []}\n onChange={onNotesChange ?? ((): void => {})}\n bars={editBars ?? 4}\n bpm={editBpm ?? 120}\n snap={editSnap}\n onAuditionNote={onAuditionNote}\n />\n </div>\n );\n }\n\n // ---- FX tab ----\n if (effectiveTab === 'fx') {\n return (\n <div className=\"flex flex-col gap-2\" data-testid=\"sdk-drawer-fx\">\n {header}\n <FxToggleBar\n trackId={trackId}\n fxState={fxState}\n onToggle={(_t: string, category: FxCategory, enabled: boolean) =>\n onFxToggle?.(category, enabled)\n }\n onPresetChange={(_t: string, category: FxCategory, presetIndex: number) =>\n onFxPresetChange?.(category, presetIndex)\n }\n onDryWetChange={(_t: string, category: FxCategory, value: number) =>\n onFxDryWetChange?.(category, value)\n }\n disabled={fxDisabled}\n />\n </div>\n );\n }\n\n // ---- Import tab ----\n if (effectiveTab === 'import') {\n const soundNoun = /preset/i.test(importSoundLabel ?? '')\n ? 'preset'\n : /sample/i.test(importSoundLabel ?? '')\n ? 'sample'\n : 'sound';\n return (\n <div className=\"flex flex-col gap-2\" data-testid=\"sdk-drawer-import\">\n {header}\n <p className=\"text-[11px] text-sas-muted/70 leading-snug\">\n Copy the sound from a matching track in another scene — your MIDI stays, only the{' '}\n {soundNoun} changes.\n </p>\n <button\n type=\"button\"\n data-testid=\"sdk-drawer-import-sound\"\n onClick={onImportSound}\n className=\"w-full px-2 py-1.5 text-[11px] rounded-sm border border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent transition-colors\"\n title=\"Copy a sound from a track in another scene (ignores contract)\"\n >\n ⇪ {importSoundLabel ?? 'Import Sound'}\n </button>\n </div>\n );\n }\n\n // ---- History tab ----\n if (effectiveTab === 'history') {\n const order = history.map((_, i) => i).reverse(); // newest first\n return (\n <div className=\"flex flex-col gap-2\">\n {header}\n {history.length === 0 ? (\n <div\n className=\"text-xs text-sas-muted/60 text-center py-3\"\n data-testid=\"sdk-history-empty\"\n >\n No sounds yet — shuffle to build history.\n </div>\n ) : (\n <ul\n className=\"flex flex-col gap-1 max-h-[160px] overflow-y-auto\"\n data-testid=\"sdk-history-list\"\n >\n {order.map((i) => {\n const entry = history[i];\n const isCurrent = i === soundHistoryCursor;\n return (\n <li key={i} className=\"flex items-center gap-1\">\n <button\n type=\"button\"\n data-testid=\"sdk-history-entry\"\n disabled={isCurrent}\n onClick={() => onRestoreSound?.(i)}\n className={`flex-1 min-w-0 flex items-center justify-between px-2 py-1.5 rounded-sm border text-left text-xs transition-colors ${\n isCurrent\n ? 'border-sas-accent bg-sas-accent/20 text-sas-accent cursor-default'\n : 'border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={isCurrent ? 'Current sound' : `Restore: ${entry.label}`}\n >\n <span className=\"truncate\">{entry.label}</span>\n <span className=\"text-[10px] text-sas-muted/60 flex-shrink-0 ml-2\">\n {isCurrent ? '● current' : 'restore'}\n </span>\n </button>\n {onToggleFavorite && (\n <button\n type=\"button\"\n data-testid=\"sdk-history-favorite\"\n onClick={() => onToggleFavorite(i)}\n className={`flex-shrink-0 px-1 py-0.5 text-sm leading-none transition-colors ${\n entry.favorite\n ? 'text-yellow-400'\n : 'text-sas-muted/40 hover:text-yellow-400'\n }`}\n title={entry.favorite ? 'Unfavorite' : 'Favorite (keeps it from being evicted)'}\n >\n {entry.favorite ? '★' : '☆'}\n </button>\n )}\n </li>\n );\n })}\n </ul>\n )}\n </div>\n );\n }\n\n // ---- Pick tab: native editor stage ----\n if (effectiveTab === 'pick' && editorStage) {\n return (\n <div className=\"flex flex-col gap-2\">\n {header}\n <div className=\"flex items-center gap-2\">\n <button\n onClick={() => onBackToInstruments?.()}\n className=\"px-2 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors\"\n >\n ← Back\n </button>\n <span className=\"text-xs text-sas-muted font-medium truncate flex-1\">\n {selectedInstrumentName ?? 'Plugin'}\n </span>\n </div>\n <button\n onClick={() => onShowEditor?.()}\n className=\"w-full py-2 text-xs font-medium rounded-sm border border-sas-accent bg-sas-accent/20 text-sas-accent hover:bg-sas-accent/40 transition-colors\"\n >\n Open Plugin Editor\n </button>\n </div>\n );\n }\n\n // ---- Pick tab: instrument grid (default) ----\n const isDefaultSelected = currentPluginId === null;\n const isSelected = (pluginId: string): boolean => pluginId === currentPluginId;\n\n return (\n <div className=\"flex flex-col gap-2\">\n {header}\n {/* Search + Refresh row */}\n <div className=\"flex items-center gap-2\">\n <input\n type=\"text\"\n value={search}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.target.value)}\n placeholder=\"Search instruments...\"\n className=\"sas-input flex-1 px-2 py-1 text-xs\"\n />\n <button\n onClick={() => onRefresh?.()}\n disabled={isLoading}\n className=\"px-2 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors disabled:opacity-50\"\n title=\"Re-scan plugins\"\n >\n {isLoading ? '...' : 'Refresh'}\n </button>\n </div>\n\n {/* Instrument grid */}\n {isLoading && instruments.length === 0 ? (\n <div className=\"text-xs text-sas-muted/60 text-center py-3\">Scanning plugins...</div>\n ) : (\n <div className=\"grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto\">\n {/* Permanent \"Surge XT (Default)\" entry — always available */}\n <button\n key=\"__surge-xt-default__\"\n onClick={() => onSelect?.(SURGE_XT_DEFAULT_ID)}\n className={`flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors ${\n isDefaultSelected\n ? 'border-sas-accent bg-sas-accent/20 text-sas-accent'\n : 'border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title=\"Surge XT — Default instrument\"\n >\n <span className=\"text-xs font-medium truncate w-full\">\n {isDefaultSelected && '✓ '}Surge XT\n </span>\n <span className=\"text-[9px] text-sas-muted/50 truncate w-full\">Default</span>\n </button>\n {/* Scanned instruments */}\n {filtered.map((inst: InstrumentDescriptor) => {\n const selected = isSelected(inst.pluginId);\n return (\n <button\n key={inst.pluginId}\n onClick={() => onSelect?.(inst.pluginId)}\n className={`flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors ${\n selected\n ? 'border-sas-accent bg-sas-accent/20 text-sas-accent'\n : inst.missing\n ? 'border-amber-500/50 bg-amber-500/10 text-amber-400 hover:border-amber-500'\n : 'border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={`${inst.name} by ${inst.manufacturer} (${inst.type.toUpperCase()})${inst.missing ? ' — MISSING' : ''}`}\n >\n <span className=\"text-xs font-medium truncate w-full\">\n {selected && '✓ '}\n {inst.name}\n </span>\n <span className=\"text-[9px] text-sas-muted/50 truncate w-full\">\n {inst.manufacturer || inst.type.toUpperCase()}\n </span>\n </button>\n );\n })}\n {filtered.length === 0 && (\n <div className=\"col-span-2 text-xs text-sas-muted/60 text-center py-2\">\n {search.trim() ? 'No matches' : 'No other plugins found'}\n </div>\n )}\n </div>\n )}\n </div>\n );\n}\n\n/** Backwards-compatible alias — the drawer was named `InstrumentDrawer` before it grew FX/Import tabs. */\nexport { TrackDrawer as InstrumentDrawer };\n\nexport default TrackDrawer;\n","/**\n * FX Preset Definitions\n *\n * 5 presets per FX category (30 total).\n *\n * Parameter names must match the Tracktion Engine's AutomatableParameter names\n * for each built-in plugin exactly (case-sensitive).\n *\n * Chorus & Phaser have ZERO automatable parameters — all values are set via\n * XML state (CachedValues on the plugin ValueTree).\n *\n * Lives in shared/ so both main process (services) and renderer (UI) can import.\n */\n\nimport type { FxCategory, FxPresetConfig } from '../types/fx-toggle.types';\n\n// ============================================================================\n// EQ (4-Band Equaliser)\n// ============================================================================\n\nconst EQ_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'The Smiley',\n shortLabel: 'SM',\n params: {\n 'Low-shelf freq': 80, 'Low-shelf gain': 4, 'Low-shelf Q': 0.5,\n 'Mid freq 1': 500, 'Mid gain 1': -3, 'Mid Q 1': 0.7,\n 'Mid freq 2': 2000, 'Mid gain 2': -2, 'Mid Q 2': 0.7,\n 'High-shelf freq': 12000, 'High-shelf gain': 4, 'High-shelf Q': 0.5,\n },\n },\n {\n name: 'Telephone',\n shortLabel: 'TP',\n params: {\n 'Low-shelf freq': 400, 'Low-shelf gain': -20, 'Low-shelf Q': 1.0,\n 'Mid freq 1': 1000, 'Mid gain 1': 5, 'Mid Q 1': 2.0,\n 'Mid freq 2': 3000, 'Mid gain 2': -5, 'Mid Q 2': 1.0,\n 'High-shelf freq': 5000, 'High-shelf gain': -20, 'High-shelf Q': 1.0,\n },\n },\n {\n name: 'Warmth',\n shortLabel: 'WM',\n params: {\n 'Low-shelf freq': 120, 'Low-shelf gain': 3, 'Low-shelf Q': 0.7,\n 'Mid freq 1': 400, 'Mid gain 1': 2, 'Mid Q 1': 1.0,\n 'Mid freq 2': 4000, 'Mid gain 2': 0, 'Mid Q 2': 0.5,\n 'High-shelf freq': 10000, 'High-shelf gain': -4, 'High-shelf Q': 0.5,\n },\n },\n {\n name: 'Vocal Air',\n shortLabel: 'VA',\n params: {\n 'Low-shelf freq': 100, 'Low-shelf gain': -6, 'Low-shelf Q': 0.7,\n 'Mid freq 1': 300, 'Mid gain 1': -2, 'Mid Q 1': 1.0,\n 'Mid freq 2': 1500, 'Mid gain 2': 0, 'Mid Q 2': 0.5,\n 'High-shelf freq': 14000, 'High-shelf gain': 6, 'High-shelf Q': 0.4,\n },\n },\n {\n name: 'De-Box',\n shortLabel: 'DB',\n params: {\n 'Low-shelf freq': 60, 'Low-shelf gain': 0, 'Low-shelf Q': 0.5,\n 'Mid freq 1': 350, 'Mid gain 1': -5, 'Mid Q 1': 2.0,\n 'Mid freq 2': 800, 'Mid gain 2': -3, 'Mid Q 2': 2.0,\n 'High-shelf freq': 10000, 'High-shelf gain': 0, 'High-shelf Q': 0.5,\n },\n },\n ],\n mixParamName: null,\n mixInterpolation: 'gain-scale',\n};\n\n// ============================================================================\n// Compressor\n// ============================================================================\n\nconst COMPRESSOR_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Vocal Leveler',\n shortLabel: 'VL',\n params: { 'Threshold': 0.251, 'Ratio': 0.5, 'Attack': 20.0, 'Release': 200.0, 'Output': 2.0 },\n },\n {\n name: 'Drum Smash',\n shortLabel: 'DS',\n params: { 'Threshold': 0.100, 'Ratio': 0.1, 'Attack': 0.5, 'Release': 100.0, 'Output': 8.0 },\n },\n {\n name: 'Bus Glue',\n shortLabel: 'BG',\n params: { 'Threshold': 0.316, 'Ratio': 0.666, 'Attack': 80.0, 'Release': 150.0, 'Output': 1.0 },\n },\n {\n name: 'Bass Anchor',\n shortLabel: 'BA',\n params: { 'Threshold': 0.177, 'Ratio': 0.25, 'Attack': 10.0, 'Release': 250.0, 'Output': 4.0 },\n },\n {\n name: 'Safety Net',\n shortLabel: 'SN',\n params: { 'Threshold': 0.891, 'Ratio': 0.0, 'Attack': 0.3, 'Release': 50.0, 'Output': 0.0 },\n },\n ],\n mixParamName: null,\n mixInterpolation: 'ratio-scale',\n};\n\n// ============================================================================\n// Chorus\n// ============================================================================\n\nconst CHORUS_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Dimension',\n shortLabel: 'DM',\n params: {},\n xmlStateParams: { depthMs: 1.5, speedHz: 0.5, width: 1.0, mixProportion: 0.5 },\n },\n {\n name: '80s Crystal',\n shortLabel: '80',\n params: {},\n xmlStateParams: { depthMs: 4.0, speedHz: 2.5, width: 0.8, mixProportion: 0.4 },\n },\n {\n name: 'Sea Sick',\n shortLabel: 'SS',\n params: {},\n xmlStateParams: { depthMs: 7.0, speedHz: 0.8, width: 0.3, mixProportion: 1.0 },\n },\n {\n name: 'Pseudo-Leslie',\n shortLabel: 'PL',\n params: {},\n xmlStateParams: { depthMs: 2.0, speedHz: 6.0, width: 0.9, mixProportion: 0.7 },\n },\n {\n name: 'Thickener',\n shortLabel: 'TK',\n params: {},\n xmlStateParams: { depthMs: 1.0, speedHz: 0.2, width: 1.0, mixProportion: 0.3 },\n },\n ],\n mixParamName: null,\n mixXmlAttr: 'mixProportion',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Phaser\n// ============================================================================\n\nconst PHASER_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Slow Burn',\n shortLabel: 'SB',\n params: {},\n xmlStateParams: { depth: 6.0, rate: 0.1, feedback: 0.3 },\n },\n {\n name: 'Funky Quack',\n shortLabel: 'FQ',\n params: {},\n xmlStateParams: { depth: 3.0, rate: 2.0, feedback: 0.8 },\n },\n {\n name: 'Jet Plane',\n shortLabel: 'JP',\n params: {},\n xmlStateParams: { depth: 8.0, rate: 0.2, feedback: 0.9 },\n },\n {\n name: 'Underwater',\n shortLabel: 'UW',\n params: {},\n xmlStateParams: { depth: 1.5, rate: 4.0, feedback: 0.1 },\n },\n {\n name: 'Static Notch',\n shortLabel: 'ST',\n params: {},\n xmlStateParams: { depth: 2.0, rate: 0.05, feedback: 0.6 },\n },\n ],\n mixParamName: null,\n mixXmlAttr: 'depth',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Delay\n// ============================================================================\n\nconst DELAY_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Vocal Slap',\n shortLabel: 'VS',\n fixedLengthMs: 110,\n params: { 'Feedback': -20.0, 'Mix proportion': 0.25 },\n },\n {\n name: 'Grand Canyon',\n shortLabel: 'GC',\n noteMultiplier: 1.0,\n params: { 'Feedback': -4.0, 'Mix proportion': 0.45 },\n },\n {\n name: 'Wide Doubler',\n shortLabel: 'WD',\n fixedLengthMs: 25,\n params: { 'Feedback': -30.0, 'Mix proportion': 0.5 },\n },\n {\n name: 'Dub Echo',\n shortLabel: 'DE',\n noteMultiplier: 0.6,\n params: { 'Feedback': -1.5, 'Mix proportion': 0.4 },\n },\n {\n name: 'Rhythmic Wash',\n shortLabel: 'RW',\n noteMultiplier: 0.75,\n params: { 'Feedback': -8.0, 'Mix proportion': 0.2 },\n },\n ],\n mixParamName: 'Mix proportion',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Reverb\n// ============================================================================\n\nconst REVERB_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Drum Room',\n shortLabel: 'DR',\n params: { 'Room Size': 0.2, 'Damping': 0.2, 'Wet Level': 0.15, 'Dry Level': 0.5, 'Width': 0.8 },\n },\n {\n name: 'Vocal Hall',\n shortLabel: 'VH',\n params: { 'Room Size': 0.8, 'Damping': 0.6, 'Wet Level': 0.25, 'Dry Level': 0.5, 'Width': 1.0 },\n },\n {\n name: 'Cathedral',\n shortLabel: 'CT',\n params: { 'Room Size': 1.0, 'Damping': 0.1, 'Wet Level': 0.333, 'Dry Level': 0.2, 'Width': 1.0 },\n },\n {\n name: 'Tile Bathroom',\n shortLabel: 'TB',\n params: { 'Room Size': 0.15, 'Damping': 0.0, 'Wet Level': 0.2, 'Dry Level': 0.5, 'Width': 0.5 },\n },\n {\n name: 'Vintage Plate',\n shortLabel: 'VP',\n params: { 'Room Size': 0.4, 'Damping': 1.0, 'Wet Level': 0.2, 'Dry Level': 0.5, 'Width': 1.0 },\n },\n ],\n mixParamName: 'Wet Level',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Export\n// ============================================================================\n\n/** All preset configs keyed by FX category */\nexport const FX_PRESET_CONFIGS: Record<FxCategory, FxPresetConfig> = {\n eq: EQ_PRESETS,\n compressor: COMPRESSOR_PRESETS,\n chorus: CHORUS_PRESETS,\n phaser: PHASER_PRESETS,\n delay: DELAY_PRESETS,\n reverb: REVERB_PRESETS,\n};\n","/**\n * FxToggleBar Component\n *\n * Per-track FX control panel with 6 rows (one per FX category).\n * Each row: [Category toggle] [Preset 1-5 buttons] [Dry/Wet slider]\n *\n * Signal chain order: EQ -> Compressor -> Chorus -> Phaser -> Delay -> Reverb\n */\n\nimport React from 'react';\nimport type { FxCategory, TrackFxDetailState, FxCategoryDetailState } from '../types/fx-toggle.types';\nimport { FX_CATEGORIES, FX_DISPLAY_LABELS } from '../types/fx-toggle.types';\nimport { FX_PRESET_CONFIGS } from '../constants/fx-presets';\n\n/** Per-category active colors */\nconst FX_COLORS: Record<FxCategory, string> = {\n eq: 'bg-blue-500',\n compressor: 'bg-orange-500',\n chorus: 'bg-teal-500',\n phaser: 'bg-purple-500',\n delay: 'bg-green-500',\n reverb: 'bg-cyan-500',\n};\n\nexport interface FxToggleBarProps {\n trackId: string;\n fxState: TrackFxDetailState;\n onToggle: (trackId: string, category: FxCategory, enabled: boolean) => void;\n onPresetChange: (trackId: string, category: FxCategory, presetIndex: number) => void;\n onDryWetChange: (trackId: string, category: FxCategory, value: number) => void;\n disabled?: boolean;\n}\n\nexport const FxToggleBar: React.FC<FxToggleBarProps> = ({\n trackId,\n fxState,\n onToggle,\n onPresetChange,\n onDryWetChange,\n disabled = false,\n}) => {\n return (\n <div className=\"flex flex-col gap-1\" data-testid=\"fx-toggle-bar\">\n {FX_CATEGORIES.map((category: FxCategory) => {\n const detail: FxCategoryDetailState = fxState[category];\n const isActive = detail.enabled;\n const label = FX_DISPLAY_LABELS[category];\n const activeColor = FX_COLORS[category];\n const config = FX_PRESET_CONFIGS[category];\n\n return (\n <div key={category} className=\"flex items-center gap-0.5\">\n {/* Category toggle button */}\n <button\n data-testid={`fx-toggle-${category}`}\n disabled={disabled}\n onClick={() => onToggle(trackId, category, !isActive)}\n className={`w-14 py-0.5 text-[10px] font-semibold rounded-sm transition-colors leading-none flex-shrink-0 text-center ${\n disabled\n ? 'bg-sas-panel text-sas-muted/30 cursor-not-allowed'\n : isActive\n ? `${activeColor} text-white`\n : 'bg-sas-panel-alt text-sas-muted/60 hover:bg-sas-border hover:text-sas-muted'\n }`}\n title={`${isActive ? 'Disable' : 'Enable'} ${category.toUpperCase()}`}\n >\n {label}\n </button>\n\n {/* Preset buttons 1-5 */}\n {config.presets.map((preset, idx: number) => (\n <button\n key={idx}\n data-testid={`fx-preset-${category}-${idx}`}\n disabled={disabled || !isActive}\n onClick={() => onPresetChange(trackId, category, idx)}\n className={`w-5 h-5 text-[9px] font-medium rounded-sm transition-colors leading-none flex-shrink-0 ${\n disabled || !isActive\n ? 'bg-sas-panel text-sas-muted/20 cursor-not-allowed'\n : detail.presetIndex === idx\n ? `${activeColor} text-white`\n : 'bg-sas-panel-alt text-sas-muted/50 hover:bg-sas-border hover:text-sas-muted'\n }`}\n title={preset.name}\n >\n {idx + 1}\n </button>\n ))}\n\n {/* Dry/Wet slider */}\n <input\n type=\"range\"\n data-testid={`fx-drywet-${category}`}\n min=\"0\"\n max=\"100\"\n value={Math.round(detail.dryWet * 100)}\n disabled={disabled || !isActive}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) =>\n onDryWetChange(trackId, category, Number(e.target.value) / 100)\n }\n className=\"flex-1 min-w-[30px] h-3 accent-sas-accent disabled:opacity-30 cursor-pointer disabled:cursor-not-allowed\"\n title={`Dry/Wet: ${Math.round(detail.dryWet * 100)}%`}\n />\n <span className=\"text-[8px] text-sas-muted/50 w-6 text-right flex-shrink-0\">\n {Math.round(detail.dryWet * 100)}%\n </span>\n </div>\n );\n })}\n </div>\n );\n};\n","/**\n * PianoRollEditor — a compact, DOM-based MIDI note editor for the track drawer.\n *\n * Controlled: `notes` in, `onChange(next)` out. Notes render as absolutely-\n * positioned divs over a beat/pitch grid (DOM, not canvas — so it themes with\n * sas-* tokens and is fully driveable by React Testing Library). Supports:\n * - add : click an empty grid cell\n * - delete : click an existing note (no drag)\n * - move : drag a note's body (snap-quantised)\n * - resize : drag a note's right-edge handle (snap-quantised, ≥ one step)\n * - octave : shift the whole clip ±12 (toolbar) — no velocity lane\n * / marquee yet.\n * On load the viewport auto-scrolls to vertically center the note cluster, so a\n * low melody isn't stranded off-screen at the bottom of the pitch range.\n *\n * Coordinate spaces:\n * pitch (0-127) ── row = hi - pitch ── top px = row * ROW_HEIGHT\n * beat (¼ notes) ─────────────────────── left px = beat * PX_PER_BEAT\n *\n * The pure helpers (`cellToPx` / `pxToCell` / `transposeNotes`) and layout\n * constants are exported so coordinate math can be unit-tested without a DOM.\n */\nimport React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';\nimport type { PluginMidiNote } from '../types/plugin-sdk.types';\n\n// ============================================================================\n// Layout constants (exported for tests)\n// ============================================================================\n\n/** Horizontal pixels per quarter-note beat. */\nexport const PX_PER_BEAT = 24;\n/** Vertical pixels per semitone row. */\nexport const ROW_HEIGHT = 12;\n/** Left keyboard-gutter width (px). */\nexport const GUTTER_W = 28;\n/** Pointer travel (px) before a press on a note becomes a drag instead of a click. */\nexport const DRAG_DEAD_ZONE = 4;\n/** Width (px) of the right-edge grab handle that resizes a note's length. */\nexport const RESIZE_HANDLE_PX = 6;\n/** Max height (px) of the vertical scroll viewport — drives load-time centering. */\nexport const SCROLL_MAX_H = 150;\n\nconst NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] as const;\nconst BLACK_KEYS = new Set([1, 3, 6, 8, 10]);\n\nconst SNAP_LABELS: Record<string, string> = {\n '2': '1/2',\n '1': '1/4',\n '0.5': '1/8',\n '0.25': '1/16',\n '0.125': '1/32',\n};\n\nfunction clamp(v: number, lo: number, hi: number): number {\n return Math.max(lo, Math.min(hi, v));\n}\n\nfunction snapLabel(s: number): string {\n return SNAP_LABELS[String(s)] ?? `${s}`;\n}\n\n// ============================================================================\n// Pure helpers (DOM-free, exported for unit tests)\n// ============================================================================\n\n/** MIDI pitch → scientific note name (60 = C4). */\nexport function pitchToName(pitch: number): string {\n const name = NOTE_NAMES[((pitch % 12) + 12) % 12];\n const octave = Math.floor(pitch / 12) - 1;\n return `${name}${octave}`;\n}\n\n/**\n * Cell (pitch, startBeat) → top-left pixel offset within the grid.\n * `hi` is the highest (top) visible pitch.\n */\nexport function cellToPx(\n pitch: number,\n startBeat: number,\n hi: number,\n): { left: number; top: number } {\n return { left: startBeat * PX_PER_BEAT, top: (hi - pitch) * ROW_HEIGHT };\n}\n\n/**\n * Grid-local pixel → snapped cell. `hi` is the highest visible pitch; the beat\n * snaps to the nearest `snap` step and clamps to `[0, totalBeats - snap]`;\n * pitch clamps to `[0, 127]`.\n */\nexport function pxToCell(\n localX: number,\n localY: number,\n hi: number,\n snap: number,\n bars: number,\n beatsPerBar: number,\n): { pitch: number; startBeat: number } {\n const totalBeats = bars * beatsPerBar;\n const pitch = clamp(hi - Math.floor(localY / ROW_HEIGHT), 0, 127);\n const rawBeat = localX / PX_PER_BEAT;\n const snapped = Math.round(rawBeat / snap) * snap;\n const startBeat = clamp(snapped, 0, Math.max(0, totalBeats - snap));\n return { pitch, startBeat };\n}\n\n/**\n * New `durationBeats` for a note whose right edge is dragged to grid-local pixel\n * `localX`. The end snaps to the nearest `snap` step, is clamped to at least one\n * step past `startBeat`, and never extends beyond the grid's right edge\n * (`bars * beatsPerBar`). `startBeat` and `pitch` are untouched.\n */\nexport function resizeNoteDuration(\n startBeat: number,\n localX: number,\n snap: number,\n bars: number,\n beatsPerBar: number,\n): number {\n const totalBeats = bars * beatsPerBar;\n const snappedEnd = Math.round(localX / PX_PER_BEAT / snap) * snap;\n const end = clamp(snappedEnd, startBeat + snap, totalBeats);\n return end - startBeat;\n}\n\n/**\n * `scrollTop` that vertically centers the bulk of the notes in a `viewportH`-px\n * window. Targets the MEDIAN pitch (robust to a stray high/low outlier — keeps\n * \"where the majority of notes are\" framed) and clamps to the valid scroll\n * range. `hi` is the top visible pitch; `rowCount` the total rows in the grid.\n * Returns 0 when there are no notes.\n */\nexport function centerScrollTop(\n pitches: readonly number[],\n hi: number,\n rowCount: number,\n viewportH: number,\n): number {\n if (pitches.length === 0) return 0;\n const sorted = [...pitches].sort((a, b) => a - b);\n const mid = Math.floor(sorted.length / 2);\n const median = sorted.length % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;\n // Pixel center of the median row, then offset so it lands mid-viewport.\n const medianRowCenterPx = (hi - median) * ROW_HEIGHT + ROW_HEIGHT / 2;\n const maxScroll = Math.max(0, rowCount * ROW_HEIGHT - viewportH);\n return clamp(medianRowCenterPx - viewportH / 2, 0, maxScroll);\n}\n\n/** Transpose every note by `semitones`, clamping pitch to [0,127] (never drops a note). */\nexport function transposeNotes(\n notes: readonly PluginMidiNote[],\n semitones: number,\n): PluginMidiNote[] {\n return notes.map((n) => ({ ...n, pitch: clamp(n.pitch + semitones, 0, 127) }));\n}\n\n// ============================================================================\n// Props\n// ============================================================================\n\nexport interface PianoRollEditorProps {\n /** Controlled note list (quarter-note beats). The editor never mutates this. */\n notes: readonly PluginMidiNote[];\n /** Emitted on every edit (add / delete / move / transpose) with the full next array. */\n onChange: (next: PluginMidiNote[]) => void;\n /** Scene length in bars → grid width = bars * beatsPerBar * PX_PER_BEAT. */\n bars: number;\n /** BPM — used only for audition timing in v1. */\n bpm: number;\n /** Beats per bar (time-signature numerator). Default 4. */\n beatsPerBar?: number;\n /** Snap step in quarter notes (1 = ¼ note, 0.25 = 1/16). Default 0.25. */\n snap?: number;\n /** Snap steps the toolbar selector offers. Default [1, 0.5, 0.25]. */\n snapOptions?: number[];\n /** Notified when the user changes snap (the editor still tracks it internally). */\n onSnapChange?: (snap: number) => void;\n /** Lowest pitch always visible. Default C2 (36). */\n minPitch?: number;\n /** Highest pitch always visible. Default C6 (84). */\n maxPitch?: number;\n /** Expand the visible window to include notes outside [minPitch,maxPitch]. Default true. */\n autoFit?: boolean;\n /** Optional single-note preview, fired when a note is added. */\n onAuditionNote?: (pitch: number, velocity: number, durationMs: number) => void;\n /** Velocity for newly-added notes. Default 100. */\n defaultVelocity?: number;\n /** Disable all interaction (e.g. while the track is generating). Default false. */\n disabled?: boolean;\n /** Extra className for the outer container. */\n className?: string;\n /** Test id for the outer container. Default \"sdk-piano-roll\". */\n testId?: string;\n}\n\ninterface DragState {\n /**\n * `pending-*` is an undecided press (becomes the matching committed mode once\n * the pointer travels past {@link DRAG_DEAD_ZONE}, else resolves on pointer-up):\n * pending-note → drag (move) | no travel → delete\n * pending-resize → resize | no travel → delete\n * pending-add → (on up) add a note\n */\n mode: 'pending-note' | 'pending-resize' | 'pending-add' | 'drag' | 'resize';\n /** Index into `notes` for a note press; -1 for an empty-grid press. */\n index: number;\n startX: number;\n startY: number;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function PianoRollEditor({\n notes,\n onChange,\n bars,\n bpm,\n beatsPerBar = 4,\n snap = 0.25,\n snapOptions = [1, 0.5, 0.25],\n onSnapChange,\n minPitch = 36,\n maxPitch = 84,\n autoFit = true,\n onAuditionNote,\n defaultVelocity = 100,\n disabled = false,\n className,\n testId = 'sdk-piano-roll',\n}: PianoRollEditorProps): React.ReactElement {\n const [snapState, setSnapState] = useState(snap);\n const gridRef = useRef<HTMLDivElement | null>(null);\n const scrollRef = useRef<HTMLDivElement | null>(null);\n const dragRef = useRef<DragState | null>(null);\n // True once we've auto-centered the current note set; re-armed when the notes\n // clear or the user octave-shifts, so the view re-frames only on a fresh load.\n const didCenterRef = useRef(false);\n\n // Visible pitch window: the default [minPitch, maxPitch], expanded to include\n // any notes that fall outside (± 2 semitones of headroom). Stable + testable.\n const { lo, hi } = useMemo((): { lo: number; hi: number } => {\n if (autoFit && notes.length > 0) {\n const ps = notes.map((n) => n.pitch);\n return {\n lo: Math.max(0, Math.min(minPitch, Math.min(...ps) - 2)),\n hi: Math.min(127, Math.max(maxPitch, Math.max(...ps) + 2)),\n };\n }\n return { lo: minPitch, hi: maxPitch };\n }, [autoFit, notes, minPitch, maxPitch]);\n\n const rowCount = hi - lo + 1;\n const totalBeats = bars * beatsPerBar;\n const gridWidth = totalBeats * PX_PER_BEAT;\n const gridHeight = rowCount * ROW_HEIGHT;\n\n // Latest values for the stable pointer handlers — avoids stale closures and\n // handler re-binding (the documented render-loop hazard). Assigned during\n // render so the handlers always read current props/state.\n const stateRef = useRef({\n notes, onChange, snapState, hi, bars, beatsPerBar, defaultVelocity, bpm, onAuditionNote, disabled,\n });\n stateRef.current = {\n notes, onChange, snapState, hi, bars, beatsPerBar, defaultVelocity, bpm, onAuditionNote, disabled,\n };\n\n const localCoords = useCallback((clientX: number, clientY: number): { x: number; y: number } => {\n const rect = gridRef.current?.getBoundingClientRect();\n return { x: clientX - (rect?.left ?? 0), y: clientY - (rect?.top ?? 0) };\n }, []);\n\n const handlePointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>): void => {\n if (stateRef.current.disabled) return;\n const target = e.target as HTMLElement;\n const noteEl = target.closest('[data-testid=\"sdk-pr-note\"]') as HTMLElement | null;\n const idxAttr = noteEl?.getAttribute('data-index');\n // A press that lands on the note's right-edge handle resizes; anywhere else\n // on the note moves/deletes; empty grid adds.\n const onResizeHandle = idxAttr != null && target.closest('[data-resize-handle]') != null;\n dragRef.current = {\n mode: idxAttr == null ? 'pending-add' : onResizeHandle ? 'pending-resize' : 'pending-note',\n index: idxAttr != null ? Number(idxAttr) : -1,\n startX: e.clientX,\n startY: e.clientY,\n };\n try {\n (e.currentTarget as HTMLElement).setPointerCapture?.(e.pointerId);\n } catch {\n /* jsdom / unsupported — drag still works via grid-level handlers */\n }\n }, []);\n\n const handlePointerMove = useCallback((e: React.PointerEvent<HTMLDivElement>): void => {\n const drag = dragRef.current;\n if (!drag) return;\n const dist = Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY);\n if (dist > DRAG_DEAD_ZONE) {\n if (drag.mode === 'pending-note') drag.mode = 'drag';\n else if (drag.mode === 'pending-resize') drag.mode = 'resize';\n }\n const s = stateRef.current;\n const { x, y } = localCoords(e.clientX, e.clientY);\n\n if (drag.mode === 'resize') {\n const note = s.notes[drag.index];\n if (!note) return;\n const durationBeats = resizeNoteDuration(note.startBeat, x, s.snapState, s.bars, s.beatsPerBar);\n if (durationBeats === note.durationBeats) return;\n const next = s.notes.map((n, i) => (i === drag.index ? { ...n, durationBeats } : n));\n s.onChange(next);\n return;\n }\n\n if (drag.mode !== 'drag') return;\n const { pitch, startBeat } = pxToCell(x, y, s.hi, s.snapState, s.bars, s.beatsPerBar);\n const next = s.notes.map((n, i) => (i === drag.index ? { ...n, pitch, startBeat } : n));\n s.onChange(next);\n }, [localCoords]);\n\n const handlePointerUp = useCallback((e: React.PointerEvent<HTMLDivElement>): void => {\n const drag = dragRef.current;\n dragRef.current = null;\n if (!drag) return;\n const s = stateRef.current;\n if (s.disabled) return;\n\n if (drag.mode === 'pending-note' || drag.mode === 'pending-resize') {\n // Pressed a note (body or resize handle) without dragging past the dead\n // zone → treat as a plain click → delete it.\n s.onChange(s.notes.filter((_, i) => i !== drag.index));\n return;\n }\n if (drag.mode === 'pending-add') {\n const { x, y } = localCoords(e.clientX, e.clientY);\n const { pitch, startBeat } = pxToCell(x, y, s.hi, s.snapState, s.bars, s.beatsPerBar);\n const note: PluginMidiNote = {\n pitch,\n startBeat,\n durationBeats: s.snapState,\n velocity: s.defaultVelocity,\n channel: 0,\n };\n s.onChange([...s.notes, note]);\n s.onAuditionNote?.(pitch, s.defaultVelocity, Math.max(1, s.snapState * (60 / s.bpm) * 1000));\n }\n // mode 'drag' / 'resize' already emitted their final state during pointermove.\n }, [localCoords]);\n\n const handlePointerCancel = useCallback((): void => {\n dragRef.current = null;\n }, []);\n\n const handleOctave = useCallback((delta: number): void => {\n const s = stateRef.current;\n if (s.disabled) return;\n // The whole clip jumps an octave — re-frame the view onto its new position.\n didCenterRef.current = false;\n s.onChange(transposeNotes(s.notes, delta));\n }, []);\n\n const handleSnapChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>): void => {\n const v = Number(e.target.value);\n setSnapState(v);\n onSnapChange?.(v);\n }, [onSnapChange]);\n\n // Auto-frame the notes on load: the autoFit window already contains every note\n // vertically, but the scroll viewport starts pinned to the top — so a melody\n // sitting low needs a manual scroll to find. Center the note cluster once per\n // load (re-armed on clear / octave-shift), never mid-edit or mid-drag.\n useLayoutEffect(() => {\n const el = scrollRef.current;\n if (!el) return;\n if (notes.length === 0) {\n didCenterRef.current = false;\n return;\n }\n if (didCenterRef.current || dragRef.current) return;\n didCenterRef.current = true;\n const viewportH = el.clientHeight || SCROLL_MAX_H;\n el.scrollTop = centerScrollTop(\n notes.map((n) => n.pitch),\n hi,\n rowCount,\n viewportH,\n );\n }, [notes, hi, rowCount]);\n\n // Pitch rows for the keyboard gutter, top (hi) first.\n const rows = useMemo((): number[] => {\n const out: number[] = [];\n for (let p = hi; p >= lo; p--) out.push(p);\n return out;\n }, [hi, lo]);\n\n // Beat columns + bar columns + row lines, drawn purely in CSS so the only\n // hit-testable DOM in the grid is the notes themselves.\n const gridBg = useMemo((): string => {\n const beatPx = PX_PER_BEAT;\n const barPx = PX_PER_BEAT * beatsPerBar;\n return [\n `repeating-linear-gradient(to right, transparent 0 ${beatPx - 1}px, rgba(255,255,255,0.06) ${beatPx - 1}px ${beatPx}px)`,\n `repeating-linear-gradient(to right, transparent 0 ${barPx - 1}px, rgba(255,255,255,0.16) ${barPx - 1}px ${barPx}px)`,\n `repeating-linear-gradient(to bottom, transparent 0 ${ROW_HEIGHT - 1}px, rgba(255,255,255,0.04) ${ROW_HEIGHT - 1}px ${ROW_HEIGHT}px)`,\n ].join(', ');\n }, [beatsPerBar]);\n\n const octaveDisabled = disabled || notes.length === 0;\n\n return (\n <div className={`flex flex-col gap-1 ${className ?? ''}`} data-testid={testId}>\n {/* Toolbar */}\n <div className=\"flex items-center gap-1\" data-testid=\"sdk-pr-toolbar\">\n <button\n type=\"button\"\n data-testid=\"sdk-pr-octave-down\"\n disabled={octaveDisabled}\n onClick={() => handleOctave(-12)}\n className=\"px-1.5 py-0.5 text-[10px] rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors disabled:opacity-40\"\n title=\"Octave down (−12 semitones)\"\n >\n Oct −\n </button>\n <button\n type=\"button\"\n data-testid=\"sdk-pr-octave-up\"\n disabled={octaveDisabled}\n onClick={() => handleOctave(12)}\n className=\"px-1.5 py-0.5 text-[10px] rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors disabled:opacity-40\"\n title=\"Octave up (+12 semitones)\"\n >\n Oct +\n </button>\n <label className=\"flex items-center gap-1 text-[10px] text-sas-muted/70 ml-1\">\n Snap\n <select\n data-testid=\"sdk-pr-snap\"\n value={snapState}\n disabled={disabled}\n onChange={handleSnapChange}\n className=\"sas-input px-1 py-0.5 text-[10px]\"\n >\n {snapOptions.map((s) => (\n <option key={s} value={s}>\n {snapLabel(s)}\n </option>\n ))}\n </select>\n </label>\n <span className=\"text-[10px] text-sas-muted/60 ml-auto\" data-testid=\"sdk-pr-note-count\">\n {notes.length} {notes.length === 1 ? 'note' : 'notes'}\n </span>\n </div>\n\n {/* Scroll region: keyboard gutter + note grid */}\n <div\n ref={scrollRef}\n className=\"overflow-auto border border-sas-border rounded-sm bg-sas-bg\"\n style={{ maxHeight: SCROLL_MAX_H }}\n data-testid=\"sdk-pr-scroll\"\n >\n <div className=\"flex\" style={{ width: GUTTER_W + gridWidth }}>\n {/* Keyboard gutter — pinned left during horizontal scroll */}\n <div\n data-testid=\"sdk-pr-gutter\"\n className=\"sticky left-0 z-10 flex-shrink-0 bg-sas-panel-alt\"\n style={{ width: GUTTER_W }}\n >\n {rows.map((p) => (\n <div\n key={p}\n data-testid=\"sdk-pr-key\"\n data-pitch={p}\n className={`flex items-center justify-end pr-1 text-[8px] leading-none border-b border-sas-border/30 ${\n BLACK_KEYS.has(((p % 12) + 12) % 12)\n ? 'bg-sas-bg text-sas-muted/40'\n : 'text-sas-muted/70'\n }`}\n style={{ height: ROW_HEIGHT }}\n >\n {p % 12 === 0 ? pitchToName(p) : ''}\n </div>\n ))}\n </div>\n\n {/* Note grid */}\n <div\n ref={gridRef}\n data-testid=\"sdk-pr-grid\"\n className=\"relative flex-shrink-0\"\n style={{\n width: gridWidth,\n height: gridHeight,\n backgroundImage: gridBg,\n cursor: disabled ? 'not-allowed' : 'crosshair',\n touchAction: 'none',\n }}\n onPointerDown={handlePointerDown}\n onPointerMove={handlePointerMove}\n onPointerUp={handlePointerUp}\n onPointerCancel={handlePointerCancel}\n >\n {notes.map((n, i) => {\n const { left, top } = cellToPx(n.pitch, n.startBeat, hi);\n const width = Math.max(3, n.durationBeats * PX_PER_BEAT);\n // Handle never exceeds half the note, so even a 1-step note keeps a\n // left \"body\" zone for moving.\n const handleW = Math.min(RESIZE_HANDLE_PX, width / 2);\n return (\n <div\n key={i}\n data-testid=\"sdk-pr-note\"\n data-index={i}\n data-pitch={n.pitch}\n data-start-beat={n.startBeat}\n data-duration-beats={n.durationBeats}\n className=\"absolute rounded-[2px] bg-sas-accent/80 border border-sas-accent hover:bg-sas-accent\"\n style={{ left, top, width, height: ROW_HEIGHT }}\n title={`${pitchToName(n.pitch)} · beat ${n.startBeat} · ${n.durationBeats}♪ · vel ${n.velocity}`}\n >\n {!disabled && (\n <div\n data-resize-handle=\"\"\n data-testid=\"sdk-pr-note-resize\"\n className=\"absolute top-0 right-0 h-full rounded-r-[2px] hover:bg-sas-bg/40\"\n style={{ width: handleW, cursor: 'ew-resize' }}\n />\n )}\n </div>\n );\n })}\n {notes.length === 0 && (\n <div\n data-testid=\"sdk-pr-empty\"\n className=\"absolute inset-0 flex items-center justify-center text-[10px] text-sas-muted/50 pointer-events-none\"\n >\n No notes — click to add\n </div>\n )}\n </div>\n </div>\n </div>\n </div>\n );\n}\n\nexport default PianoRollEditor;\n","/**\n * ConfirmDialog — styled in-app confirmation modal (SDK component).\n *\n * A small, reusable \"are you sure?\" dialog matching the app's dark theme\n * (mirrors ImportTrackModal chrome: sas-panel / sas-border / shadow-xl). It\n * guards destructive actions; the first consumer is track deletion, which was\n * one stray click away from losing a track's MIDI + sound.\n *\n * Controlled component — the caller owns `open` and the confirm/cancel\n * handlers. Escape and a backdrop click both cancel, and the Cancel button is\n * auto-focused on open so a reflexive Enter dismisses rather than deletes.\n *\n * @since SDK 2.17.0\n */\n\nimport React, { useRef } from 'react';\nimport { Modal } from './Modal';\n\nexport interface ConfirmDialogProps {\n /** Controls visibility (the caller owns open/closed). */\n open: boolean;\n /** Bold heading line. */\n title: string;\n /** Body copy — a string or richer node. */\n message: React.ReactNode;\n /** Confirm button label (default \"Delete\"). */\n confirmLabel?: string;\n /** Cancel button label (default \"Cancel\"). */\n cancelLabel?: string;\n /** When true (default), the confirm button reads as a destructive (red) action. */\n destructive?: boolean;\n /** Fired when the user confirms. */\n onConfirm: () => void;\n /** Fired on Cancel, Escape, or backdrop click. */\n onCancel: () => void;\n /** data-testid prefix so each dialog is addressable in tests. */\n testIdPrefix?: string;\n}\n\nexport function ConfirmDialog({\n open,\n title,\n message,\n confirmLabel = 'Delete',\n cancelLabel = 'Cancel',\n destructive = true,\n onConfirm,\n onCancel,\n testIdPrefix = 'confirm-dialog',\n}: ConfirmDialogProps): React.ReactElement | null {\n const cancelRef = useRef<HTMLButtonElement>(null);\n\n // Escape, backdrop click, and focus-on-open are owned by the shared <Modal>.\n return (\n <Modal open={open} onClose={onCancel} testIdPrefix={testIdPrefix} initialFocusRef={cancelRef}>\n <div\n className=\"w-[360px] max-w-[90vw] flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl\"\n onClick={(e) => e.stopPropagation()}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n data-testid={`${testIdPrefix}-modal`}\n >\n {/* Header */}\n <div className=\"px-4 py-3 border-b border-sas-border\">\n <span className=\"text-sm font-medium text-sas-text\" data-testid={`${testIdPrefix}-title`}>\n {title}\n </span>\n </div>\n\n {/* Body */}\n <div\n className=\"px-4 py-3 text-xs text-sas-muted leading-relaxed break-words\"\n data-testid={`${testIdPrefix}-message`}\n >\n {message}\n </div>\n\n {/* Footer */}\n <div className=\"flex justify-end gap-2 px-4 py-3 border-t border-sas-border\">\n <button\n ref={cancelRef}\n type=\"button\"\n className=\"px-3 py-1 rounded-sm text-xs font-medium border border-sas-border bg-sas-panel-alt text-sas-text hover:border-sas-accent hover:text-sas-accent transition-colors\"\n onClick={onCancel}\n data-testid={`${testIdPrefix}-cancel`}\n >\n {cancelLabel}\n </button>\n <button\n type=\"button\"\n className={`px-3 py-1 rounded-sm text-xs font-medium border transition-colors ${\n destructive\n ? 'border-sas-danger bg-sas-danger/20 text-sas-danger hover:bg-sas-danger hover:text-sas-bg'\n : 'border-sas-accent bg-sas-accent/20 text-sas-accent hover:bg-sas-accent hover:text-sas-bg'\n }`}\n onClick={onConfirm}\n data-testid={`${testIdPrefix}-confirm`}\n >\n {confirmLabel}\n </button>\n </div>\n </div>\n </Modal>\n );\n}\n\nexport default ConfirmDialog;\n","/**\n * Modal — the SDK's one modal-stacking primitive (portal + z-tier + backdrop).\n *\n * Every SDK modal renders INSIDE a plugin's accordion section, whose animated\n * `overflow-hidden` + `transition-all` wrapper establishes a stacking context.\n * An inline `position: fixed` overlay is therefore scoped to that section and\n * can be painted UNDER a neighbouring panel (the \"import modal invisible on a\n * later open\" bug). This component solves that once: it portals the overlay to\n * <body> — out of every panel's stacking context — at a z-tier above all the\n * app's `z-50` dropdowns/banners but below the toast tier (`z-[9999]`), so\n * toasts still float over modals.\n *\n * Controlled: the caller owns `open` and `onClose`. The caller renders its own\n * dialog box as `children` (keep the box's `onClick={e => e.stopPropagation()}`\n * so inside-clicks don't dismiss). Escape and a backdrop click both close.\n *\n * @since SDK 2.21.0\n */\n\nimport React, { useEffect } from 'react';\nimport { createPortal } from 'react-dom';\n\nexport interface ModalProps {\n /** Controls visibility (the caller owns open/closed). */\n open: boolean;\n /** Close handler — fired on Escape and backdrop click. */\n onClose: () => void;\n /** The dialog box. Give it `onClick={e => e.stopPropagation()}`. */\n children: React.ReactNode;\n /** data-testid prefix; the backdrop is `${testIdPrefix}-overlay`. */\n testIdPrefix?: string;\n /** Close when the backdrop is clicked (default true). */\n closeOnBackdrop?: boolean;\n /** Close on Escape (default true). */\n closeOnEscape?: boolean;\n /** Focused when the modal opens (e.g. a Cancel button) so a reflexive Enter is safe. */\n initialFocusRef?: React.RefObject<HTMLElement>;\n}\n\nexport function Modal({\n open,\n onClose,\n children,\n testIdPrefix = 'modal',\n closeOnBackdrop = true,\n closeOnEscape = true,\n initialFocusRef,\n}: ModalProps): React.ReactElement | null {\n // Escape closes; focus the requested element on open.\n useEffect(() => {\n if (!open) return undefined;\n const onKey = (e: KeyboardEvent): void => {\n if (closeOnEscape && e.key === 'Escape') {\n e.preventDefault();\n onClose();\n }\n };\n window.addEventListener('keydown', onKey);\n initialFocusRef?.current?.focus();\n return () => window.removeEventListener('keydown', onKey);\n }, [open, onClose, closeOnEscape, initialFocusRef]);\n\n if (!open) return null;\n\n return createPortal(\n <div\n className=\"fixed inset-0 z-[1000] flex items-center justify-center bg-black/60\"\n data-testid={`${testIdPrefix}-overlay`}\n onClick={closeOnBackdrop ? onClose : undefined}\n >\n {children}\n </div>,\n document.body,\n );\n}\n\nexport default Modal;\n","/**\n * Shared level-meter component.\n *\n * Renders a horizontal LED-style bar over -60dBFS → 0dBFS:\n * - A fixed left-to-right gradient (green → orange → red), so the color is\n * tied to POSITION: a quiet signal lights only the green left, a hot signal\n * reaches the red right. An \"unlit\" mask hides the gradient beyond the\n * current level.\n * - A deterministic segment grid (the \"LED monitor\" look) drawn as a pure-CSS\n * repeating overlay — constant DOM, no per-frame cost.\n * - An optional peak-hold marker (`peakHoldDb`) — a bright line at the recent\n * maximum that the caller holds/decays (see `useTrackMeter`).\n * - An optional CLIP badge the caller wires up.\n *\n * Pure presentational: takes the current dB + `active` flag (+ optional held\n * peak) and draws. The only production consumer is the per-track strip\n * (`TrackMeterStrip`, via `compact`). `compact` shrinks the bar and drops the\n * numeric dB readout.\n */\n\nimport React from 'react';\n\n// Traffic-light gradient (introduced for the LED meter; the Magic Terminal\n// palette has no green/orange/red tokens). Tweakable.\nconst COLOR_GREEN = '#2BD576';\nconst COLOR_ORANGE = '#F5A623';\nconst COLOR_RED = '#FF4D5E';\nconst COLOR_TRACK_BG = '#121822'; // panel-alt — the unlit bar / mask\nconst COLOR_TRACK_BORDER = '#1F2A3A'; // border\nconst COLOR_SEGMENT_GAP = '#0A0E14'; // dark gutter between LED cells\nconst COLOR_PEAK = '#F7FFFB'; // held-peak marker (bright)\n\n// The positional gradient. Mostly green, orange in the upper-mid, red near the\n// top — the classic meter feel, while still visibly tri-color across the bar.\nconst METER_GRADIENT = `linear-gradient(90deg, ${COLOR_GREEN} 0%, ${COLOR_GREEN} 45%, ${COLOR_ORANGE} 72%, ${COLOR_RED} 90%, ${COLOR_RED} 100%)`;\n\n// Deterministic LED sections + the gutter width between them.\nconst SEGMENTS = 22;\nconst SEGMENT_GAP_PX = 2;\n\n/** dBFS → bar % : -60dB → 0%, 0dB → 100%, clamped. */\nfunction dbToPct(db: number): number {\n return Math.max(0, Math.min(100, ((db + 60) / 60) * 100));\n}\n\nexport interface LevelMeterProps {\n /** Current peak level in dBFS. -120 means \"no signal\". */\n peakDb: number;\n /** True when the underlying audio callback is firing. False = floor. */\n active: boolean;\n /**\n * Held peak in dBFS for the peak-hold marker. Omit to draw no marker. The\n * marker is hidden when this is at/below the visible floor (-60).\n */\n peakHoldDb?: number;\n /** Latched clip flag. When true, render the CLIP badge. */\n clipped?: boolean;\n /** User-clickable handler to clear the latched clip indicator. */\n onClearClip?: () => void;\n /**\n * Thin strip mode for per-track meters: hides the numeric dB readout and\n * shrinks the bar. Keeps the (rare) CLIP badge.\n */\n compact?: boolean;\n /** Optional className overlaid on the wrapper for layout tweaks. */\n className?: string;\n /** Inline test id — make multiple instances distinguishable. */\n 'data-testid'?: string;\n}\n\nexport const LevelMeter: React.FC<LevelMeterProps> = ({\n peakDb,\n active,\n peakHoldDb,\n clipped,\n onClearClip,\n compact = false,\n className,\n 'data-testid': testId,\n}) => {\n const id = testId ?? 'sas-level-meter';\n const widthPct = active ? dbToPct(peakDb) : 0;\n const showPeak = peakHoldDb != null && active && peakHoldDb > -60;\n const peakHoldPct = showPeak ? dbToPct(peakHoldDb!) : 0;\n\n return (\n <div\n className={`sas-level-meter ${className ?? ''}`}\n data-testid={id}\n style={{\n display: 'flex',\n alignItems: 'center',\n gap: compact ? 0 : 6,\n }}\n >\n <div\n style={{\n position: 'relative',\n flex: 1,\n height: compact ? 5 : 7,\n background: COLOR_TRACK_BG,\n border: `1px solid ${COLOR_TRACK_BORDER}`,\n borderRadius: 2,\n overflow: 'hidden',\n minWidth: compact ? 0 : 60,\n }}\n >\n {/* Positional green→orange→red gradient, full bar width. */}\n <div style={{ position: 'absolute', inset: 0, background: METER_GRADIENT }} />\n\n {/* Unlit mask: hides the gradient from the current level rightward. */}\n <div\n style={{\n position: 'absolute',\n top: 0,\n bottom: 0,\n left: `${widthPct}%`,\n right: 0,\n background: COLOR_TRACK_BG,\n transition: 'left 30ms linear',\n }}\n />\n\n {/* Deterministic LED segment gutters — pure CSS, constant DOM. */}\n <div\n data-testid={`${id}-segments`}\n style={{\n position: 'absolute',\n inset: 0,\n pointerEvents: 'none',\n backgroundImage: `linear-gradient(90deg, transparent 0, transparent calc(100% - ${SEGMENT_GAP_PX}px), ${COLOR_SEGMENT_GAP} calc(100% - ${SEGMENT_GAP_PX}px), ${COLOR_SEGMENT_GAP} 100%)`,\n backgroundSize: `calc(100% / ${SEGMENTS}) 100%`,\n }}\n />\n\n {/* Peak-hold marker: a bright line at the recent maximum. */}\n {showPeak && (\n <div\n data-testid={`${id}-peak`}\n style={{\n position: 'absolute',\n top: -1,\n bottom: -1,\n left: `${peakHoldPct}%`,\n width: 2,\n marginLeft: -1,\n background: COLOR_PEAK,\n boxShadow: '0 0 4px rgba(247, 255, 251, 0.7)',\n transition: 'left 80ms linear',\n }}\n title=\"Peak\"\n />\n )}\n </div>\n\n {!compact && (\n <span\n style={{\n fontSize: 10,\n color: 'var(--sas-muted, #888)',\n fontVariantNumeric: 'tabular-nums',\n minWidth: 48,\n textAlign: 'right',\n }}\n >\n {active && peakDb > -120 ? `${peakDb.toFixed(0)} dB` : '—'}\n </span>\n )}\n {clipped && (\n <span\n data-testid={`${id}-clip`}\n onClick={onClearClip}\n style={{\n padding: '1px 5px',\n fontSize: 9,\n fontWeight: 'bold',\n background: COLOR_RED,\n color: '#0A0E14',\n borderRadius: 2,\n cursor: onClearClip ? 'pointer' : 'default',\n marginLeft: compact ? 3 : 0,\n }}\n title={onClearClip ? 'Clipped — click to clear' : 'Clipped'}\n >\n CLIP\n </span>\n )}\n </div>\n );\n};\n\nexport default LevelMeter;\n","/**\n * useTrackLevels — drives the cosmetic per-track strip meters.\n *\n * The hard constraint for this feature is \"playback ALWAYS wins over the GUI;\n * NO blocking threads.\" This hook is built around that:\n *\n * - It polls `host.getTrackLevels()` at ~30Hz with a recursive setTimeout that\n * only schedules the NEXT tick AFTER the previous await resolves. That is\n * automatic backpressure: a slow/stalled engine simply slows the meter, it\n * can never queue a backlog of requests. (The host + bridge also coalesce,\n * so a busy engine yields a STALE snapshot, never a pile-up.)\n * - It writes into a ref-held Map and notifies row subscribers, so the OWNING\n * panel never re-renders at 30Hz. Each row reads its own value via\n * `useTrackLevel` and re-renders only itself.\n * - It polls while the panel is mounted and the window is visible, and pauses\n * when the window is hidden. It deliberately does NOT gate on transport\n * \"is playing\": this app drives playback through decks / the clip launcher,\n * and the linear-transport play flag does not track that reliably. When\n * audio is stopped the engine simply returns floor levels, so the bars are\n * empty anyway — no need (and no reliable signal) to stop polling.\n *\n * Usage (panel):\n * const levels = useTrackLevels(host);\n * ...<TrackRow levels={levels} ... /> // row calls useTrackLevel(levels, id)\n */\n\nimport { useEffect, useRef, useState } from 'react';\nimport type { PluginHost, PluginTrackLevel } from '../types/plugin-sdk.types';\n\n/** Polling cadence — matches the recording input meter (~30Hz). */\nconst POLL_INTERVAL_MS = 33;\n/** Slow idle re-check while the window is hidden (polling is paused). */\nconst HIDDEN_RECHECK_MS = 250;\n\n/** dBFS floor / \"no signal\" sentinel (matches PluginTrackLevel). */\nconst METER_FLOOR_DB = -120;\n/** Hold the peak marker this long after a fresh peak before it starts to fall. */\nconst PEAK_HOLD_MS = 1500;\n/** Fall rate once the hold window expires (dB per second). */\nconst PEAK_DECAY_DB_PER_SEC = 24;\n\n/**\n * Stable handle returned by {@link useTrackLevels}. Rows read their own level\n * and subscribe to per-tick notifications through it; its identity is stable\n * across renders so a row's subscription is set up once.\n */\nexport interface TrackLevelsHandle {\n /** Current level for a track, or null when idle/absent (renders an empty bar). */\n getLevel(trackId: string): PluginTrackLevel | null;\n /** Subscribe to per-tick updates. Returns an unsubscribe function. */\n subscribe(listener: () => void): () => void;\n}\n\nfunction isHidden(): boolean {\n return typeof document !== 'undefined' && document.hidden === true;\n}\n\n/**\n * Poll every owned track's level while mounted + visible. Returns a stable\n * handle; the owning component does NOT re-render per tick. Pass `enabled =\n * false` to turn it off entirely (e.g. a panel that wants no meters). Safe to\n * call even when the host predates `getTrackLevels` (older SDK) — it stays idle.\n */\nexport function useTrackLevels(\n host: PluginHost | null | undefined,\n enabled: boolean = true\n): TrackLevelsHandle {\n const mapRef = useRef<Map<string, PluginTrackLevel>>(new Map());\n const listenersRef = useRef<Set<() => void>>(new Set());\n\n // Built exactly once so the handle identity is stable across renders.\n const handleRef = useRef<TrackLevelsHandle | null>(null);\n if (handleRef.current === null) {\n handleRef.current = {\n getLevel: (trackId: string) => mapRef.current.get(trackId) ?? null,\n subscribe: (listener: () => void) => {\n listenersRef.current.add(listener);\n return () => {\n listenersRef.current.delete(listener);\n };\n },\n };\n }\n\n useEffect(() => {\n const notify = (): void => {\n listenersRef.current.forEach((l) => l());\n };\n\n const clearToIdle = (): void => {\n if (mapRef.current.size > 0) {\n mapRef.current.clear();\n notify();\n }\n };\n\n const canPoll =\n enabled && !!host && typeof host.getTrackLevels === 'function';\n\n if (!canPoll) {\n clearToIdle();\n return;\n }\n\n let stopped = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const schedule = (delay: number): void => {\n if (stopped) return;\n timer = setTimeout(tick, delay);\n };\n\n const tick = async (): Promise<void> => {\n if (stopped) return;\n\n // Paused while the window is hidden: do no engine work, just idle-poll\n // until it comes back. (visibilitychange below resumes immediately.)\n if (isHidden()) {\n schedule(HIDDEN_RECHECK_MS);\n return;\n }\n\n try {\n const levels = await host!.getTrackLevels!();\n if (stopped) return;\n\n // Rebuild the map: upsert present tracks, drop ones that vanished.\n const seen = new Set<string>();\n for (const lvl of levels) {\n mapRef.current.set(lvl.trackId, lvl);\n seen.add(lvl.trackId);\n }\n for (const key of Array.from(mapRef.current.keys())) {\n if (!seen.has(key)) mapRef.current.delete(key);\n }\n notify();\n } catch {\n // Cosmetic meter: swallow transient read failures and keep polling.\n }\n\n // Schedule the NEXT tick only now — backpressure: never overlap reads.\n schedule(POLL_INTERVAL_MS);\n };\n\n const onVisibility = (): void => {\n if (stopped) return;\n if (!isHidden()) {\n // Becoming visible: cancel the slow idle-poll and resume immediately.\n if (timer) clearTimeout(timer);\n void tick();\n }\n };\n\n if (typeof document !== 'undefined') {\n document.addEventListener('visibilitychange', onVisibility);\n }\n\n void tick();\n\n return () => {\n stopped = true;\n if (timer) clearTimeout(timer);\n if (typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', onVisibility);\n }\n // Leave the map intact on teardown; the next active effect rebuilds it.\n };\n }, [host, enabled]);\n\n return handleRef.current;\n}\n\n/** Cheap equality so unchanged rows skip re-rendering between ticks. */\nfunction sameLevel(\n a: PluginTrackLevel | null,\n b: PluginTrackLevel | null\n): boolean {\n if (a === b) return true;\n if (a === null || b === null) return false;\n return a.peakDb === b.peakDb && a.clipped === b.clipped;\n}\n\n/**\n * Per-row selector. Subscribes to the shared scheduler and re-renders ONLY the\n * calling component when this track's level changes. Returns null when idle\n * (transport stopped, window hidden, or the track has no meter yet).\n */\nexport function useTrackLevel(\n handle: TrackLevelsHandle | null | undefined,\n trackId: string\n): PluginTrackLevel | null {\n const [level, setLevel] = useState<PluginTrackLevel | null>(null);\n\n useEffect(() => {\n if (!handle) {\n setLevel(null);\n return;\n }\n const update = (): void => {\n const next = handle.getLevel(trackId);\n setLevel((prev) => (sameLevel(prev, next) ? prev : next));\n };\n update(); // seed immediately\n return handle.subscribe(update);\n }, [handle, trackId]);\n\n return level;\n}\n\n/**\n * Per-row meter view-model: the current level plus a held peak for the meter UI.\n */\nexport interface TrackMeterView {\n /** Current mono peak in dBFS (floored at -120). */\n peakDb: number;\n /** Held peak in dBFS — stays at the recent maximum for ~PEAK_HOLD_MS, then falls. */\n peakHoldDb: number;\n /** Latched clip flag for the last poll window. */\n clipped: boolean;\n /** True when the track currently has a live meter row. */\n active: boolean;\n}\n\nconst IDLE_METER_VIEW: TrackMeterView = {\n peakDb: METER_FLOOR_DB,\n peakHoldDb: METER_FLOOR_DB,\n clipped: false,\n active: false,\n};\n\n/** Equality gate for the meter view. Quantizes the held peak to ½ dB so a\n * steady hold and sub-pixel decay don't thrash renders, while a real change\n * (level jitter, decay step, clip, active) still re-renders the strip. */\nfunction sameMeter(a: TrackMeterView, b: TrackMeterView): boolean {\n return (\n a.active === b.active &&\n a.clipped === b.clipped &&\n a.peakDb === b.peakDb &&\n Math.round(a.peakHoldDb * 2) === Math.round(b.peakHoldDb * 2)\n );\n}\n\n/**\n * Per-row meter selector WITH PEAK-HOLD. Like {@link useTrackLevel} it subscribes\n * to the shared ~30Hz scheduler and re-renders only the calling component, but it\n * also tracks a held peak that stays at the recent maximum for ~PEAK_HOLD_MS then\n * decays — so the eye can register where the signal peaked while the bar itself\n * moves fast. No extra timers or rAF: the held value is recomputed on each\n * scheduler notify, using performance.now() for hold/decay timing.\n */\nexport function useTrackMeter(\n handle: TrackLevelsHandle | null | undefined,\n trackId: string\n): TrackMeterView {\n const [view, setView] = useState<TrackMeterView>(IDLE_METER_VIEW);\n\n // Peak-hold state lives in refs so it survives between notifies without\n // forcing a render; only the derived `view` is state.\n const heldDbRef = useRef(METER_FLOOR_DB);\n const heldAtRef = useRef(0);\n const lastTickRef = useRef(0);\n\n useEffect(() => {\n if (!handle) {\n heldDbRef.current = METER_FLOOR_DB;\n lastTickRef.current = 0;\n setView(IDLE_METER_VIEW);\n return;\n }\n\n const update = (): void => {\n const level = handle.getLevel(trackId);\n const now = performance.now();\n const dtSec = lastTickRef.current ? Math.max(0, (now - lastTickRef.current) / 1000) : 0;\n lastTickRef.current = now;\n\n if (level === null) {\n // No live row for this track — go idle and reset the hold.\n heldDbRef.current = METER_FLOOR_DB;\n setView((prev) => (sameMeter(prev, IDLE_METER_VIEW) ? prev : IDLE_METER_VIEW));\n return;\n }\n\n const p = level.peakDb;\n if (p >= heldDbRef.current) {\n // Fresh peak: snap the held value up and restart the hold window.\n heldDbRef.current = p;\n heldAtRef.current = now;\n } else if (now - heldAtRef.current > PEAK_HOLD_MS) {\n // Hold expired: fall toward the current level.\n heldDbRef.current = Math.max(p, heldDbRef.current - PEAK_DECAY_DB_PER_SEC * dtSec);\n }\n // else: still within the hold window — keep the held value steady.\n\n const next: TrackMeterView = {\n peakDb: p,\n peakHoldDb: heldDbRef.current,\n clipped: level.clipped,\n active: true,\n };\n setView((prev) => (sameMeter(prev, next) ? prev : next));\n };\n\n update(); // seed immediately\n return handle.subscribe(update);\n }, [handle, trackId]);\n\n return view;\n}\n\n/**\n * Track the transport's play/stop state for a plugin. Seeds from\n * `getTransportState()` and follows `onTransportEvent`. Use its result as the\n * `active` arg to {@link useTrackLevels} so meters animate only during playback.\n */\nexport function useTransportPlaying(host: PluginHost | null | undefined): boolean {\n const [playing, setPlaying] = useState(false);\n\n useEffect(() => {\n if (!host) {\n setPlaying(false);\n return;\n }\n let cancelled = false;\n\n host\n .getTransportState()\n .then((state) => {\n if (!cancelled) setPlaying(!!state.isPlaying);\n })\n .catch(() => {\n /* seed best-effort; events will correct it */\n });\n\n const unsub = host.onTransportEvent?.((evt) => {\n if (typeof evt.isPlaying === 'boolean') {\n setPlaying(evt.isPlaying);\n } else if (evt.type === 'play') {\n setPlaying(true);\n } else if (evt.type === 'stop' || evt.type === 'pause') {\n setPlaying(false);\n }\n });\n\n return () => {\n cancelled = true;\n unsub?.();\n };\n }, [host]);\n\n return playing;\n}\n","/**\n * TrackMeterStrip — the thin per-track peak meter welded to the bottom of a\n * track row. Cosmetic: gives a general sense of each track's level and adds\n * motion during playback.\n *\n * This is deliberately its OWN component so the per-row meter selector\n * (`useTrackMeter`) re-renders ONLY this strip at ~30Hz, never the heavy\n * TrackRow around it. Render it as a full-width sibling directly under a row\n * body; it welds on with a squared top edge (like the track drawer does).\n */\n\nimport React from 'react';\nimport { LevelMeter } from './LevelMeter';\nimport { useTrackMeter, type TrackLevelsHandle } from '../hooks/useTrackLevels';\n\nexport interface TrackMeterStripProps {\n /** Shared meter handle from `useTrackLevels(host, isPlaying)`. */\n levels: TrackLevelsHandle;\n /** Tracktion engine track id (matches `PluginTrackHandle.id`). */\n trackId: string;\n /** Round the bottom corners (false when a drawer welds on below). Default true. */\n roundBottom?: boolean;\n /** Optional className for layout tweaks on the wrapper. */\n className?: string;\n}\n\nexport const TrackMeterStrip: React.FC<TrackMeterStripProps> = ({\n levels,\n trackId,\n roundBottom = true,\n className,\n}) => {\n const meter = useTrackMeter(levels, trackId);\n\n return (\n <div\n data-testid=\"sdk-track-meter\"\n className={`w-full px-2 py-1 bg-sas-panel-alt border border-t-0 border-sas-border ${roundBottom ? 'rounded-b-sm' : ''} ${className ?? ''}`}\n >\n <LevelMeter\n compact\n active={meter.active}\n peakDb={meter.peakDb}\n peakHoldDb={meter.peakHoldDb}\n clipped={meter.clipped}\n data-testid={`sdk-track-meter-bar-${trackId}`}\n />\n </div>\n );\n};\n\nexport default TrackMeterStrip;\n","/**\n * VolumeSlider Component\n *\n * Compact horizontal volume slider for track volume control.\n * Uses native HTML range input with custom styling.\n */\n\nimport React, { useCallback, useState, useRef, useEffect } from 'react';\nimport { sliderToDb } from '../utils/volume-conversion';\n\ninterface VolumeSliderProps {\n /** Volume value from 0 to 1 */\n value: number;\n /** Called when volume changes (debounced) */\n onChange: (value: number) => void;\n /** Disable the slider */\n disabled?: boolean;\n /** Additional CSS classes */\n className?: string;\n}\n\n/**\n * Format slider value as dB for tooltip display\n */\nfunction formatDb(value: number): string {\n const db = sliderToDb(value);\n if (db <= -60) return '-∞ dB';\n const sign = db >= 0 ? '+' : '';\n return `${sign}${db.toFixed(1)} dB`;\n}\n\n/**\n * Debounce helper for volume changes\n */\nfunction useDebouncedCallback<T extends (...args: never[]) => void>(\n callback: T,\n delay: number\n): T {\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n\n // Update callback ref when callback changes\n useEffect(() => {\n callbackRef.current = callback;\n }, [callback]);\n\n const debouncedCallback = useCallback(\n (...args: Parameters<T>) => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n timeoutRef.current = setTimeout(() => {\n callbackRef.current(...args);\n }, delay);\n },\n [delay]\n ) as T;\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return debouncedCallback;\n}\n\nexport const VolumeSlider: React.FC<VolumeSliderProps> = ({\n value,\n onChange,\n disabled = false,\n className = '',\n}) => {\n // Local state for immediate visual feedback\n const [localValue, setLocalValue] = useState(value);\n const [isDragging, setIsDragging] = useState(false);\n\n // Sync local value with prop when not dragging\n useEffect(() => {\n if (!isDragging) {\n setLocalValue(value);\n }\n }, [value, isDragging]);\n\n // Debounced onChange to prevent IPC spam\n const debouncedOnChange = useDebouncedCallback(onChange, 50);\n\n const handleChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const newValue = parseFloat(e.target.value);\n setLocalValue(newValue);\n debouncedOnChange(newValue);\n },\n [debouncedOnChange]\n );\n\n const handleMouseDown = useCallback(() => {\n setIsDragging(true);\n }, []);\n\n const handleMouseUp = useCallback(() => {\n setIsDragging(false);\n // Send final value immediately on release\n onChange(localValue);\n }, [localValue, onChange]);\n\n return (\n <div\n className={`flex items-center ${className}`}\n title={`Volume: ${formatDb(localValue)}`}\n >\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={localValue}\n onChange={handleChange}\n onMouseDown={handleMouseDown}\n onMouseUp={handleMouseUp}\n onTouchStart={handleMouseDown}\n onTouchEnd={handleMouseUp}\n disabled={disabled}\n className={`\n w-full h-1.5 rounded-full appearance-none cursor-pointer\n bg-gray-700\n disabled:opacity-50 disabled:cursor-not-allowed\n [&::-webkit-slider-thumb]:appearance-none\n [&::-webkit-slider-thumb]:w-3\n [&::-webkit-slider-thumb]:h-3\n [&::-webkit-slider-thumb]:rounded-full\n [&::-webkit-slider-thumb]:bg-sas-accent\n [&::-webkit-slider-thumb]:cursor-pointer\n [&::-webkit-slider-thumb]:transition-transform\n [&::-webkit-slider-thumb]:hover:scale-110\n [&::-moz-range-thumb]:w-3\n [&::-moz-range-thumb]:h-3\n [&::-moz-range-thumb]:rounded-full\n [&::-moz-range-thumb]:bg-sas-accent\n [&::-moz-range-thumb]:border-0\n [&::-moz-range-thumb]:cursor-pointer\n `}\n />\n </div>\n );\n};\n\nexport default VolumeSlider;\n","/**\n * Volume Conversion Utilities\n *\n * Converts between UI slider position (0-1) and engine dB values using a power\n * curve with +6 dB headroom. The curve places unity gain (0 dB) at slider 0.75,\n * giving the top 25% of the slider a meaningful 6 dB boost range instead of the\n * previous perceptual dead zone.\n *\n * Mapping:\n * slider 0.00 → -60 dB (silence)\n * slider 0.75 → 0 dB (unity gain)\n * slider 1.00 → +6 dB (max boost)\n */\n\n/** Slider position that maps to 0 dB (unity gain) */\nexport const SLIDER_UNITY = 0.75;\n\n/** Maximum dB value at slider = 1.0 */\nexport const DB_MAX = 6;\n\n/** Minimum dB value (silence floor) */\nexport const DB_MIN = -60;\n\n/**\n * Exponent derived so that slider=1.0 yields exactly DB_MAX dB.\n *\n * gain_at_1 = (1 / SLIDER_UNITY) ^ EXPONENT = 10^(DB_MAX/20)\n * EXPONENT = log(10^(DB_MAX/20)) / log(1/SLIDER_UNITY)\n */\nconst EXPONENT: number =\n Math.log(Math.pow(10, DB_MAX / 20)) / Math.log(1 / SLIDER_UNITY);\n\n/**\n * Convert a UI slider position (0-1) to engine dB.\n *\n * @param slider - Slider value in [0, 1]\n * @returns dB value in [DB_MIN, DB_MAX]\n */\nexport function sliderToDb(slider: number): number {\n if (slider <= 0) return DB_MIN;\n const gain = Math.pow(slider / SLIDER_UNITY, EXPONENT);\n const db = 20 * Math.log10(gain);\n return Math.max(DB_MIN, Math.min(DB_MAX, db));\n}\n\n/**\n * Convert an engine dB value back to a UI slider position (0-1).\n * Inverse of sliderToDb().\n *\n * @param db - Volume in dB\n * @returns Slider value in [0, 1]\n */\nexport function dbToSlider(db: number): number {\n if (db <= DB_MIN) return 0;\n if (db >= DB_MAX) return 1;\n const gain = Math.pow(10, db / 20);\n const slider = SLIDER_UNITY * Math.pow(gain, 1 / EXPONENT);\n return Math.min(1, Math.max(0, slider));\n}\n","/**\n * PanSlider Component\n *\n * Compact horizontal pan slider for track stereo positioning.\n * Range: -1 (left) to +1 (right), 0 = center.\n * No text label - tooltip only.\n */\n\nimport React, { useCallback, useState, useRef, useEffect } from 'react';\n\ninterface PanSliderProps {\n /** Pan value from -1 (left) to 1 (right), 0 = center */\n value: number;\n /** Called when pan changes (debounced) */\n onChange: (value: number) => void;\n /** Disable the slider */\n disabled?: boolean;\n /** Additional CSS classes */\n className?: string;\n}\n\n/**\n * Convert pan value (-1 to 1) to display string\n */\nfunction toPanDisplay(value: number): string {\n if (Math.abs(value) < 0.02) {\n return 'Center';\n }\n const percent = Math.abs(Math.round(value * 100));\n return value < 0 ? `L${percent}` : `R${percent}`;\n}\n\n/**\n * Debounce helper for pan changes\n */\nfunction useDebouncedCallback<T extends (...args: never[]) => void>(\n callback: T,\n delay: number\n): T {\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n\n useEffect(() => {\n callbackRef.current = callback;\n }, [callback]);\n\n const debouncedCallback = useCallback(\n (...args: Parameters<T>) => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n timeoutRef.current = setTimeout(() => {\n callbackRef.current(...args);\n }, delay);\n },\n [delay]\n ) as T;\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return debouncedCallback;\n}\n\nexport const PanSlider: React.FC<PanSliderProps> = ({\n value,\n onChange,\n disabled = false,\n className = '',\n}) => {\n // Local state for immediate visual feedback\n const [localValue, setLocalValue] = useState(value);\n const [isDragging, setIsDragging] = useState(false);\n\n // Sync local value with prop when not dragging\n useEffect(() => {\n if (!isDragging) {\n setLocalValue(value);\n }\n }, [value, isDragging]);\n\n // Debounced onChange to prevent IPC spam\n const debouncedOnChange = useDebouncedCallback(onChange, 50);\n\n const handleChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const newValue = parseFloat(e.target.value);\n setLocalValue(newValue);\n debouncedOnChange(newValue);\n },\n [debouncedOnChange]\n );\n\n const handleMouseDown = useCallback(() => {\n setIsDragging(true);\n }, []);\n\n const handleMouseUp = useCallback(() => {\n setIsDragging(false);\n // Send final value immediately on release\n onChange(localValue);\n }, [localValue, onChange]);\n\n // Double-click to reset to center\n const handleDoubleClick = useCallback(() => {\n setLocalValue(0);\n onChange(0);\n }, [onChange]);\n\n return (\n <div\n className={`flex items-center ${className}`}\n title={`Pan: ${toPanDisplay(localValue)}`}\n >\n <input\n type=\"range\"\n min=\"-1\"\n max=\"1\"\n step=\"0.01\"\n value={localValue}\n onChange={handleChange}\n onMouseDown={handleMouseDown}\n onMouseUp={handleMouseUp}\n onTouchStart={handleMouseDown}\n onTouchEnd={handleMouseUp}\n onDoubleClick={handleDoubleClick}\n disabled={disabled}\n className={`\n w-full h-1.5 rounded-full appearance-none cursor-pointer\n bg-gray-700\n disabled:opacity-50 disabled:cursor-not-allowed\n [&::-webkit-slider-thumb]:appearance-none\n [&::-webkit-slider-thumb]:w-3\n [&::-webkit-slider-thumb]:h-3\n [&::-webkit-slider-thumb]:rounded-full\n [&::-webkit-slider-thumb]:bg-sas-accent\n [&::-webkit-slider-thumb]:cursor-pointer\n [&::-webkit-slider-thumb]:transition-transform\n [&::-webkit-slider-thumb]:hover:scale-110\n [&::-moz-range-thumb]:w-3\n [&::-moz-range-thumb]:h-3\n [&::-moz-range-thumb]:rounded-full\n [&::-moz-range-thumb]:bg-sas-accent\n [&::-moz-range-thumb]:border-0\n [&::-moz-range-thumb]:cursor-pointer\n `}\n />\n </div>\n );\n};\n\nexport default PanSlider;\n","/**\n * SorceryProgressBar Component\n *\n * A progress bar for long, uncertain wait times (10-30s). Supports two modes:\n *\n * 1. **Time-based mode** (when `estimatedDurationMs` is provided):\n * Uses elapsed time and an ease-out curve to pace progress realistically.\n * Reaches ~90% at the estimated completion time, then asymptotically\n * approaches 95% if the operation runs long.\n *\n * 2. **Phase-based mode** (legacy fallback, no `estimatedDurationMs`):\n * \"Zeno's Paradox\" style - progress moves quickly at first, then\n * asymptotically slows toward 95%.\n *\n * Visual style: Segmented \"retro CLI\" look with glowing teal accent,\n * diagonal stripes, and subtle pulse animation.\n */\n\nimport React, { useState, useEffect, useRef } from 'react';\n\n/**\n * Props for SorceryProgressBar component\n */\ninterface SorceryProgressBarProps {\n /** Whether loading is in progress */\n isLoading: boolean;\n /** Text shown during loading (default: \"CONJURING...\") */\n statusText?: string;\n /** Text shown on completion (default: \"COMPLETE\") */\n completeText?: string;\n /** Callback when loading completes */\n onComplete?: () => void;\n /** Height class override (default: \"h-10\") */\n heightClass?: string;\n /** Initial progress value (0-100) to resume from - persists across scene switches */\n initialProgress?: number;\n /** Callback when progress changes - use to persist progress in parent state */\n onProgressChange?: (progress: number) => void;\n /** Estimated total duration in ms - enables time-aware pacing */\n estimatedDurationMs?: number;\n}\n\n/**\n * Calculates target progress based on elapsed time and estimated duration.\n * Uses an ease-out power curve for natural-feeling progress:\n * - At 10% of estimated time: ~21% (feels responsive early)\n * - At 30% of estimated time: ~53% (good midpoint feel)\n * - At 50% of estimated time: ~74% (past halfway visually)\n * - At 80% of estimated time: ~88% (approaching completion)\n * - At 100% of estimated time: 90% (leaves room for overshoot)\n * - Beyond estimate: asymptotically approaches 95%\n */\nexport function calculateTimeBasedTarget(elapsedMs: number, estimatedDurationMs: number): number {\n const t = elapsedMs / estimatedDurationMs;\n if (t <= 0) return 0;\n\n if (t <= 1.0) {\n // Ease-out power curve reaching 90% at t=1.0\n return 90 * (1 - Math.pow(1 - t, 2.5));\n }\n\n // Beyond estimate: asymptotically approach 95%\n const overshootRatio = (elapsedMs - estimatedDurationMs) / estimatedDurationMs;\n return 90 + 5 * (1 - Math.exp(-overshootRatio * 3));\n}\n\n/**\n * Calculates the next progress value using \"Zeno's Paradox\" algorithm (legacy fallback).\n * - Phase 1 (0-20%): Rapid progress (5-15% per tick)\n * - Phase 2 (20-60%): Steady progress (2-7% per tick)\n * - Phase 3 (60-95%): Asymptotic slowdown\n * - Caps at 95% until actual completion\n */\nfunction calculateNextProgress(currentProgress: number): number {\n if (currentProgress < 20) {\n return currentProgress + Math.random() * 10 + 5;\n }\n if (currentProgress < 60) {\n return currentProgress + Math.random() * 5 + 2;\n }\n if (currentProgress < 95) {\n const remaining = 95 - currentProgress;\n const increment = remaining * (Math.random() * 0.2 + 0.1);\n return currentProgress + Math.max(increment, 0.1);\n }\n return 95;\n}\n\n/**\n * Calculates the next tick interval for phase-based mode (legacy fallback).\n */\nfunction calculateNextTickInterval(progress: number): number {\n if (progress < 30) {\n return Math.random() * 200 + 150; // 150-350ms\n }\n if (progress < 70) {\n return Math.random() * 300 + 200; // 200-500ms\n }\n return Math.random() * 600 + 400; // 400-1000ms\n}\n\n/** Tick interval for time-based mode (ms) */\nconst TIME_BASED_TICK_MIN = 200;\nconst TIME_BASED_TICK_RANGE = 100;\n\n/**\n * SorceryProgressBar - A mystical progress bar for uncertain wait times\n */\nexport function SorceryProgressBar({\n isLoading,\n statusText = 'CONJURING...',\n completeText = 'COMPLETE',\n onComplete,\n heightClass = 'h-10',\n initialProgress = 0,\n onProgressChange,\n estimatedDurationMs,\n}: SorceryProgressBarProps): React.ReactElement | null {\n const [progress, setProgress] = useState<number>(initialProgress);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n // Initialize to false so first render with isLoading=true triggers animation start\n const isLoadingRef = useRef<boolean>(false);\n const hasStartedRef = useRef<boolean>(false);\n const startTimeRef = useRef<number>(0);\n\n // Store callbacks in refs to avoid dependency issues\n const onProgressChangeRef = useRef(onProgressChange);\n const onCompleteRef = useRef(onComplete);\n onProgressChangeRef.current = onProgressChange;\n onCompleteRef.current = onComplete;\n\n // Store props in refs - only used when loading starts, not as dependencies\n const initialProgressRef = useRef(initialProgress);\n initialProgressRef.current = initialProgress;\n const estimatedDurationMsRef = useRef(estimatedDurationMs);\n estimatedDurationMsRef.current = estimatedDurationMs;\n\n // Effect to handle loading state changes - ONLY depends on isLoading\n useEffect(() => {\n const wasLoading = isLoadingRef.current;\n isLoadingRef.current = isLoading;\n\n if (isLoading && !wasLoading) {\n // Loading just started\n hasStartedRef.current = true;\n startTimeRef.current = Date.now();\n\n // Start fresh or resume from initial progress (read from ref)\n const startProgress = initialProgressRef.current > 0 ? initialProgressRef.current : 0;\n setProgress(startProgress);\n\n const duration = estimatedDurationMsRef.current;\n\n if (duration && duration > 0) {\n // Time-based mode: pace progress using elapsed time\n const tick = (): void => {\n setProgress((prev) => {\n const elapsed = Date.now() - startTimeRef.current;\n const target = calculateTimeBasedTarget(elapsed, duration);\n\n // Add subtle jitter for organic feel (±0.5%)\n const jitter = (Math.random() - 0.5) * 1.0;\n // Move toward target, ensure monotonically increasing, cap at 95%\n const next = Math.min(Math.max(target + jitter, prev + 0.05), 95);\n\n onProgressChangeRef.current?.(next);\n timerRef.current = setTimeout(tick, TIME_BASED_TICK_MIN + Math.random() * TIME_BASED_TICK_RANGE);\n return next;\n });\n };\n\n timerRef.current = setTimeout(tick, TIME_BASED_TICK_MIN);\n } else {\n // Phase-based mode (legacy fallback)\n const tick = (): void => {\n setProgress((prev) => {\n if (prev >= 95) {\n timerRef.current = setTimeout(tick, 1000);\n return 95;\n }\n\n const next = Math.min(calculateNextProgress(prev), 95);\n onProgressChangeRef.current?.(next);\n\n const interval = calculateNextTickInterval(next);\n timerRef.current = setTimeout(tick, interval);\n\n return next;\n });\n };\n\n const firstInterval = calculateNextTickInterval(startProgress);\n timerRef.current = setTimeout(tick, firstInterval);\n }\n } else if (!isLoading && wasLoading && hasStartedRef.current) {\n // Loading just finished - jump to 100%\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n setProgress(100);\n onProgressChangeRef.current?.(100);\n onCompleteRef.current?.();\n hasStartedRef.current = false;\n }\n\n // Cleanup on unmount only\n return () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n // ONLY depend on isLoading - other props are read from refs\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isLoading]);\n\n // Don't render if not loading and progress is 0\n if (!isLoading && progress === 0) {\n return null;\n }\n\n const displayProgress = Math.floor(progress);\n const isComplete = !isLoading && progress === 100;\n\n // Calculate transition duration based on progress phase\n const transitionDuration = progress < 50 ? '300ms' : progress < 80 ? '500ms' : '700ms';\n\n return (\n <div\n className={`relative w-full ${heightClass} bg-sas-panel-alt border border-sas-border rounded-sm overflow-hidden shadow-inner`}\n >\n {/* Progress fill with stripes and glow */}\n <div\n className={`\n h-full\n bg-gradient-to-r from-sas-accent/70 to-sas-accent\n shadow-glow-soft\n sorcery-progress-fill\n animate-progress-stripes\n ${progress > 70 ? 'animate-progress-pulse' : ''}\n transition-all ease-out\n `}\n style={{\n width: `${progress}%`,\n transitionDuration,\n }}\n />\n\n {/* Text overlay */}\n <div className=\"absolute inset-0 flex items-center justify-center\">\n {isLoading && progress < 100 ? (\n <span className=\"font-mono text-xs text-sas-accent font-bold drop-shadow-md tracking-wider\">\n {statusText} {displayProgress}%\n </span>\n ) : isComplete ? (\n <span className=\"font-mono text-xs text-sas-text font-bold drop-shadow-md tracking-wider\">\n {completeText}\n </span>\n ) : null}\n </div>\n\n {/* Scanline overlay for retro CRT effect */}\n <div\n className=\"absolute inset-0 pointer-events-none opacity-10\"\n style={{\n backgroundImage: `repeating-linear-gradient(\n to bottom,\n transparent,\n transparent 2px,\n rgba(0, 0, 0, 0.3) 2px,\n rgba(0, 0, 0, 0.3) 4px\n )`,\n }}\n />\n </div>\n );\n}\n\nexport default SorceryProgressBar;\n","/**\n * CrossfadeTrackRow — a transition \"crossfade track\": two stacked TrackRows\n * (origin on top, target on bottom) joined by a horizontal crossfade slider.\n *\n * Both layers play the SAME generated MIDI; the top wears the ORIGIN scene\n * track's preset and the bottom wears the TARGET scene track's preset. The user\n * cannot regenerate, shuffle, or change the preset/sample on either layer —\n * those controls are simply not wired into the inner TrackRows (the SDK\n * TrackRow is \"controlled by omission\"). What remains: per-layer volume/pan,\n * GROUP mute/solo (both layers toggle together), and a single delete that\n * removes the whole pair.\n *\n * The slider represents WHERE the crossfade happens. In this phase it is\n * centered and non-functional (omit `onSliderChange` → it renders disabled); a\n * later phase wires it to fade origin→target across the bars.\n *\n * @since SDK 2.22.0\n */\nimport React from 'react';\nimport { TrackRow } from './TrackRow';\nimport { ConfirmDialog } from './ConfirmDialog';\nimport { EMPTY_FX_DETAIL_STATE } from '../types/fx-toggle.types';\nimport type { TrackLevelsHandle } from '../hooks/useTrackLevels';\nimport type { CrossfadeSlot } from '../crossfade-meta';\n\n/** One layer (engine track) of a crossfade pair. */\nexport interface CrossfadeLayer {\n /** Engine track id of this layer's track (also the meter key). */\n trackId: string;\n /** Display name of this layer's (newly created) track. */\n name: string;\n /** Musical role (same for both layers — crossfades are same-role). */\n role?: string;\n /** Name of the SOURCE track this layer was cloned from (origin/target scene). */\n sourceName?: string;\n /** Human label of the copied preset/sound, shown in the caption. */\n soundLabel?: string;\n /** Playback state for this layer. */\n runtimeState: { muted: boolean; solo: boolean; volume: number; pan: number };\n}\n\nexport interface CrossfadeTrackRowProps {\n /** Top layer — wears the origin (from) scene track's preset. */\n origin: CrossfadeLayer;\n /** Bottom layer — wears the target (to) scene track's preset. */\n target: CrossfadeLayer;\n /** Crossfade position 0..1 (0 = all origin, 1 = all target). Defaults centered. */\n sliderPos?: number;\n /** Toggle mute on BOTH layers together (group mute). */\n onMuteToggle: () => void;\n /** Toggle solo on BOTH layers together (group solo). */\n onSoloToggle: () => void;\n /** Change one layer's volume (per-layer). */\n onVolumeChange: (slot: CrossfadeSlot, volume: number) => void;\n /** Change one layer's pan (per-layer). */\n onPanChange: (slot: CrossfadeSlot, pan: number) => void;\n /** Delete the whole pair. */\n onDelete: () => void;\n /** Move the crossfade point. Omit to render the slider read-only (phase 1). */\n onSliderChange?: (pos: number) => void;\n /** Shared meter handle (welds a peak meter to each layer). */\n levels?: TrackLevelsHandle;\n /** Left-border accent. Defaults to transition purple. */\n accentColor?: string;\n}\n\nfunction LayerCaption({ tag, layer }: { tag: string; layer: CrossfadeLayer }): React.ReactElement {\n return (\n <div className=\"flex items-center gap-1.5 min-w-0 px-2 py-0.5\">\n <span className=\"text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0\">{tag}</span>\n <span className=\"text-[11px] text-sas-text truncate\" title={layer.sourceName ?? layer.name}>\n {layer.sourceName ?? layer.name}\n </span>\n {layer.soundLabel && (\n <span className=\"text-[9px] text-sas-muted/60 truncate flex-shrink-0\" title={layer.soundLabel}>\n · {layer.soundLabel}\n </span>\n )}\n </div>\n );\n}\n\nexport function CrossfadeTrackRow({\n origin,\n target,\n sliderPos = 0.5,\n onMuteToggle,\n onSoloToggle,\n onVolumeChange,\n onPanChange,\n onDelete,\n onSliderChange,\n levels,\n accentColor = '#9333EA',\n}: CrossfadeTrackRowProps): React.ReactElement {\n const [confirmDelete, setConfirmDelete] = React.useState(false);\n\n // A locked crossfade layer. The inner track's `name` is suppressed (the\n // meaningful name lives in the caption); every sound/generation handler is\n // omitted so shuffle / Create / preset-pick / FX / drawer / delete never\n // render. Mute/solo are GROUP-wired (same handler on both layers); volume/pan\n // are per-layer.\n const renderLayer = (layer: CrossfadeLayer, slot: CrossfadeSlot, tag: string): React.ReactElement => (\n <TrackRow\n track={{ id: layer.trackId, name: '', role: layer.role }}\n runtimeState={layer.runtimeState}\n fxDetailState={EMPTY_FX_DETAIL_STATE}\n drawerOpen={false}\n drawerTab=\"fx\"\n levels={levels}\n accentColor={accentColor}\n contentSlot={<LayerCaption tag={tag} layer={layer} />}\n onMuteToggle={onMuteToggle}\n onSoloToggle={onSoloToggle}\n onVolumeChange={(v: number) => onVolumeChange(slot, v)}\n onPanChange={(p: number) => onPanChange(slot, p)}\n />\n );\n\n return (\n <div\n data-testid=\"crossfade-track-row\"\n className=\"w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden\"\n style={{ borderLeftColor: accentColor, borderLeftWidth: '3px' }}\n >\n {/* Header — crossfade label + single delete for the whole pair. */}\n <div className=\"flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60\">\n <span className=\"text-[10px] font-bold uppercase tracking-wide\" style={{ color: accentColor }}>\n ⇄ Crossfade\n </span>\n <button\n data-testid=\"crossfade-delete-button\"\n onClick={() => setConfirmDelete(true)}\n className=\"text-sas-danger/70 hover:text-sas-danger px-1 transition-colors text-sm\"\n title=\"Delete crossfade pair\"\n aria-label=\"Delete crossfade pair\"\n >\n x\n </button>\n </div>\n\n {renderLayer(origin, 'origin', 'Origin')}\n\n {/* Crossfade slider — represents WHERE origin fades into target. Read-only\n (disabled) until the functional fader ships. */}\n <div className=\"flex items-center gap-2 px-3 py-1.5\" data-testid=\"crossfade-slider-row\">\n <span\n className=\"text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0\"\n title={origin.sourceName ?? origin.name}\n >\n {origin.sourceName ?? origin.name}\n </span>\n <input\n type=\"range\"\n data-testid=\"crossfade-slider\"\n min={0}\n max={1}\n step={0.01}\n value={sliderPos}\n disabled={!onSliderChange}\n onChange={\n onSliderChange\n ? (e: React.ChangeEvent<HTMLInputElement>) => onSliderChange(Number(e.target.value))\n : undefined\n }\n style={{ accentColor }}\n className=\"flex-1 disabled:opacity-60 disabled:cursor-not-allowed\"\n aria-label=\"Crossfade position\"\n />\n <span\n className=\"text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0\"\n title={target.sourceName ?? target.name}\n >\n {target.sourceName ?? target.name}\n </span>\n </div>\n\n {renderLayer(target, 'target', 'Target')}\n\n <ConfirmDialog\n open={confirmDelete}\n title=\"Delete crossfade?\"\n message={\n <>\n This crossfade pair (both layers) will be permanently removed from this scene. This cannot\n be undone.\n </>\n }\n confirmLabel=\"Delete\"\n onConfirm={() => {\n setConfirmDelete(false);\n onDelete();\n }}\n onCancel={() => setConfirmDelete(false)}\n testIdPrefix=\"crossfade-delete-confirm\"\n />\n </div>\n );\n}\n\nexport default CrossfadeTrackRow;\n","/**\n * Crossfade-pair metadata — family-agnostic types + parsing shared by every\n * generator panel that supports transition crossfades (synth / drum / instrument).\n *\n * A crossfade pair is two normal tracks linked by a shared `groupId`, persisted\n * in scene plugin_data under `track:<dbId>:crossfade`. Both members play the\n * same MIDI; one wears the origin preset, the other the target preset. The panel\n * owns the family-specific create flow (how a preset/sample is copied) and the\n * render; this module owns only the shape + the scene-data → pairs parse so the\n * logic can't drift across the three panels.\n *\n * @since SDK 2.23.0\n */\n\n/** Which half of the pair a per-layer control / member targets. */\nexport type CrossfadeSlot = 'origin' | 'target';\n\n/**\n * Equal-power center gain (~-3 dB, 1/√2) applied to BOTH crossfade layers so a\n * centered, non-functional slider already sounds like a midpoint blend. The\n * per-layer volume sliders start here; a later phase's fader drives them.\n */\nexport const EQUAL_POWER_GAIN = 0.707;\n\n/**\n * Per-member crossfade metadata (one scene-data value per member track). The two\n * members (origin/target) of a pair share a `groupId`.\n */\nexport interface CrossfadeMeta {\n groupId: string;\n slot: CrossfadeSlot;\n /** DB id of the partner member track. */\n partnerDbId: string;\n /** DB id of the SOURCE track this layer's preset/sample was copied from. */\n sourceTrackDbId: string;\n /** DB id of the scene the source track lives in (the from/to scene). */\n sourceSceneId: string;\n /** Source track display name (shown in the caption). */\n sourceName: string;\n /** Copied preset/sample label (shown in the caption). */\n soundLabel: string;\n /** Crossfade position 0..1 (kept identical on both members). */\n sliderPos: number;\n}\n\n/** A complete crossfade pair (both members present), keyed by groupId. */\nexport interface CrossfadePairMeta {\n groupId: string;\n sliderPos: number;\n originDbId: string;\n targetDbId: string;\n /** DB id of the ORIGIN source track (in the from scene) — drives the \"used once\" exclusion. */\n originSourceDbId: string;\n /** DB id of the TARGET source track (in the to scene). */\n targetSourceDbId: string;\n originSourceName: string;\n originSoundLabel: string;\n targetSourceName: string;\n targetSoundLabel: string;\n}\n\n/** Narrow an unknown scene-data value to CrossfadeMeta (defensive — survives partial blobs). */\nexport function asCrossfadeMeta(val: unknown): CrossfadeMeta | null {\n if (!val || typeof val !== 'object') return null;\n const m = val as Partial<CrossfadeMeta>;\n if (typeof m.groupId !== 'string' || (m.slot !== 'origin' && m.slot !== 'target')) return null;\n if (typeof m.partnerDbId !== 'string') return null;\n return {\n groupId: m.groupId,\n slot: m.slot,\n partnerDbId: m.partnerDbId,\n sourceTrackDbId: typeof m.sourceTrackDbId === 'string' ? m.sourceTrackDbId : '',\n sourceSceneId: typeof m.sourceSceneId === 'string' ? m.sourceSceneId : '',\n sourceName: typeof m.sourceName === 'string' ? m.sourceName : '',\n soundLabel: typeof m.soundLabel === 'string' ? m.soundLabel : '',\n sliderPos: typeof m.sliderPos === 'number' ? m.sliderPos : 0.5,\n };\n}\n\n/**\n * Scan all `track:<dbId>:crossfade` keys in a scene's plugin_data and assemble\n * COMPLETE pairs (both origin + target present). A half-broken group (partner\n * deleted underneath) is omitted, so its surviving member falls back to a normal\n * row instead of vanishing.\n */\nexport function parseCrossfadePairs(sceneData: Record<string, unknown>): CrossfadePairMeta[] {\n const groups = new Map<\n string,\n { origin?: { dbId: string; meta: CrossfadeMeta }; target?: { dbId: string; meta: CrossfadeMeta } }\n >();\n for (const [key, val] of Object.entries(sceneData)) {\n const match = /^track:(.+):crossfade$/.exec(key);\n if (!match) continue;\n const meta = asCrossfadeMeta(val);\n if (!meta) continue;\n const dbId = match[1];\n const g = groups.get(meta.groupId) ?? {};\n if (meta.slot === 'origin') g.origin = { dbId, meta };\n else g.target = { dbId, meta };\n groups.set(meta.groupId, g);\n }\n const pairs: CrossfadePairMeta[] = [];\n for (const [groupId, g] of groups) {\n if (!g.origin || !g.target) continue;\n pairs.push({\n groupId,\n sliderPos: g.origin.meta.sliderPos,\n originDbId: g.origin.dbId,\n targetDbId: g.target.dbId,\n originSourceDbId: g.origin.meta.sourceTrackDbId,\n targetSourceDbId: g.target.meta.sourceTrackDbId,\n originSourceName: g.origin.meta.sourceName,\n originSoundLabel: g.origin.meta.soundLabel,\n targetSourceName: g.target.meta.sourceName,\n targetSoundLabel: g.target.meta.soundLabel,\n });\n }\n return pairs;\n}\n\n// ============================================================================\n// Crossfade volume automation (Phase 3 — the functional fader)\n// ============================================================================\n\n/** One volume-automation point: a dB value at a time offset (seconds from clip start). */\nexport interface VolumeAutomationPoint {\n time: number; // seconds\n db: number; // gain in dB (-80 ≈ silent, 0 = unity)\n}\n\n/** Origin + target volume curves for one crossfade pair. */\nexport interface CrossfadeVolumeCurves {\n origin: VolumeAutomationPoint[];\n target: VolumeAutomationPoint[];\n}\n\n// Exported so fade-meta.ts can reuse the same floor + gain→dB mapping (a fade is\n// a crossfade with one empty endpoint; its volume curve must match exactly).\nexport const FADE_FLOOR_DB = -80;\n\nexport function gainToDb(gain: number): number {\n return gain <= 1e-4 ? FADE_FLOOR_DB : Math.max(FADE_FLOOR_DB, 20 * Math.log10(gain));\n}\n\n/**\n * Equal-power crossfade volume curves over a transition of `bars` at `bpm`.\n * The ORIGIN layer fades OUT and the TARGET fades IN; `sliderPos` (0..1) sets\n * WHERE in time the equal-power (-3 dB) crossover sits — 0 = hand off near the\n * start, 1 = hold the origin until near the end. Points span the clip window\n * [0, durationSeconds] so the engine re-reads them each loop (re-fade per loop).\n * `steps`+1 points with linear interpolation approximate the cos/sin curve.\n *\n * Returns dB point arrays for `host.setTrackVolumeAutomation` — origin on the top\n * layer, target on the bottom. @since SDK 2.25.0\n */\nexport function buildCrossfadeVolumeCurves(\n bars: number,\n bpm: number,\n sliderPos: number,\n steps = 32,\n): CrossfadeVolumeCurves {\n const durationSeconds = (bars * 4 * 60) / Math.max(1, bpm);\n // Keep the crossover off the exact ends so there's always an actual fade.\n const s = Math.min(0.98, Math.max(0.02, sliderPos));\n const round = (n: number): number => Math.round(n * 1000) / 1000;\n const origin: VolumeAutomationPoint[] = [];\n const target: VolumeAutomationPoint[] = [];\n for (let i = 0; i <= steps; i++) {\n const x = i / steps; // normalized time 0..1\n const time = round(x * durationSeconds);\n // Piecewise-linear angle so the equal-power crossover (π/4) lands at x = s.\n const theta = x <= s ? (x / s) * (Math.PI / 4) : Math.PI / 4 + ((x - s) / (1 - s)) * (Math.PI / 4);\n origin.push({ time, db: Math.round(gainToDb(Math.cos(theta)) * 100) / 100 });\n target.push({ time, db: Math.round(gainToDb(Math.sin(theta)) * 100) / 100 });\n }\n return { origin, target };\n}\n","/**\n * Crossfade MIDI inpainting — builds the LLM user-prompt for a bridge that\n * MORPHS the ORIGIN part into the TARGET part.\n *\n * A normal scene generation composes a part standalone from the scene's chords.\n * A crossfade bridge is different: it is INPAINTING between two fixed endpoints.\n * The generated part must begin feeling continuous with the origin pattern and\n * end feeling continuous with the target pattern, transforming between them\n * across the transition's bars.\n *\n * The harmonic frame — Key / mode / BPM / bars / the transition chord\n * progression (with beat timing) / scene contract — is injected AUTOMATICALLY by\n * `host.generateWithLLM` (it prepends the active scene's \"Musical Context\" block\n * unless `skipContextPrefix` is set). So this prompt does NOT restate key/bpm/\n * chords — it adds only the two endpoint patterns + the morph instructions, and\n * references the harmonic frame as \"given above\".\n *\n * REPRESENTATION (researched for Gemini): ABC notation is the LLM-native format\n * for melodic generation, but it's weak for percussion, would need a separate\n * output parser (our output is JSON note-events, already proven with Gemini),\n * and an inpainting task wants input/output FORMAT SYMMETRY. So each endpoint is\n * given as the exact JSON note-events PLUS a pitch-named, bar-structured \"gloss\"\n * — the transferable wins from the research (pitch NAMES over raw MIDI numbers,\n * explicit bar/beat structure) layered on the precise, symmetric JSON. Drums\n * (uniform pitch) get a rhythmic gloss instead of pitch names.\n *\n * This changes only the LLM INPUT framing: the OUTPUT schema is unchanged, so the\n * calling panel keeps its system prompt + parser (and, for drums, its flatten step).\n *\n * @since SDK 2.24.0\n */\nimport type { PluginMidiNote } from './types/plugin-sdk.types';\n\nexport interface CrossfadeInpaintInput {\n /** Musical role of the bridge part (e.g. 'bass'). '' falls back to \"melodic\". */\n role: string;\n /** Transition length in bars (the morph timeline). */\n bars: number;\n /** Display name of the ORIGIN source track (the part the bridge begins from). */\n originName: string;\n /** Display name of the TARGET source track (the part the bridge arrives at). */\n targetName: string;\n /** ORIGIN source scene's key label (e.g. \"G minor\"). Null/omitted = unknown. */\n originKey?: string | null;\n /** TARGET source scene's key label. Null/omitted = unknown. */\n targetKey?: string | null;\n /** ORIGIN pattern notes (beat-based; from the FROM scene). May be empty. */\n originNotes: readonly PluginMidiNote[];\n /** TARGET pattern notes (beat-based; from the TO scene). May be empty. */\n targetNotes: readonly PluginMidiNote[];\n /** Drums: pitch is uniform (flattened), so gloss RHYTHM instead of pitch names. */\n percussive?: boolean;\n}\n\nconst PITCH_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];\n\n/** Round to 3 dp, dropping trailing-zero noise. */\nfunction round3(n: number): number {\n return Math.round(n * 1000) / 1000;\n}\n\n/** MIDI number → scientific pitch name (60 → C4), the app's octave convention. */\nfunction pitchName(p: number): string {\n return `${PITCH_NAMES[((p % 12) + 12) % 12]}${Math.floor(p / 12) - 1}`;\n}\n\n/** Compact a note to the 4 fields the LLM needs (drops channel), beats rounded. */\nfunction compactNote(n: PluginMidiNote): { pitch: number; startBeat: number; durationBeats: number; velocity: number } {\n return { pitch: n.pitch, startBeat: round3(n.startBeat), durationBeats: round3(n.durationBeats), velocity: n.velocity };\n}\n\n/** One-line shape summary so the LLM grasps register/density before the detail. */\nfunction summarize(notes: readonly PluginMidiNote[], percussive: boolean): string {\n if (notes.length === 0) return 'empty (no notes)';\n const span = round3(Math.max(...notes.map((n) => n.startBeat + n.durationBeats)));\n if (percussive) return `${notes.length} hits, spans ~${span} beats`;\n const pitches = notes.map((n) => n.pitch);\n return `${notes.length} notes, ${pitchName(Math.min(...pitches))}–${pitchName(Math.max(...pitches))}, spans ~${span} beats`;\n}\n\n/** Pitch-named (melodic) or rhythmic (drums) gloss, grouped by inferred bar. */\nfunction gloss(notes: readonly PluginMidiNote[], percussive: boolean): string {\n const sorted = [...notes].sort((a, b) => a.startBeat - b.startBeat);\n const maxEnd = Math.max(...sorted.map((n) => n.startBeat + n.durationBeats));\n const bars = Math.max(1, Math.ceil(maxEnd / 4));\n const lines: string[] = [];\n for (let b = 0; b < bars; b++) {\n const inBar = sorted.filter((n) => n.startBeat >= b * 4 && n.startBeat < (b + 1) * 4);\n if (inBar.length === 0) continue;\n const body = percussive\n ? inBar.map((n) => `${round3(n.startBeat)}(v${n.velocity})`).join(' ')\n : inBar.map((n) => `${pitchName(n.pitch)}@${round3(n.startBeat)}`).join(' ');\n lines.push(` Bar ${b + 1}: ${body}`);\n }\n return lines.join('\\n');\n}\n\nfunction patternBlock(\n label: string,\n name: string,\n key: string | null | undefined,\n notes: readonly PluginMidiNote[],\n percussive: boolean,\n): string {\n const keyLabel = key ? ` in ${key}` : '';\n const header = `${label} — \"${name}\"${keyLabel} (${summarize(notes, percussive)}):`;\n if (notes.length === 0) return `${header}\\n (no notes — treat this end as open)`;\n return `${header}\\n${gloss(notes, percussive)}\\n exact JSON: ${JSON.stringify(notes.map(compactNote))}`;\n}\n\n/**\n * Build the inpainting user-prompt. The result is the prompt BODY only — pass it\n * as `request.user` to `host.generateWithLLM` with the panel's normal system\n * prompt and `responseFormat: 'json'`; the harmonic context auto-prefixes.\n */\nexport function buildCrossfadeInpaintPrompt(input: CrossfadeInpaintInput): string {\n const { role, bars, originName, targetName, originKey, targetKey, originNotes, targetNotes } = input;\n const percussive = input.percussive ?? false;\n const part = role || (percussive ? 'drum' : 'melodic');\n const modulation =\n originKey && targetKey\n ? originKey === targetKey\n ? `stays in ${targetKey}`\n : `modulates from ${originKey} toward ${targetKey}`\n : 'resolves toward the destination key';\n\n const lines: string[] = [\n `TASK — TRANSITION BRIDGE (musical inpainting).`,\n `Compose a ${part} part that MORPHS from the ORIGIN pattern into the TARGET pattern across the ${bars} bars`,\n `of this transition. The Key / BPM / chord progression are given above — it ${modulation}; honour that`,\n `frame, don't restate it. Each pattern below is shown as a pitch/rhythm gloss for musicality plus its exact`,\n `JSON; output your bridge in the same JSON note schema (per the system prompt).`,\n ``,\n patternBlock('ORIGIN pattern (where the bridge BEGINS)', originName, originKey, originNotes, percussive),\n ``,\n patternBlock('TARGET pattern (where the bridge must ARRIVE)', targetName, targetKey, targetNotes, percussive),\n ``,\n `Requirements:`,\n `- The FIRST bar feels continuous with the ORIGIN — borrow its register, rhythm, and contour so the seam`,\n ` from the previous scene is seamless.`,\n `- Across the middle bars, gradually transform toward the TARGET (shift register / rhythm / motifs step by step).`,\n `- The LAST bar lands on the TARGET's material and resolves onto the destination chord, so the seam into the`,\n ` next scene is seamless.`,\n `- Stay within the transition chord progression above; favour chord tones at the bar boundaries.`,\n `- This is inpainting between two FIXED endpoints — a listener should not be able to point to where the`,\n ` origin ends or the target begins.`,\n ];\n\n if (originNotes.length === 0 || targetNotes.length === 0) {\n lines.push(\n ``,\n originNotes.length === 0 && targetNotes.length === 0\n ? `(Both endpoints are empty — compose a short ${part} bridge from the chords alone.)`\n : originNotes.length === 0\n ? `(The ORIGIN is empty — begin sparse and grow INTO the TARGET.)`\n : `(The TARGET is empty — begin from the ORIGIN and dissolve toward the destination chord.)`,\n );\n }\n\n return lines.join('\\n');\n}\n","/**\n * Fade metadata — family-agnostic types + parsing for transition orphan fades\n * (synth / drum / instrument panels).\n *\n * A fade is a CROSSFADE WITH ONE EMPTY ENDPOINT: a single generated track that\n * either fades IN (a target-only track entering — `morph(∅ → target)`) or fades\n * OUT (an origin-only track leaving — `morph(origin → ∅)`) across the transition\n * loop. It reuses the same generation pipeline (`buildCrossfadeInpaintPrompt`\n * with one empty endpoint) and the same volume-automation fader as crossfade.\n *\n * Stored in scene plugin_data under `track:<dbId>:fade` (ONE entry per track —\n * unlike crossfade there is no partner / groupId). Kept as a SEPARATE type from\n * crossfade so the load-bearing \"drop a half-broken pair\" guard in\n * `parseCrossfadePairs` stays intact.\n *\n * @since SDK 2.28.0\n */\n\nimport { type VolumeAutomationPoint, FADE_FLOOR_DB, gainToDb } from './crossfade-meta';\n\n/** Which way the lone track fades over the transition. */\nexport type FadeDirection = 'in' | 'out';\n\n/**\n * How the fade is shaped:\n * - `volume` — a one-sided level ramp does the work (DJ-style). Best for\n * textural/sustained material (pads, atmospheres).\n * - `build` — the MIDI carries the fade (the inpaint grows sparse→full on the way\n * in, or dissolves on the way out); the level stays flat. Best for articulated\n * material (lead, bass, drums, winds, vocals).\n */\nexport type FadeGesture = 'volume' | 'build';\n\n/** Per-track fade metadata (one scene-data value per fade track). */\nexport interface FadeMeta {\n direction: FadeDirection;\n gesture: FadeGesture;\n /** DB id of the SOURCE track this fade's preset/sample + pattern was seeded from. */\n sourceTrackDbId: string;\n /** DB id of the scene the source track lives in (the from/to scene). */\n sourceSceneId: string;\n /** Source track display name (shown in the caption). */\n sourceName: string;\n /** Copied preset/sample label (shown in the caption). */\n soundLabel: string;\n /** Fade position 0..1 — WHERE in time the fade midpoint sits. */\n sliderPos: number;\n}\n\n/** A fade entry resolved from scene data: the fade track's dbId + its metadata. */\nexport interface FadeEntry {\n dbId: string;\n meta: FadeMeta;\n}\n\n/** Narrow an unknown scene-data value to FadeMeta (defensive — survives partial blobs). */\nexport function asFadeMeta(val: unknown): FadeMeta | null {\n if (!val || typeof val !== 'object') return null;\n const m = val as Partial<FadeMeta>;\n if (m.direction !== 'in' && m.direction !== 'out') return null;\n if (m.gesture !== 'volume' && m.gesture !== 'build') return null;\n return {\n direction: m.direction,\n gesture: m.gesture,\n sourceTrackDbId: typeof m.sourceTrackDbId === 'string' ? m.sourceTrackDbId : '',\n sourceSceneId: typeof m.sourceSceneId === 'string' ? m.sourceSceneId : '',\n sourceName: typeof m.sourceName === 'string' ? m.sourceName : '',\n soundLabel: typeof m.soundLabel === 'string' ? m.soundLabel : '',\n sliderPos: typeof m.sliderPos === 'number' ? m.sliderPos : 0.5,\n };\n}\n\n/**\n * Scan all `track:<dbId>:fade` keys in a scene's plugin_data and return one entry\n * per valid fade. Unlike crossfade there is no grouping or both-present gate — a\n * fade is intrinsically a single track.\n */\nexport function parseFades(sceneData: Record<string, unknown>): FadeEntry[] {\n const out: FadeEntry[] = [];\n for (const [key, val] of Object.entries(sceneData)) {\n const match = /^track:(.+):fade$/.exec(key);\n if (!match) continue;\n const meta = asFadeMeta(val);\n if (!meta) continue;\n out.push({ dbId: match[1], meta });\n }\n return out;\n}\n\n/**\n * Build a ONE-sided volume-automation curve for a fade over `bars` at `bpm`.\n *\n * - `gesture === 'build'` → flat at unity (0 dB). The compositional build in the\n * MIDI carries the fade; layering a volume ramp on top would double-fade.\n * - `gesture === 'volume'` → an equal-power ramp identical to ONE half of\n * `buildCrossfadeVolumeCurves`: fade-out ≡ its `origin` curve (unity→floor),\n * fade-in ≡ its `target` curve (floor→unity). `sliderPos` sets WHERE the −3 dB\n * midpoint sits in time.\n *\n * Points span [0, durationSeconds] so the engine re-reads them each loop. Returns\n * dB points for `host.setTrackVolumeAutomation`.\n *\n * @since SDK 2.28.0\n */\nexport function buildFadeVolumeCurve(\n bars: number,\n bpm: number,\n direction: FadeDirection,\n sliderPos: number,\n gesture: FadeGesture,\n steps = 32,\n): VolumeAutomationPoint[] {\n const durationSeconds = (bars * 4 * 60) / Math.max(1, bpm);\n\n // build: the notes do the fade — hold the level flat at unity.\n if (gesture === 'build') {\n return [\n { time: 0, db: 0 },\n { time: Math.round(durationSeconds * 1000) / 1000, db: 0 },\n ];\n }\n\n // volume: one half of the equal-power crossfade curve.\n const s = Math.min(0.98, Math.max(0.02, sliderPos));\n const round = (n: number): number => Math.round(n * 1000) / 1000;\n const points: VolumeAutomationPoint[] = [];\n for (let i = 0; i <= steps; i++) {\n const x = i / steps; // normalized time 0..1\n const time = round(x * durationSeconds);\n // Piecewise-linear angle so the equal-power midpoint (π/4) lands at x = s.\n const theta = x <= s ? (x / s) * (Math.PI / 4) : Math.PI / 4 + ((x - s) / (1 - s)) * (Math.PI / 4);\n // fade-out follows cos (unity→floor); fade-in follows sin (floor→unity).\n const gain = direction === 'out' ? Math.cos(theta) : Math.sin(theta);\n points.push({ time, db: Math.round(gainToDb(gain) * 100) / 100 });\n }\n return points;\n}\n\n/**\n * Roles whose fades default to a `volume` (level) ramp — sustained/textural\n * material that enters/leaves by level in real productions. Everything else\n * defaults to `build` (the notes carry the fade).\n *\n * This is a UI default heuristic over role tokens, NOT the canonical role list\n * (that lives in the app's instrument-classification + `host.getValidRoles()`).\n * There is no textural↔articulated axis in the taxonomy, so this small curated\n * subset lives next to its consumer (the fade modal/panels).\n *\n * @since SDK 2.28.0\n */\nexport const TEXTURAL_ROLES: ReadonlySet<string> = new Set<string>([\n 'pads',\n 'pad',\n 'strings',\n 'atmospheres',\n 'atmosphere',\n 'atmos',\n 'drones',\n 'drone',\n 'soundscapes',\n 'soundscape',\n]);\n\n/** Pick the default fade gesture for a track's role (textural → volume, else build). */\nexport function defaultFadeGesture(role: string | null | undefined): FadeGesture {\n if (!role) return 'build';\n const norm = role.toLowerCase().replace(/[\\s_-]+/g, ' ').trim();\n if (TEXTURAL_ROLES.has(norm)) return 'volume';\n for (const token of norm.split(' ')) {\n if (TEXTURAL_ROLES.has(token)) return 'volume';\n }\n return 'build';\n}\n","/**\n * FadeTrackRow — a transition \"fade track\": a single locked TrackRow with a\n * direction badge (Fade in / Fade out) and a one-sided fade slider.\n *\n * A fade is a crossfade with one empty endpoint — a lone generated track that\n * either enters (fade in, for a target-only track) or leaves (fade out, for an\n * origin-only track) across the transition loop. Like a crossfade layer, the\n * sound/generation controls are omitted (the SDK TrackRow is \"controlled by\n * omission\"): no shuffle / Create / preset-pick / FX / drawer / inner-delete.\n * What remains: per-track volume/pan/mute/solo and a single delete.\n *\n * The slider represents WHERE in the loop the fade sits (earlier ↔ later). Omit\n * `onSliderChange` to render it read-only.\n *\n * @since SDK 2.28.0\n */\nimport React from 'react';\nimport { TrackRow } from './TrackRow';\nimport { ConfirmDialog } from './ConfirmDialog';\nimport { EMPTY_FX_DETAIL_STATE } from '../types/fx-toggle.types';\nimport type { TrackLevelsHandle } from '../hooks/useTrackLevels';\nimport type { FadeDirection, FadeGesture } from '../fade-meta';\n\n/** The single (engine track) layer of a fade. */\nexport interface FadeLayer {\n /** Engine track id of this fade's track (also the meter key). */\n trackId: string;\n /** Display name of this fade's (newly created) track. */\n name: string;\n /** Musical role (drives the auto gesture). */\n role?: string;\n /** Name of the SOURCE track this fade was seeded from (origin/target scene). */\n sourceName?: string;\n /** Human label of the copied preset/sound, shown in the caption. */\n soundLabel?: string;\n /** Playback state for this track. */\n runtimeState: { muted: boolean; solo: boolean; volume: number; pan: number };\n}\n\nexport interface FadeTrackRowProps {\n /** The lone fade track. */\n layer: FadeLayer;\n /** 'in' (enters across the loop) or 'out' (leaves across the loop). */\n direction: FadeDirection;\n /** How the fade is shaped — shown read-only (volume = level ramp, build = notes). */\n gesture: FadeGesture;\n /** Fade position 0..1 — WHERE in time the fade sits. Defaults centered. */\n sliderPos?: number;\n /** Toggle mute. */\n onMuteToggle: () => void;\n /** Toggle solo. */\n onSoloToggle: () => void;\n /** Change the track's volume. */\n onVolumeChange: (volume: number) => void;\n /** Change the track's pan. */\n onPanChange: (pan: number) => void;\n /** Delete the fade. */\n onDelete: () => void;\n /** Move the fade point. Omit to render the slider read-only. */\n onSliderChange?: (pos: number) => void;\n /** Shared meter handle (welds a peak meter to the track). */\n levels?: TrackLevelsHandle;\n /** Left-border accent. Defaults to transition purple. */\n accentColor?: string;\n}\n\nfunction FadeCaption({\n layer,\n direction,\n gesture,\n}: {\n layer: FadeLayer;\n direction: FadeDirection;\n gesture: FadeGesture;\n}): React.ReactElement {\n const tag = direction === 'in' ? 'Fade in' : 'Fade out';\n return (\n <div className=\"flex items-center gap-1.5 min-w-0 px-2 py-0.5\">\n <span className=\"text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0\">{tag}</span>\n <span className=\"text-[11px] text-sas-text truncate\" title={layer.sourceName ?? layer.name}>\n {layer.sourceName ?? layer.name}\n </span>\n {layer.soundLabel && (\n <span className=\"text-[9px] text-sas-muted/60 truncate flex-shrink-0\" title={layer.soundLabel}>\n · {layer.soundLabel}\n </span>\n )}\n <span className=\"text-[9px] text-sas-muted/50 flex-shrink-0\" title={`Fade gesture: ${gesture}`}>\n · {gesture}\n </span>\n </div>\n );\n}\n\nexport function FadeTrackRow({\n layer,\n direction,\n gesture,\n sliderPos = 0.5,\n onMuteToggle,\n onSoloToggle,\n onVolumeChange,\n onPanChange,\n onDelete,\n onSliderChange,\n levels,\n accentColor = '#9333EA',\n}: FadeTrackRowProps): React.ReactElement {\n const [confirmDelete, setConfirmDelete] = React.useState(false);\n\n // Slider end labels: a fade-in goes (silent → track), a fade-out goes (track → silent).\n const leftLabel = direction === 'in' ? '(silent)' : (layer.sourceName ?? layer.name);\n const rightLabel = direction === 'in' ? (layer.sourceName ?? layer.name) : '(silent)';\n const badge = direction === 'in' ? '↗ Fade in' : '↘ Fade out';\n\n return (\n <div\n data-testid=\"fade-track-row\"\n className=\"w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden\"\n style={{ borderLeftColor: accentColor, borderLeftWidth: '3px' }}\n >\n {/* Header — direction badge + single delete. */}\n <div className=\"flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60\">\n <span\n data-testid=\"fade-direction-badge\"\n className=\"text-[10px] font-bold uppercase tracking-wide\"\n style={{ color: accentColor }}\n >\n {badge}\n </span>\n <button\n data-testid=\"fade-delete-button\"\n onClick={() => setConfirmDelete(true)}\n className=\"text-sas-danger/70 hover:text-sas-danger px-1 transition-colors text-sm\"\n title=\"Delete fade\"\n aria-label=\"Delete fade\"\n >\n x\n </button>\n </div>\n\n {/* The lone, locked track. Sound/generation controls are omitted; the\n meaningful name + direction live in the caption. */}\n <TrackRow\n track={{ id: layer.trackId, name: '', role: layer.role }}\n runtimeState={layer.runtimeState}\n fxDetailState={EMPTY_FX_DETAIL_STATE}\n drawerOpen={false}\n drawerTab=\"fx\"\n levels={levels}\n accentColor={accentColor}\n contentSlot={<FadeCaption layer={layer} direction={direction} gesture={gesture} />}\n onMuteToggle={onMuteToggle}\n onSoloToggle={onSoloToggle}\n onVolumeChange={onVolumeChange}\n onPanChange={onPanChange}\n />\n\n {/* Fade slider — WHERE in the loop the fade sits. Read-only until wired. */}\n <div className=\"flex items-center gap-2 px-3 py-1.5\" data-testid=\"fade-slider-row\">\n <span\n className=\"text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0\"\n title={leftLabel}\n >\n {leftLabel}\n </span>\n <input\n type=\"range\"\n data-testid=\"fade-slider\"\n min={0}\n max={1}\n step={0.01}\n value={sliderPos}\n disabled={!onSliderChange}\n onChange={\n onSliderChange\n ? (e: React.ChangeEvent<HTMLInputElement>) => onSliderChange(Number(e.target.value))\n : undefined\n }\n style={{ accentColor }}\n className=\"flex-1 disabled:opacity-60 disabled:cursor-not-allowed\"\n aria-label=\"Fade position\"\n />\n <span\n className=\"text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0\"\n title={rightLabel}\n >\n {rightLabel}\n </span>\n </div>\n\n <ConfirmDialog\n open={confirmDelete}\n title=\"Delete fade?\"\n message={<>This fade track will be permanently removed from this scene. This cannot be undone.</>}\n confirmLabel=\"Delete\"\n onConfirm={() => {\n setConfirmDelete(false);\n onDelete();\n }}\n onCancel={() => setConfirmDelete(false)}\n testIdPrefix=\"fade-delete-confirm\"\n />\n </div>\n );\n}\n\nexport default FadeTrackRow;\n","/**\n * FadeModal — \"add a fade\" picker for a transition scene.\n *\n * Shown only inside a `scene_type='transition'` scene. It self-fetches the FROM\n * (origin) and TO (target) scenes' family tracks and diffs them by role to find\n * ORPHANS — tracks with no counterpart on the other side:\n * - origin-only surplus → \"Fade out\" candidates (the track leaves)\n * - target-only surplus → \"Fade in\" candidates (the track enters)\n * Tracks whose role is matched on both sides are crossfade territory and are NOT\n * shown here. A source already used in a crossfade or a fade is hidden (via\n * excludeSourceDbIds).\n *\n * The fade GESTURE (volume vs build) is auto-selected from the track's role and\n * shown read-only — the user does not choose it. On confirm the modal hands the\n * selection + direction + gesture to `onCreate`, which the panel implements\n * (create one track, generate a chord-conforming part, copy the sound, apply a\n * one-sided volume curve). `onCreate` should reject on failure so the modal can\n * show it and stay open.\n *\n * @since SDK 2.28.0\n */\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { Modal } from './Modal';\nimport type { PluginHost, SceneFamilyTrack } from '../types/plugin-sdk.types';\nimport { type FadeDirection, type FadeGesture, defaultFadeGesture } from '../fade-meta';\n\n/** A picked orphan track handed to `onCreate`. */\nexport interface FadeSelection {\n /** Source track DB id (selector for getTrackSound + seeding the part). */\n dbId: string;\n /** Display name (for the row caption). */\n name: string;\n /** Musical role of the source track (drives the auto gesture). */\n role?: string;\n}\n\nexport interface FadeModalProps {\n /** Scoped host — the modal calls listSceneFamilyTracks itself. */\n host: PluginHost;\n /** Controls visibility (the panel owns open/closed from its header button). */\n open: boolean;\n /** DB id of the transition's FROM (origin) scene. */\n fromSceneId: string;\n /** DB id of the transition's TO (target) scene. */\n toSceneId: string;\n /** Display name for the origin scene heading (optional). */\n fromSceneName?: string;\n /** Display name for the target scene heading (optional). */\n toSceneName?: string;\n /** Source-track DB ids already used in a crossfade OR a fade — hidden here. */\n excludeSourceDbIds?: readonly string[];\n /** Close handler (Escape, backdrop, Cancel, or after a successful create). */\n onClose: () => void;\n /** Build the fade. Should reject on failure so the modal shows it. */\n onCreate: (selection: FadeSelection, direction: FadeDirection, gesture: FadeGesture) => Promise<void>;\n /** data-testid prefix. */\n testIdPrefix?: string;\n}\n\ntype LoadState =\n | { status: 'loading' }\n | { status: 'error'; message: string }\n | { status: 'ready'; from: SceneFamilyTrack[]; to: SceneFamilyTrack[] };\n\n/** Short, recognisable id prefix — the full id lives in the row's title. */\nfunction shortId(dbId: string): string {\n return dbId.length > 8 ? dbId.slice(0, 8) : dbId;\n}\n\nconst normRole = (r: string | undefined): string => (r ?? '').toLowerCase().trim();\n\n/**\n * Multiset role-diff: per role token, pair min(from,to) as SHARED (crossfade\n * territory — hidden), and return the surplus on each side as orphans.\n */\nfunction computeOrphans(\n from: SceneFamilyTrack[],\n to: SceneFamilyTrack[],\n excludeSet: ReadonlySet<string>,\n): { fadeOut: SceneFamilyTrack[]; fadeIn: SceneFamilyTrack[] } {\n const bucket = (list: SceneFamilyTrack[]): Map<string, SceneFamilyTrack[]> => {\n const m = new Map<string, SceneFamilyTrack[]>();\n for (const t of list) {\n const k = normRole(t.role);\n const arr = m.get(k);\n if (arr) arr.push(t);\n else m.set(k, [t]);\n }\n return m;\n };\n const fromByRole = bucket(from);\n const toByRole = bucket(to);\n const roles = new Set<string>([...fromByRole.keys(), ...toByRole.keys()]);\n const fadeOut: SceneFamilyTrack[] = [];\n const fadeIn: SceneFamilyTrack[] = [];\n for (const role of roles) {\n const f = fromByRole.get(role) ?? [];\n const t = toByRole.get(role) ?? [];\n const shared = Math.min(f.length, t.length);\n fadeOut.push(...f.slice(shared));\n fadeIn.push(...t.slice(shared));\n }\n return {\n fadeOut: fadeOut.filter((x) => !excludeSet.has(x.dbId)),\n fadeIn: fadeIn.filter((x) => !excludeSet.has(x.dbId)),\n };\n}\n\n/**\n * One selectable orphan row. Prompt-first (users recognise tracks by prompt);\n * role + id + the auto gesture sit underneath in a smaller, muted font.\n */\nfunction OrphanRow({\n track,\n gesture,\n selected,\n disabled,\n onSelect,\n testId,\n}: {\n track: SceneFamilyTrack;\n gesture: FadeGesture;\n selected: boolean;\n disabled: boolean;\n onSelect: () => void;\n testId: string;\n}): React.ReactElement {\n const primary = track.prompt?.trim() || track.name;\n const meta = [track.role, shortId(track.dbId), gesture].filter(Boolean).join(' · ');\n return (\n <button\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n data-testid={testId}\n data-value={track.dbId}\n onClick={onSelect}\n disabled={disabled}\n className={`w-full text-left px-2 py-1.5 rounded-sm border transition-colors disabled:opacity-50 ${\n selected\n ? 'bg-sas-accent/15 border-sas-accent'\n : 'bg-sas-panel border-sas-border hover:border-sas-accent/50'\n }`}\n >\n <div className=\"text-xs text-sas-text truncate\" title={primary}>\n {primary}\n </div>\n {meta && (\n <div className=\"text-[10px] text-sas-muted truncate mt-0.5\" title={track.dbId}>\n {meta}\n </div>\n )}\n </button>\n );\n}\n\nexport function FadeModal({\n host,\n open,\n fromSceneId,\n toSceneId,\n fromSceneName,\n toSceneName,\n excludeSourceDbIds,\n onClose,\n onCreate,\n testIdPrefix = 'fade-modal',\n}: FadeModalProps): React.ReactElement | null {\n const [load, setLoad] = useState<LoadState>({ status: 'loading' });\n const [selectedDbId, setSelectedDbId] = useState<string>('');\n const [isCreating, setIsCreating] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [fromName, setFromName] = useState<string | null>(null);\n const [toName, setToName] = useState<string | null>(null);\n const cancelRef = useRef<HTMLButtonElement>(null);\n\n const refresh = useCallback(async (): Promise<void> => {\n if (!host.listSceneFamilyTracks) {\n setLoad({ status: 'error', message: 'This host does not support fades.' });\n return;\n }\n setLoad({ status: 'loading' });\n try {\n const [from, to, fName, tName] = await Promise.all([\n host.listSceneFamilyTracks(fromSceneId),\n host.listSceneFamilyTracks(toSceneId),\n host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),\n host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null),\n ]);\n setFromName(fName);\n setToName(tName);\n setLoad({ status: 'ready', from, to });\n } catch (err: unknown) {\n setLoad({ status: 'error', message: err instanceof Error ? err.message : 'Failed to load tracks.' });\n }\n }, [host, fromSceneId, toSceneId]);\n\n // Fetch on open; reset state.\n useEffect(() => {\n if (open) {\n setError(null);\n setIsCreating(false);\n setSelectedDbId('');\n void refresh();\n }\n }, [open, refresh]);\n\n const excludeSet = useMemo(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);\n\n const { fadeOut, fadeIn } = useMemo(\n () =>\n load.status === 'ready'\n ? computeOrphans(load.from, load.to, excludeSet)\n : { fadeOut: [] as SceneFamilyTrack[], fadeIn: [] as SceneFamilyTrack[] },\n [load, excludeSet],\n );\n\n // One flat selection space across both sections (dbIds are unique).\n const allOrphans = useMemo(\n () => [\n ...fadeOut.map((t) => ({ track: t, direction: 'out' as FadeDirection })),\n ...fadeIn.map((t) => ({ track: t, direction: 'in' as FadeDirection })),\n ],\n [fadeOut, fadeIn],\n );\n\n // Keep the selection valid / defaulted to the first orphan.\n useEffect(() => {\n if (!allOrphans.some((o) => o.track.dbId === selectedDbId)) {\n setSelectedDbId(allOrphans[0]?.track.dbId ?? '');\n }\n }, [allOrphans, selectedDbId]);\n\n const selected = allOrphans.find((o) => o.track.dbId === selectedDbId) ?? null;\n const canCreate = !isCreating && !!selected;\n\n const handleClose = useCallback((): void => {\n if (!isCreating) onClose();\n }, [isCreating, onClose]);\n\n const handleCreate = useCallback(async (): Promise<void> => {\n if (!selected) return;\n setIsCreating(true);\n setError(null);\n try {\n await onCreate(\n { dbId: selected.track.dbId, name: selected.track.name, role: selected.track.role },\n selected.direction,\n defaultFadeGesture(selected.track.role),\n );\n onClose();\n } catch (err: unknown) {\n setError(err instanceof Error ? err.message : 'Failed to create fade.');\n setIsCreating(false);\n }\n }, [selected, onCreate, onClose]);\n\n const fromLabel = fromName ?? fromSceneName ?? null;\n const toLabel = toName ?? toSceneName ?? null;\n\n if (!open) return null;\n\n const renderSection = (\n heading: string,\n list: SceneFamilyTrack[],\n section: 'out' | 'in',\n ): React.ReactElement | null => {\n if (list.length === 0) return null;\n return (\n <div className=\"block\">\n <span className=\"text-[10px] uppercase tracking-wide text-sas-muted\">{heading}</span>\n <div\n role=\"radiogroup\"\n aria-label={heading}\n data-testid={`${testIdPrefix}-${section === 'out' ? 'fade-out' : 'fade-in'}-list`}\n className=\"mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5\"\n >\n {list.map((t) => (\n <OrphanRow\n key={t.dbId}\n track={t}\n gesture={defaultFadeGesture(t.role)}\n selected={t.dbId === selectedDbId}\n disabled={isCreating}\n onSelect={() => setSelectedDbId(t.dbId)}\n testId={`${testIdPrefix}-option-${t.dbId}`}\n />\n ))}\n </div>\n </div>\n );\n };\n\n return (\n <Modal open={open} onClose={handleClose} testIdPrefix={testIdPrefix} initialFocusRef={cancelRef}>\n <div\n className=\"bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3\"\n onClick={(e: React.MouseEvent) => e.stopPropagation()}\n data-testid={`${testIdPrefix}-box`}\n >\n <h3 className=\"text-sm font-bold text-sas-text\">Add fade</h3>\n <p className=\"text-[11px] text-sas-muted leading-relaxed\">\n Tracks with no counterpart between{' '}\n <span className=\"text-sas-text\">{fromLabel ?? 'the origin scene'}</span> and{' '}\n <span className=\"text-sas-text\">{toLabel ?? 'the target scene'}</span> can gracefully fade\n out (leaving) or fade in (entering) across this transition.\n </p>\n\n {load.status === 'loading' && (\n <div className=\"text-xs text-sas-muted py-4 text-center\">Loading tracks…</div>\n )}\n {load.status === 'error' && (\n <div className=\"text-xs text-sas-danger py-4 text-center\">{load.message}</div>\n )}\n {load.status === 'ready' &&\n (allOrphans.length === 0 ? (\n <div className=\"text-xs text-sas-muted py-4 text-center\" data-testid={`${testIdPrefix}-empty`}>\n Every track has a counterpart in the other scene — nothing to fade. Use “+ Crossfade” to\n bridge matching tracks.\n </div>\n ) : (\n <>\n {renderSection(`Fade out${fromLabel ? ` (from ${fromLabel})` : ''}`, fadeOut, 'out')}\n {renderSection(`Fade in${toLabel ? ` (to ${toLabel})` : ''}`, fadeIn, 'in')}\n </>\n ))}\n\n {error && (\n <div className=\"text-xs text-sas-danger\" data-testid={`${testIdPrefix}-error`}>\n {error}\n </div>\n )}\n\n <div className=\"flex justify-end gap-2 pt-1\">\n <button\n ref={cancelRef}\n data-testid={`${testIdPrefix}-cancel`}\n onClick={onClose}\n disabled={isCreating}\n className=\"px-3 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-text disabled:opacity-50\"\n >\n Cancel\n </button>\n <button\n data-testid={`${testIdPrefix}-confirm`}\n onClick={handleCreate}\n disabled={!canCreate}\n className={`px-3 py-1 text-xs rounded-sm border transition-colors ${\n canCreate\n ? 'bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg'\n : 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n }`}\n >\n {isCreating ? 'Generating fade…' : 'Create fade'}\n </button>\n </div>\n </div>\n </Modal>\n );\n}\n\nexport default FadeModal;\n","/**\n * ImportTrackModal — \"import a track from another scene\" picker (SDK component).\n *\n * Shared by all five generator panels (drums / instruments / synths / loops /\n * stems). Self-fetching: given the scoped `host`, it calls\n * `host.listImportableTracks()` to enumerate candidates (already filtered to\n * the calling panel's type and gate-annotated by the host) and\n * `host.importTrack()` to perform the copy. The UI only renders `importable` +\n * `disabledReason` — it never computes the harmonic/length/tempo gate itself.\n *\n * Two-step picker: choose a source scene, then a track in it. Incompatible\n * tracks render disabled with a reason tooltip (never hidden), per product\n * decision.\n *\n * @since SDK 2.13.0\n */\n\nimport React, { useCallback, useEffect, useState } from 'react';\nimport { Modal } from './Modal';\nimport type {\n PluginHost,\n ImportCandidateScene,\n ImportCandidateTrack,\n PluginTrackHandle,\n} from '../types/plugin-sdk.types';\n\nexport interface ImportTrackModalProps {\n /** Scoped host — the modal calls listImportableTracks / importTrack itself. */\n host: PluginHost;\n /** Controls visibility (the panel owns open/closed from its header button). */\n open: boolean;\n /** Close handler (Escape, backdrop, Cancel, or after a successful import). */\n onClose: () => void;\n /** Fired after a successful import with the new track handle. */\n onImported: (handle: PluginTrackHandle) => void;\n /** Optional modal title (default names the whole-track import). */\n title?: string;\n /** data-testid prefix so each panel's modal is addressable in tests. */\n testIdPrefix?: string;\n /**\n * 'track' (default) imports a whole track via `importTrack`. 'sound' copies\n * ONLY the sound onto an existing track: every candidate is selectable (the\n * contract gate is ignored) and the chosen track is handed back via `onPick`\n * instead of being imported — the panel applies it via `host.getTrackSound`.\n */\n mode?: 'track' | 'sound';\n /** Sound-mode pick handler — required when `mode='sound'`. */\n onPick?: (sel: { sourceTrackDbId: string; trackName: string; sceneName: string }) => void | Promise<void>;\n /**\n * Cross-panel port handler (track mode). When provided, the modal also lists\n * the ACTIVE scene's tracks owned by OTHER panels as a `sameScene` group —\n * shown first and selected by default — and routes a pick there to this\n * callback instead of `importTrack`. The panel re-sounds the part on its own\n * instrument (create track → copy MIDI → load native sound). @since SDK 2.20.0\n */\n onPortTrack?: (sel: { sourceTrackDbId: string; trackName: string; role?: string }) => void | Promise<void>;\n}\n\ntype LoadState =\n | { status: 'loading' }\n | { status: 'error'; message: string }\n | { status: 'ready'; scenes: ImportCandidateScene[] };\n\nexport function ImportTrackModal({\n host,\n open,\n onClose,\n onImported,\n title = 'Import track from scene (must match contract)',\n testIdPrefix = 'import-track',\n mode = 'track',\n onPick,\n onPortTrack,\n}: ImportTrackModalProps): React.ReactElement | null {\n const [load, setLoad] = useState<LoadState>({ status: 'loading' });\n const [selectedSceneId, setSelectedSceneId] = useState<string | null>(null);\n const [importingTrackId, setImportingTrackId] = useState<string | null>(null);\n\n const refresh = useCallback(async (): Promise<void> => {\n if (!host.listImportableTracks) {\n setLoad({ status: 'error', message: 'This host does not support importing tracks.' });\n return;\n }\n setLoad({ status: 'loading' });\n try {\n // Track mode with a port handler also wants the \"this scene — other\n // panels\" group (cross-panel re-sound source); plain/sound flows don't.\n const wantsPort = mode === 'track' && !!onPortTrack;\n const scenes = await host.listImportableTracks(wantsPort ? { includeSameScene: true } : undefined);\n setLoad({ status: 'ready', scenes });\n // Default to the same-scene group when present so the user lands on\n // cross-panel tracks (they can ← back to pick another scene).\n const sameScene = scenes.find((s) => s.sameScene);\n if (sameScene) setSelectedSceneId(sameScene.sceneId);\n } catch (err: unknown) {\n setLoad({ status: 'error', message: err instanceof Error ? err.message : 'Failed to load scenes.' });\n }\n }, [host, mode, onPortTrack]);\n\n // Fetch candidates each time the modal opens; reset selection on close.\n useEffect(() => {\n if (open) {\n setSelectedSceneId(null);\n setImportingTrackId(null);\n void refresh();\n }\n }, [open, refresh]);\n\n const handleImport = useCallback(\n async (\n track: ImportCandidateTrack,\n sourceSceneId: string,\n sceneName: string,\n isSameScene: boolean,\n ): Promise<void> => {\n // Same-scene, other-panel pick: re-sound the part on THIS panel's\n // instrument. The panel creates a track, copies the MIDI, and loads its\n // own sound (see onPortTrack) — never a faithful copy / importTrack.\n if (isSameScene && onPortTrack) {\n if (!track.importable) return;\n setImportingTrackId(track.trackId);\n try {\n await onPortTrack({ sourceTrackDbId: track.dbId, trackName: track.name, role: track.role });\n onClose();\n } catch (err: unknown) {\n host.showToast?.('error', err instanceof Error ? err.message : 'Import failed');\n setImportingTrackId(null);\n }\n return;\n }\n // Sound mode: ignore the gate and hand the pick back to the panel, which\n // reads the source sound via host.getTrackSound and applies it itself.\n if (mode === 'sound') {\n setImportingTrackId(track.trackId);\n try {\n await onPick?.({ sourceTrackDbId: track.dbId, trackName: track.name, sceneName });\n onClose();\n } catch (err: unknown) {\n host.showToast?.('error', err instanceof Error ? err.message : 'Import failed');\n setImportingTrackId(null);\n }\n return;\n }\n if (!track.importable || !host.importTrack) return;\n setImportingTrackId(track.trackId);\n try {\n const handle = await host.importTrack({ sourceSceneId, sourceTrackId: track.trackId });\n onImported(handle);\n onClose();\n } catch (err: unknown) {\n host.showToast?.('error', err instanceof Error ? err.message : 'Import failed');\n setImportingTrackId(null);\n }\n },\n [host, onImported, onClose, mode, onPick, onPortTrack],\n );\n\n if (!open) return null;\n\n const scenes = load.status === 'ready' ? load.scenes : [];\n const selectedScene = scenes.find((s) => s.sceneId === selectedSceneId) ?? null;\n\n return (\n <Modal open={open} onClose={onClose} testIdPrefix={testIdPrefix}>\n <div\n className=\"w-[420px] max-h-[70vh] overflow-hidden flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl\"\n onClick={(e) => e.stopPropagation()}\n data-testid={`${testIdPrefix}-modal`}\n >\n {/* Header */}\n <div className=\"flex items-center justify-between px-3 py-2 border-b border-sas-border\">\n <div className=\"flex items-center gap-2\">\n {selectedScene && (\n <button\n className=\"text-sas-muted hover:text-sas-accent text-xs\"\n onClick={() => setSelectedSceneId(null)}\n data-testid={`${testIdPrefix}-back`}\n >\n ←\n </button>\n )}\n <span className=\"text-sm font-medium text-sas-text\">\n {selectedScene ? selectedScene.sceneName : title}\n </span>\n </div>\n <button\n className=\"text-sas-muted hover:text-sas-accent text-sm\"\n onClick={onClose}\n data-testid={`${testIdPrefix}-close`}\n >\n ✕\n </button>\n </div>\n\n {/* Body */}\n <div className=\"overflow-y-auto p-2 flex-1\">\n {load.status === 'loading' && (\n <div className=\"py-8 text-center text-xs text-sas-muted\" data-testid={`${testIdPrefix}-loading`}>\n Loading scenes…\n </div>\n )}\n\n {load.status === 'error' && (\n <div className=\"py-8 text-center text-xs text-red-400\" data-testid={`${testIdPrefix}-error`}>\n {load.message}\n </div>\n )}\n\n {load.status === 'ready' && scenes.length === 0 && (\n <div className=\"py-8 text-center text-xs text-sas-muted\" data-testid={`${testIdPrefix}-empty`}>\n {mode === 'sound'\n ? 'No other scenes have a sound to import.'\n : 'No other scenes have a compatible track to import.'}\n </div>\n )}\n\n {/* Scene list */}\n {load.status === 'ready' && scenes.length > 0 && !selectedScene && (\n <ul className=\"flex flex-col gap-1\" data-testid={`${testIdPrefix}-scene-list`}>\n {scenes.map((scene) => (\n <li key={scene.sceneId}>\n <button\n className=\"w-full flex items-center justify-between px-2 py-1.5 rounded-sm border border-sas-border bg-sas-panel-alt text-left text-xs text-sas-text hover:border-sas-accent hover:text-sas-accent transition-colors\"\n onClick={() => setSelectedSceneId(scene.sceneId)}\n data-testid={`${testIdPrefix}-scene`}\n >\n <span className=\"truncate\">{scene.sceneName}</span>\n <span className=\"text-sas-muted\">{scene.tracks.length} →</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n\n {/* Track list */}\n {selectedScene && (\n <ul className=\"flex flex-col gap-1\" data-testid={`${testIdPrefix}-track-list`}>\n {selectedScene.tracks.map((track) => {\n const busy = importingTrackId === track.trackId;\n // Sound mode ignores the contract gate — every candidate is a\n // valid sound source. Track mode honors `importable`.\n const gated = mode === 'track' && !track.importable;\n const disabled = gated || busy;\n return (\n <li key={track.dbId}>\n <button\n className={`w-full flex items-center justify-between px-2 py-1.5 rounded-sm border text-left text-xs transition-colors ${\n disabled\n ? 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n : 'bg-sas-panel-alt border-sas-border text-sas-text hover:border-sas-accent hover:text-sas-accent'\n }`}\n disabled={disabled}\n title={gated ? track.disabledReason : undefined}\n onClick={() => void handleImport(track, selectedScene.sceneId, selectedScene.sceneName, !!selectedScene.sameScene)}\n data-testid={`${testIdPrefix}-track`}\n data-importable={mode === 'sound' || track.importable ? 'true' : 'false'}\n >\n <span className=\"truncate\">\n {track.name}\n {track.role ? <span className=\"text-sas-muted\"> · {track.role}</span> : null}\n </span>\n {busy ? (\n <span className=\"text-sas-muted\">…</span>\n ) : gated ? (\n <span className=\"text-sas-muted\">⊘</span>\n ) : null}\n </button>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n </div>\n </Modal>\n );\n}\n","/**\n * CrossfadeModal — \"add a crossfade track\" picker for a transition scene.\n *\n * Shown only inside a `scene_type='transition'` scene. The user picks an ORIGIN\n * track (from the transition's FROM scene) and a TARGET track (from its TO\n * scene), in ANY order — the only constraint is same plugin/family (the picker is\n * per-panel). A source track already used in a crossfade is hidden (via\n * excludeSourceDbIds), so each source is used at most once.\n *\n * Self-fetching: given the scoped `host`, it calls `host.listSceneFamilyTracks`\n * for both scenes (ungated — a transition deliberately bridges different keys).\n * It does NOT build the pair itself; it hands the two selections to `onCreate`,\n * which the panel implements (create two tracks, generate one shared MIDI clip,\n * copy each preset). `onCreate` should reject on failure so the modal can show\n * it and stay open.\n *\n * @since SDK 2.22.0\n */\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { Modal } from './Modal';\nimport type { PluginHost, SceneFamilyTrack } from '../types/plugin-sdk.types';\n\n/** A picked source track handed to `onCreate`. */\nexport interface CrossfadeSelection {\n /** Source track DB id (selector for getTrackSound + crossfade metadata). */\n dbId: string;\n /** Display name (for the row caption). */\n name: string;\n /** Musical role of the source track (the panel uses the TARGET's for generation). */\n role?: string;\n}\n\nexport interface CrossfadeModalProps {\n /** Scoped host — the modal calls listSceneFamilyTracks itself. */\n host: PluginHost;\n /** Controls visibility (the panel owns open/closed from its header button). */\n open: boolean;\n /** DB id of the transition's FROM (origin) scene. */\n fromSceneId: string;\n /** DB id of the transition's TO (target) scene. */\n toSceneId: string;\n /** Display name for the origin scene heading (optional). */\n fromSceneName?: string;\n /** Display name for the target scene heading (optional). */\n toSceneName?: string;\n /**\n * Source-track DB ids already used in a crossfade (origin + target of every\n * existing pair in this panel). Hidden from BOTH dropdowns so each source is\n * used at most once. @since SDK 2.26.0\n */\n excludeSourceDbIds?: readonly string[];\n /** Close handler (Escape, backdrop, Cancel, or after a successful create). */\n onClose: () => void;\n /** Build the crossfade pair. Should reject on failure so the modal shows it. */\n onCreate: (origin: CrossfadeSelection, target: CrossfadeSelection) => Promise<void>;\n /** data-testid prefix. */\n testIdPrefix?: string;\n}\n\ntype LoadState =\n | { status: 'loading' }\n | { status: 'error'; message: string }\n | { status: 'ready'; origin: SceneFamilyTrack[]; target: SceneFamilyTrack[] };\n\n/** Short, recognisable id prefix — the full id lives in the row's title. */\nfunction shortId(dbId: string): string {\n return dbId.length > 8 ? dbId.slice(0, 8) : dbId;\n}\n\n/**\n * One selectable track row. Users recognise tracks by their generation prompt,\n * so the prompt is the prominent line; role + id sit underneath in a smaller,\n * muted font (prompt → role → id order). Falls back to the display name when a\n * track has no prompt (e.g. sample/audio).\n */\nfunction CandidateRow({\n track,\n selected,\n disabled,\n onSelect,\n testId,\n}: {\n track: SceneFamilyTrack;\n selected: boolean;\n disabled: boolean;\n onSelect: () => void;\n testId: string;\n}): React.ReactElement {\n const primary = track.prompt?.trim() || track.name;\n const meta = [track.role, shortId(track.dbId)].filter(Boolean).join(' · ');\n return (\n <button\n type=\"button\"\n role=\"radio\"\n aria-checked={selected}\n data-testid={testId}\n data-value={track.dbId}\n onClick={onSelect}\n disabled={disabled}\n className={`w-full text-left px-2 py-1.5 rounded-sm border transition-colors disabled:opacity-50 ${\n selected\n ? 'bg-sas-accent/15 border-sas-accent'\n : 'bg-sas-panel border-sas-border hover:border-sas-accent/50'\n }`}\n >\n <div className=\"text-xs text-sas-text truncate\" title={primary}>\n {primary}\n </div>\n {meta && (\n <div className=\"text-[10px] text-sas-muted truncate mt-0.5\" title={track.dbId}>\n {meta}\n </div>\n )}\n </button>\n );\n}\n\nexport function CrossfadeModal({\n host,\n open,\n fromSceneId,\n toSceneId,\n fromSceneName,\n toSceneName,\n excludeSourceDbIds,\n onClose,\n onCreate,\n testIdPrefix = 'crossfade-modal',\n}: CrossfadeModalProps): React.ReactElement | null {\n const [load, setLoad] = useState<LoadState>({ status: 'loading' });\n const [originDbId, setOriginDbId] = useState<string>('');\n const [targetDbId, setTargetDbId] = useState<string>('');\n const [isCreating, setIsCreating] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [fromName, setFromName] = useState<string | null>(null);\n const [toName, setToName] = useState<string | null>(null);\n const cancelRef = useRef<HTMLButtonElement>(null);\n\n const refresh = useCallback(async (): Promise<void> => {\n if (!host.listSceneFamilyTracks) {\n setLoad({ status: 'error', message: 'This host does not support crossfade tracks.' });\n return;\n }\n setLoad({ status: 'loading' });\n try {\n const [origin, target, fName, tName] = await Promise.all([\n host.listSceneFamilyTracks(fromSceneId),\n host.listSceneFamilyTracks(toSceneId),\n host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),\n host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null),\n ]);\n setFromName(fName);\n setToName(tName);\n setLoad({ status: 'ready', origin, target });\n } catch (err: unknown) {\n setLoad({ status: 'error', message: err instanceof Error ? err.message : 'Failed to load tracks.' });\n }\n }, [host, fromSceneId, toSceneId]);\n\n // Fetch on open; reset state.\n useEffect(() => {\n if (open) {\n setError(null);\n setIsCreating(false);\n setOriginDbId('');\n setTargetDbId('');\n void refresh();\n }\n }, [open, refresh]);\n\n // Hide any source track already used in a crossfade (each source used once).\n const excludeSet = useMemo(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);\n\n // The only constraint is same plugin/family (already enforced per-panel), so the\n // two lists are independent — pick in any order, any role.\n const originCandidates = useMemo(\n () => (load.status === 'ready' ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : []),\n [load, excludeSet],\n );\n const targetCandidates = useMemo(\n () => (load.status === 'ready' ? load.target.filter((t) => !excludeSet.has(t.dbId)) : []),\n [load, excludeSet],\n );\n\n // Keep each selection valid / defaulted to its first candidate, independently.\n useEffect(() => {\n if (!originCandidates.some((t) => t.dbId === originDbId)) {\n setOriginDbId(originCandidates[0]?.dbId ?? '');\n }\n }, [originCandidates, originDbId]);\n useEffect(() => {\n if (!targetCandidates.some((t) => t.dbId === targetDbId)) {\n setTargetDbId(targetCandidates[0]?.dbId ?? '');\n }\n }, [targetCandidates, targetDbId]);\n\n const originTrack = originCandidates.find((t) => t.dbId === originDbId) ?? null;\n const targetTrack = targetCandidates.find((t) => t.dbId === targetDbId) ?? null;\n const canCreate = !isCreating && !!originTrack && !!targetTrack;\n\n const handleClose = useCallback((): void => {\n if (!isCreating) onClose();\n }, [isCreating, onClose]);\n\n const handleCreate = useCallback(async (): Promise<void> => {\n if (!originTrack || !targetTrack) return;\n setIsCreating(true);\n setError(null);\n try {\n await onCreate(\n { dbId: originTrack.dbId, name: originTrack.name, role: originTrack.role },\n { dbId: targetTrack.dbId, name: targetTrack.name, role: targetTrack.role },\n );\n onClose();\n } catch (err: unknown) {\n setError(err instanceof Error ? err.message : 'Failed to create crossfade.');\n setIsCreating(false);\n }\n }, [originTrack, targetTrack, onCreate, onClose]);\n\n // Prefer the live-fetched scene names; fall back to the optional props.\n const fromLabel = fromName ?? fromSceneName ?? null;\n const toLabel = toName ?? toSceneName ?? null;\n\n if (!open) return null;\n\n return (\n <Modal open={open} onClose={handleClose} testIdPrefix={testIdPrefix} initialFocusRef={cancelRef}>\n <div\n className=\"bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3\"\n onClick={(e: React.MouseEvent) => e.stopPropagation()}\n data-testid={`${testIdPrefix}-box`}\n >\n <h3 className=\"text-sm font-bold text-sas-text\">Add crossfade</h3>\n <p className=\"text-[11px] text-sas-muted leading-relaxed\">\n Bridge a track from{' '}\n <span className=\"text-sas-text\">{fromLabel ?? 'the origin scene'}</span> into one from{' '}\n <span className=\"text-sas-text\">{toLabel ?? 'the target scene'}</span>. Both layers share one\n generated part; each keeps its own preset.\n </p>\n\n {load.status === 'loading' && (\n <div className=\"text-xs text-sas-muted py-4 text-center\">Loading tracks…</div>\n )}\n {load.status === 'error' && (\n <div className=\"text-xs text-sas-danger py-4 text-center\">{load.message}</div>\n )}\n {load.status === 'ready' &&\n (originCandidates.length === 0 ? (\n <div\n className=\"text-xs text-sas-muted py-4 text-center\"\n data-testid={`${testIdPrefix}-empty-origin`}\n >\n No available tracks in {fromLabel ?? 'the origin scene'}. Add one (or free one from another\n crossfade) first.\n </div>\n ) : (\n <>\n <div className=\"block\">\n <span className=\"text-[10px] uppercase tracking-wide text-sas-muted\">\n Origin {fromLabel ? `(${fromLabel})` : '(top)'}\n </span>\n <div\n role=\"radiogroup\"\n aria-label=\"Origin track\"\n data-testid={`${testIdPrefix}-origin-list`}\n className=\"mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5\"\n >\n {originCandidates.map((t) => (\n <CandidateRow\n key={t.dbId}\n track={t}\n selected={t.dbId === originDbId}\n disabled={isCreating}\n onSelect={() => setOriginDbId(t.dbId)}\n testId={`${testIdPrefix}-origin-option-${t.dbId}`}\n />\n ))}\n </div>\n </div>\n\n <div className=\"block\">\n <span className=\"text-[10px] uppercase tracking-wide text-sas-muted\">\n Target {toLabel ? `(${toLabel})` : '(bottom)'}\n </span>\n {targetCandidates.length === 0 ? (\n <div className=\"text-xs text-sas-danger mt-0.5\" data-testid={`${testIdPrefix}-empty-target`}>\n No available tracks in {toLabel ?? 'the target scene'} to crossfade into.\n </div>\n ) : (\n <div\n role=\"radiogroup\"\n aria-label=\"Target track\"\n data-testid={`${testIdPrefix}-target-list`}\n className=\"mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5\"\n >\n {targetCandidates.map((t) => (\n <CandidateRow\n key={t.dbId}\n track={t}\n selected={t.dbId === targetDbId}\n disabled={isCreating}\n onSelect={() => setTargetDbId(t.dbId)}\n testId={`${testIdPrefix}-target-option-${t.dbId}`}\n />\n ))}\n </div>\n )}\n </div>\n </>\n ))}\n\n {error && (\n <div className=\"text-xs text-sas-danger\" data-testid={`${testIdPrefix}-error`}>\n {error}\n </div>\n )}\n\n <div className=\"flex justify-end gap-2 pt-1\">\n <button\n ref={cancelRef}\n data-testid={`${testIdPrefix}-cancel`}\n onClick={onClose}\n disabled={isCreating}\n className=\"px-3 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-text disabled:opacity-50\"\n >\n Cancel\n </button>\n <button\n data-testid={`${testIdPrefix}-confirm`}\n onClick={handleCreate}\n disabled={!canCreate}\n className={`px-3 py-1 text-xs rounded-sm border transition-colors ${\n canCreate\n ? 'bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg'\n : 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n }`}\n >\n {isCreating ? 'Generating bridge…' : 'Create crossfade'}\n </button>\n </div>\n </div>\n </Modal>\n );\n}\n\nexport default CrossfadeModal;\n","/**\n * DownloadPackButton — versioned-pack download trigger (SDK component).\n *\n * Parameterized by `packId`; drives the download through the host\n * (`host.startSamplePackDownload` / `host.onSamplePackProgress`) so plugins\n * never reach into the app's IPC (`window.electronAPI`). Two display variants:\n * - 'compact' (default) — small uppercase button for panel headers\n * - 'large' — bigger CTA used inside SamplePackCTACard\n *\n * @since SDK 2.8.0 (moved from the app and refactored onto PluginHost).\n */\n\nimport React, { useCallback, useEffect, useState } from 'react';\nimport type { PluginHost } from '../types/plugin-sdk.types';\n\nexport type DownloadPackButtonVariant = 'compact' | 'large';\n\ntype PackDownloadStatus =\n | 'idle'\n | 'downloading'\n | 'verifying'\n | 'extracting'\n | 'installing'\n | 'complete'\n | 'error';\n\nexport interface DownloadPackButtonProps {\n /** Host the plugin received; drives the download + progress. */\n host: PluginHost;\n packId: string;\n /** Pack display name, e.g. 'Drum Sample Library'. Used in tooltips/labels. */\n displayName: string;\n /** Bundle size in bytes (shown in the large-variant label). */\n sizeBytes?: number;\n variant?: DownloadPackButtonVariant;\n /** Called once after the install completes (status === 'complete'). */\n onDownloadComplete?: () => void;\n}\n\n// Base-1024 (GiB/MiB) to match the host's own SamplePackDownloader formatter and\n// the `_pack-version.json` / sample-packs.ts size comments (e.g. a 28.5e9-byte\n// instrument bundle reads as \"26.6 GB\", not the decimal \"28.5 GB\").\nfunction formatSize(bytes?: number): string {\n if (!bytes || bytes <= 0) return '';\n const gb = bytes / 1024 ** 3;\n if (gb >= 1) return `${gb.toFixed(1)} GB`;\n const mb = bytes / 1024 ** 2;\n return `${Math.round(mb)} MB`;\n}\n\nexport const DownloadPackButton: React.FC<DownloadPackButtonProps> = ({\n host,\n packId,\n displayName,\n sizeBytes,\n variant = 'compact',\n onDownloadComplete,\n}) => {\n const [status, setStatus] = useState<PackDownloadStatus>('idle');\n const [progress, setProgress] = useState(0);\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n\n useEffect(() => {\n const unsub = host.onSamplePackProgress(packId, (p) => {\n setStatus(p.status as PackDownloadStatus);\n setProgress(p.progress);\n if (p.status === 'error') {\n setErrorMessage(p.message || 'Download failed');\n } else if (p.status === 'complete') {\n setErrorMessage(null);\n setTimeout(() => onDownloadComplete?.(), 250);\n } else {\n setErrorMessage(null);\n }\n });\n return unsub;\n }, [host, packId, onDownloadComplete]);\n\n const handleClick = useCallback(async (): Promise<void> => {\n if (status !== 'idle' && status !== 'error') return;\n try {\n setStatus('downloading');\n setProgress(0);\n setErrorMessage(null);\n const result = await host.startSamplePackDownload(packId);\n if (!result.success) {\n setStatus('error');\n setErrorMessage(result.error || 'Download failed');\n }\n } catch (err) {\n console.error('[DownloadPackButton] start failed:', err);\n setStatus('error');\n setErrorMessage(err instanceof Error ? err.message : String(err));\n }\n }, [host, packId, status]);\n\n const isWorking =\n status === 'downloading' ||\n status === 'verifying' ||\n status === 'extracting' ||\n status === 'installing';\n const isDisabled = isWorking || status === 'complete';\n\n const buttonLabel = (() => {\n switch (status) {\n case 'downloading':\n return `${progress}%`;\n case 'verifying':\n return 'Verifying...';\n case 'extracting':\n return 'Extracting...';\n case 'installing':\n return 'Installing...';\n case 'complete':\n return 'Done!';\n case 'error':\n return 'Retry';\n default:\n return variant === 'large'\n ? `Download ${displayName}${sizeBytes ? ` (${formatSize(sizeBytes)})` : ''}`\n : 'Download';\n }\n })();\n\n const tooltip = (() => {\n if (status === 'error') return errorMessage || 'Download failed. Click to retry.';\n if (isWorking) return `${buttonLabel} — ${displayName}`;\n if (status === 'complete') return 'Installation complete';\n return `Download ${displayName}${sizeBytes ? ` (${formatSize(sizeBytes)})` : ''}`;\n })();\n\n const baseClasses =\n variant === 'large'\n ? 'px-4 py-2 text-sm font-medium rounded border transition-colors'\n : 'px-2 py-0.5 text-[10px] uppercase tracking-wide rounded-sm border transition-colors';\n\n let className: string;\n if (status === 'error') {\n className = `${baseClasses} text-red-400 border-red-400/50 hover:text-red-300 hover:border-red-300`;\n } else if (status === 'complete') {\n className = `${baseClasses} text-green-400 border-green-400/50`;\n } else if (isDisabled) {\n className = `${baseClasses} text-sas-accent border-sas-accent/50 cursor-wait`;\n } else {\n className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;\n }\n\n return (\n <div>\n <button\n data-testid={`download-pack-button-${packId}`}\n onClick={handleClick}\n disabled={isDisabled}\n className={className}\n title={tooltip}\n >\n {buttonLabel}\n </button>\n {variant === 'large' && status === 'error' && errorMessage && (\n <div className=\"text-xs text-sas-danger mt-2\" data-testid={`download-pack-error-${packId}`}>\n {errorMessage}\n </div>\n )}\n </div>\n );\n};\n\nexport default DownloadPackButton;\n","/**\n * SamplePackCTACard — empty-state card a generator panel renders when its\n * sample pack is missing OR a newer version is available. Wraps\n * DownloadPackButton in a centered card. The completion callback should\n * re-fetch pack status on the parent so the card unmounts and the normal panel\n * UI takes over.\n *\n * @since SDK 2.8.0 (moved from the app; download driven through PluginHost).\n */\n\nimport React from 'react';\nimport type { PluginHost } from '../types/plugin-sdk.types';\nimport { DownloadPackButton } from './DownloadPackButton';\n\nexport type SamplePackCTACardStatus = 'missing' | 'stale' | 'checking';\n\n/** Minimal pack info the card needs. A PackConfig is structurally compatible. */\nexport interface SamplePackCardInfo {\n packId: string;\n displayName: string;\n description: string;\n sizeBytes?: number;\n}\n\nexport interface SamplePackCTACardProps {\n /** Host the plugin received; drives the download. */\n host: PluginHost;\n pack: SamplePackCardInfo;\n status: SamplePackCTACardStatus;\n onDownloadComplete?: () => void;\n}\n\nexport const SamplePackCTACard: React.FC<SamplePackCTACardProps> = ({\n host,\n pack,\n status,\n onDownloadComplete,\n}) => {\n if (status === 'checking') {\n return (\n <div\n data-testid={`sample-pack-cta-checking-${pack.packId}`}\n className=\"flex items-center justify-center py-16 text-sas-muted text-sm\"\n >\n Checking sample library...\n </div>\n );\n }\n\n const headline =\n status === 'stale'\n ? `${pack.displayName} update available`\n : `${pack.displayName} not installed`;\n\n const sublabel =\n status === 'stale'\n ? `A newer version is available for download.`\n : pack.description;\n\n return (\n <div\n data-testid={`sample-pack-cta-${pack.packId}`}\n className=\"flex flex-col items-center justify-center py-12 px-6 text-center\"\n >\n <div className=\"text-sm uppercase tracking-wide text-sas-muted mb-2\">\n {status === 'stale' ? 'Update available' : 'Sample library not installed'}\n </div>\n <div className=\"text-base text-sas-text mb-1\">{headline}</div>\n <div className=\"text-xs text-sas-muted mb-6 max-w-md\">{sublabel}</div>\n <DownloadPackButton\n host={host}\n packId={pack.packId}\n displayName={pack.displayName}\n sizeBytes={pack.sizeBytes}\n variant=\"large\"\n onDownloadComplete={onDownloadComplete}\n />\n </div>\n );\n};\n\nexport default SamplePackCTACard;\n","/**\n * WaveformView — small canvas waveform for an audio file on disk.\n *\n * Reads bytes via `host.getAudioFileBytes`, decodes via\n * `AudioContext.decodeAudioData`, computes peaks, and renders to a\n * canvas. Suitable for take rows, sample previews, or any place a\n * decorative ~40px waveform makes sense.\n *\n * The component is self-contained: it owns the AudioContext and the\n * peak buffer, decodes once per `filePath` change, and tears down on\n * unmount. Failures (file missing, decode error) render as a silent\n * blank canvas — the caller can decide how to surface errors.\n */\n\nimport React, { useEffect, useRef, useState } from 'react';\nimport type { PluginHost } from '../types/plugin-sdk.types';\nimport { computePeaks, drawWaveform, type WaveformPeaks } from './waveform';\n\nexport interface WaveformViewProps {\n host: PluginHost;\n filePath: string;\n /** Number of bins to compute. Default 256 — plenty for ~40px tall rows. */\n bins?: number;\n /** Tailwind / inline className for sizing. Default: w-full h-10. */\n className?: string;\n /** Override the bar fill style (e.g., to match a track color). */\n fillStyle?: string;\n /**\n * If set, the bin range spans `targetSamples` instead of the file's\n * actual length. Bins beyond the audio render as flat silence — used\n * to align a partial recording inside a full-loop-width canvas so\n * every take row has the same time scale.\n */\n targetSamples?: number;\n}\n\nexport const WaveformView: React.FC<WaveformViewProps> = ({\n host,\n filePath,\n bins = 256,\n className,\n fillStyle,\n targetSamples,\n}) => {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const [peaks, setPeaks] = useState<WaveformPeaks | null>(null);\n\n // Decode + compute peaks whenever the file changes.\n useEffect(() => {\n let cancelled = false;\n let audioContext: AudioContext | null = null;\n\n (async () => {\n try {\n const bytes = await host.getAudioFileBytes(filePath);\n if (cancelled) return;\n\n // OfflineAudioContext would be cheaper but its constructor needs\n // sampleRate/length up front — we don't know them until decode.\n const ContextCtor: typeof AudioContext =\n (window as unknown as { AudioContext?: typeof AudioContext }).AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext!;\n audioContext = new ContextCtor();\n\n // decodeAudioData mutates / detaches the buffer in some impls,\n // so pass a copy.\n const audioBuffer = await audioContext.decodeAudioData(bytes.slice(0));\n if (cancelled) return;\n\n const computed = computePeaks(audioBuffer, bins, targetSamples);\n setPeaks(computed);\n } catch (err) {\n // Silent: the canvas stays blank. Caller can layer their own\n // error UI on top if needed.\n console.warn('[WaveformView] failed to decode', filePath, err);\n } finally {\n if (audioContext) {\n audioContext.close().catch(() => { /* ignore */ });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [host, filePath, bins, targetSamples]);\n\n // Repaint whenever peaks update — including layout-driven resizes via\n // ResizeObserver so the canvas stays crisp at the current CSS width.\n useEffect(() => {\n if (!peaks) return;\n const canvas = canvasRef.current;\n if (!canvas) return;\n drawWaveform(canvas, peaks, fillStyle ? { fillStyle } : undefined);\n\n const observer = new ResizeObserver(() => {\n drawWaveform(canvas, peaks, fillStyle ? { fillStyle } : undefined);\n });\n observer.observe(canvas);\n return () => observer.disconnect();\n }, [peaks, fillStyle]);\n\n return (\n <canvas\n ref={canvasRef}\n data-testid=\"waveform-view\"\n className={className ?? 'w-full h-10'}\n />\n );\n};\n\nexport default WaveformView;\n","/**\n * Shared waveform peaks + canvas drawer.\n *\n * Originally inlined in `stems/TrimEditorDrawer.tsx`; lifted to\n * this module so the recorder plugin's per-take rows can render the\n * same compact min/max display without duplicating the math.\n *\n * Design:\n * - `computePeaks` reduces an AudioBuffer to `bins` min/max pairs (mono\n * average across channels). Output layout is interleaved\n * `[min0, max0, min1, max1, ...]` so the renderer reads pairs\n * sequentially without index arithmetic.\n * - `drawWaveform` paints one 1px vertical bar per canvas column,\n * dpr-aware so it stays crisp on retina displays.\n *\n * No host or React dependencies — pure functions are safe to use from\n * tests, web workers, or non-React renderers.\n */\n\nexport interface WaveformPeaks {\n /** Sample rate of the source file (used to convert sample → seconds). */\n sampleRate: number;\n /** Total length of the raw file in samples. */\n totalSamples: number;\n /** Min/max pairs per bin (length = bins × 2). */\n peaks: Float32Array;\n}\n\n/**\n * Reduce an AudioBuffer to `bins` min/max pairs. Mono averages across\n * channels. The output buffer is fixed-size (`bins * 2`) for fast canvas\n * traversal.\n *\n * `targetSamples` (optional) extends the bin range to a fixed sample\n * count larger than the buffer's actual length — bins falling beyond\n * the buffer get (0, 0) pairs, which renders as a flat tail. Used by\n * the recorder so a partial last chunk's waveform sits at the start of\n * a full-loop-width canvas instead of being stretched to fill.\n */\nexport function computePeaks(\n audioBuffer: AudioBuffer,\n bins: number,\n targetSamples?: number\n): WaveformPeaks {\n const { length, numberOfChannels, sampleRate } = audioBuffer;\n const channels: Float32Array[] = [];\n for (let c = 0; c < numberOfChannels; c++) {\n channels.push(audioBuffer.getChannelData(c));\n }\n const totalForBinning =\n typeof targetSamples === 'number' && targetSamples > length ? targetSamples : length;\n const samplesPerBin = Math.max(1, Math.floor(totalForBinning / bins));\n const out = new Float32Array(bins * 2);\n for (let i = 0; i < bins; i++) {\n const startIdx = i * samplesPerBin;\n const endIdx = Math.min(length, startIdx + samplesPerBin);\n if (startIdx >= length) {\n // Bin falls entirely past the audio's end — render as silence.\n out[i * 2] = 0;\n out[i * 2 + 1] = 0;\n continue;\n }\n let mn = Infinity;\n let mx = -Infinity;\n for (let j = startIdx; j < endIdx; j++) {\n let v = 0;\n for (let c = 0; c < numberOfChannels; c++) {\n v += channels[c][j];\n }\n v /= numberOfChannels;\n if (v < mn) mn = v;\n if (v > mx) mx = v;\n }\n if (!Number.isFinite(mn)) mn = 0;\n if (!Number.isFinite(mx)) mx = 0;\n out[i * 2] = mn;\n out[i * 2 + 1] = mx;\n }\n return { sampleRate, totalSamples: totalForBinning, peaks: out };\n}\n\n/**\n * Draw min/max peaks to the given canvas. Resizes the canvas backing\n * store to CSS pixels × devicePixelRatio so the result is crisp on\n * retina. Caller controls CSS sizing via the `<canvas>` element's\n * className.\n */\nexport function drawWaveform(\n canvas: HTMLCanvasElement,\n peaks: WaveformPeaks,\n options: { fillStyle?: string } = {}\n): void {\n const dpr = window.devicePixelRatio || 1;\n const cssWidth = canvas.clientWidth;\n const cssHeight = canvas.clientHeight;\n if (cssWidth === 0 || cssHeight === 0) return;\n canvas.width = Math.floor(cssWidth * dpr);\n canvas.height = Math.floor(cssHeight * dpr);\n const ctx = canvas.getContext('2d');\n if (!ctx) return;\n ctx.scale(dpr, dpr);\n ctx.clearRect(0, 0, cssWidth, cssHeight);\n ctx.fillStyle = options.fillStyle ?? 'rgba(255, 255, 255, 0.4)';\n\n const bins = peaks.peaks.length / 2;\n const mid = cssHeight / 2;\n for (let x = 0; x < cssWidth; x++) {\n const binIdx = Math.floor((x / cssWidth) * bins);\n const mn = peaks.peaks[binIdx * 2];\n const mx = peaks.peaks[binIdx * 2 + 1];\n const yTop = mid - mx * mid;\n const yBot = mid - mn * mid;\n ctx.fillRect(x, yTop, 1, Math.max(1, yBot - yTop));\n }\n}\n","/**\n * ScrollingWaveform — live waveform during recording (Phase 8.10).\n *\n * Reads the platform's `peakDb` history and renders it as a horizontal\n * bar-graph that scrolls left as new samples arrive. Two halves: top\n * band shows positive amplitude, bottom band mirrors it (matches the\n * static waveform's min/max layout in `WaveformView`).\n *\n * The data source is a function the caller supplies — typically a ref\n * to the `inputLevelDb` value from `AudioRoutingContext` polled at\n * ~30Hz. The component samples that ref via requestAnimationFrame and\n * shifts a fixed-size float ring buffer one column per frame.\n *\n * Pure presentational + animation logic; no IPC. Stops animating\n * when `active` is false (engine isn't running the audio callback).\n */\n\nimport React, { useEffect, useRef } from 'react';\n\nexport interface ScrollingWaveformProps {\n /** Function returning the latest peak in dBFS. Called per RAF. */\n getPeakDb: () => number;\n /** True while the audio callback is running; false freezes the wave. */\n active: boolean;\n /** Number of horizontal columns in the ring buffer. */\n columns?: number;\n /** Optional className for sizing. */\n className?: string;\n /** Highlight color for the wave. */\n fillStyle?: string;\n}\n\nexport const ScrollingWaveform: React.FC<ScrollingWaveformProps> = ({\n getPeakDb,\n active,\n columns = 256,\n className,\n fillStyle,\n}) => {\n const canvasRef = useRef<HTMLCanvasElement>(null);\n const ringRef = useRef<Float32Array>(new Float32Array(columns));\n const writeIdxRef = useRef(0);\n const rafRef = useRef<number | null>(null);\n\n // Recreate the ring buffer if `columns` changes — preserve any data\n // that fits.\n useEffect(() => {\n if (ringRef.current.length !== columns) {\n const next = new Float32Array(columns);\n const prev = ringRef.current;\n const copyLen = Math.min(prev.length, columns);\n // Copy the tail of the previous buffer into the head of the new one.\n for (let i = 0; i < copyLen; i++) {\n next[i] = prev[i];\n }\n ringRef.current = next;\n writeIdxRef.current = writeIdxRef.current % columns;\n }\n }, [columns]);\n\n useEffect(() => {\n if (!active) {\n // Freeze the wave but leave the existing buffer on screen.\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n return;\n }\n\n const tick = (): void => {\n const peakDb = getPeakDb();\n // Map dBFS → normalised amplitude [0, 1]. -60dB → 0, 0dB → 1.\n const amp =\n peakDb <= -120\n ? 0\n : Math.max(0, Math.min(1, (peakDb + 60) / 60));\n const ring = ringRef.current;\n ring[writeIdxRef.current] = amp;\n writeIdxRef.current = (writeIdxRef.current + 1) % ring.length;\n\n // Draw.\n const canvas = canvasRef.current;\n if (canvas) {\n const dpr = window.devicePixelRatio || 1;\n const cssW = canvas.clientWidth;\n const cssH = canvas.clientHeight;\n if (cssW > 0 && cssH > 0) {\n if (canvas.width !== Math.floor(cssW * dpr) || canvas.height !== Math.floor(cssH * dpr)) {\n canvas.width = Math.floor(cssW * dpr);\n canvas.height = Math.floor(cssH * dpr);\n }\n const ctx = canvas.getContext('2d');\n if (ctx) {\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, cssW, cssH);\n ctx.fillStyle = fillStyle ?? '#6af2c5';\n const mid = cssH / 2;\n const cols = ring.length;\n const colW = cssW / cols;\n // Read the ring oldest → newest so the wave scrolls left.\n const start = writeIdxRef.current; // oldest sample\n for (let x = 0; x < cols; x++) {\n const ringIdx = (start + x) % cols;\n const a = ring[ringIdx];\n const half = a * mid;\n ctx.fillRect(x * colW, mid - half, Math.max(1, colW), Math.max(1, half * 2));\n }\n }\n }\n }\n rafRef.current = requestAnimationFrame(tick);\n };\n rafRef.current = requestAnimationFrame(tick);\n\n return () => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n };\n }, [active, getPeakDb, fillStyle]);\n\n return (\n <canvas\n ref={canvasRef}\n data-testid=\"scrolling-waveform\"\n className={className ?? 'w-full h-12'}\n />\n );\n};\n\nexport default ScrollingWaveform;\n","/**\n * OffsetScrubber — manual sample-offset slider for Lyria-generated audio.\n *\n * Renders a thin horizontal track with one tick per detected beat (tall\n * tick on the downbeat) and a draggable thumb. Drag distance maps to a\n * sample offset that is applied to the audio clip via\n * `host.setAudioOffsetSamples(trackId, n)`.\n *\n * Snap behavior:\n * - Default: snap to the nearest beat in `cuePoints.beats`.\n * - Hold Shift: bypass snap (free 1-sample resolution).\n * - Click on a tick mark: jump to that beat exactly.\n *\n * The visible range is one bar (= meter beats) on each side of bar 1.\n * For a 4-bar / 4/4 clip at 44100 Hz, one bar at 120 BPM is 88_200\n * samples — so the slider covers ±88_200 samples, ~2 s either way. That\n * matches the alignment errors we observe from Lyria detection misses\n * (typically <1 beat off).\n *\n * BPM mismatch chip: shown when `cuePoints.detected_bpm` is more than\n * 1 BPM away from the project BPM, since the beat ticks won't line up\n * with the project grid in that case.\n */\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport type { PluginCuePoints } from '../types/plugin-sdk.types';\n\nconst SLIDER_HEIGHT_PX = 28;\nconst TICK_HEIGHT_PX = 14;\nconst DOWNBEAT_TICK_HEIGHT_PX = 22;\nconst THUMB_WIDTH_PX = 4;\n\nexport interface OffsetScrubberProps {\n /** Detected beat positions + sample rate. Slider is disabled when null. */\n cuePoints: PluginCuePoints | null;\n /** Current offset, in samples (signed). */\n offsetSamples: number;\n /** Project BPM — used to compute the visible range and the mismatch chip. */\n projectBpm: number;\n /** Beats per bar, defaults to 4. */\n meter?: number;\n /** Called on drag-end with the resolved offset (already snapped). */\n onChange: (offsetSamples: number) => void;\n /** Disable interaction (e.g., during generation / split). */\n disabled?: boolean;\n}\n\nexport function OffsetScrubber({\n cuePoints,\n offsetSamples,\n projectBpm,\n meter = 4,\n onChange,\n disabled = false,\n}: OffsetScrubberProps): React.ReactElement {\n const trackRef = useRef<HTMLDivElement | null>(null);\n // Local optimistic offset during drag — committed on mouseup\n const [draftOffset, setDraftOffset] = useState<number>(offsetSamples);\n const [isDragging, setIsDragging] = useState(false);\n\n // Keep the draft synced with the parent prop when not dragging.\n useEffect(() => {\n if (!isDragging) setDraftOffset(offsetSamples);\n }, [offsetSamples, isDragging]);\n\n // Range is ±1 bar of samples around the downbeat.\n // beats are 60 / bpm seconds; bar = meter beats.\n const sampleRate = cuePoints?.sample_rate ?? 44100;\n const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;\n const beatsForRange = useMemo(() => {\n // Use the project BPM for the visible range so the slider scale\n // matches what the user is editing against in the timeline.\n return Math.round((60 / projectBpm) * sampleRate);\n }, [projectBpm, sampleRate]);\n const rangeSamples = beatsForRange * meter; // ±1 bar\n\n // Map a sample offset to a 0..1 position on the slider track.\n const sampleToFraction = useCallback(\n (sample: number): number => {\n const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));\n return (clamped + rangeSamples) / (2 * rangeSamples);\n },\n [rangeSamples],\n );\n\n const fractionToSample = useCallback(\n (fraction: number): number => {\n const clamped = Math.max(0, Math.min(1, fraction));\n return Math.round(clamped * 2 * rangeSamples - rangeSamples);\n },\n [rangeSamples],\n );\n\n // Snap a candidate sample to the nearest detected beat. Beats are\n // CuePoints.beats positions (relative to clip start). Offset slider\n // semantics: positive = shift clip later; we map offset onto the\n // beats array so the user lines up the desired beat with bar 1.\n //\n // Implementation: each beat[i] corresponds to a candidate offset\n // value of `beats[i] - beats[0]` (the relative distance the user has\n // shifted the clip). Snap to the nearest such candidate.\n const snapTargets = useMemo(() => {\n if (!cuePoints || cuePoints.beats.length === 0) return [];\n const downbeat = cuePoints.beats[0];\n // Snap candidates: differences between every beat and the downbeat\n // (positive shifts) plus their negation (negative shifts). De-dup +\n // sort so binary search is cheap if the array gets large.\n const positives = cuePoints.beats.map((b) => b - downbeat);\n const negatives = positives.slice(1).map((p) => -p); // skip 0 to avoid dupe\n return [...negatives, ...positives].sort((a, b) => a - b);\n }, [cuePoints]);\n\n const snapToBeat = useCallback(\n (sample: number): number => {\n if (snapTargets.length === 0) return sample;\n // Linear scan — beats[] is small (≤ 16 for v1). Switch to binary\n // search if we ever generate longer clips.\n let best = snapTargets[0];\n let bestDist = Math.abs(sample - best);\n for (const t of snapTargets) {\n const d = Math.abs(sample - t);\n if (d < bestDist) {\n best = t;\n bestDist = d;\n }\n }\n return best;\n },\n [snapTargets],\n );\n\n // Drag handler — pointer events let us track outside the element.\n const handlePointerDown = useCallback(\n (e: React.PointerEvent<HTMLDivElement>): void => {\n if (disabled || !cuePoints) return;\n e.preventDefault();\n const track = trackRef.current;\n if (!track) return;\n track.setPointerCapture(e.pointerId);\n setIsDragging(true);\n\n const updateFromEvent = (clientX: number, shiftHeld: boolean): number => {\n const rect = track.getBoundingClientRect();\n const fraction = (clientX - rect.left) / rect.width;\n const raw = fractionToSample(fraction);\n return shiftHeld ? raw : snapToBeat(raw);\n };\n\n // Apply the initial click position immediately.\n setDraftOffset(updateFromEvent(e.clientX, e.shiftKey));\n\n const onMove = (ev: PointerEvent): void => {\n setDraftOffset(updateFromEvent(ev.clientX, ev.shiftKey));\n };\n const onUp = (ev: PointerEvent): void => {\n const final = updateFromEvent(ev.clientX, ev.shiftKey);\n track.releasePointerCapture(e.pointerId);\n track.removeEventListener('pointermove', onMove);\n track.removeEventListener('pointerup', onUp);\n track.removeEventListener('pointercancel', onUp);\n setIsDragging(false);\n setDraftOffset(final);\n onChange(final);\n };\n\n track.addEventListener('pointermove', onMove);\n track.addEventListener('pointerup', onUp);\n track.addEventListener('pointercancel', onUp);\n },\n [disabled, cuePoints, fractionToSample, onChange, snapToBeat],\n );\n\n // Reset to 0 (downbeat-aligned) — handy \"snap to bar 1\" button.\n const handleResetToZero = useCallback((): void => {\n if (disabled) return;\n setDraftOffset(0);\n onChange(0);\n }, [disabled, onChange]);\n\n const thumbFraction = sampleToFraction(draftOffset);\n const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;\n\n // BPM mismatch — show a chip when detected BPM diverges from project.\n const bpmMismatch = cuePoints?.detected_bpm != null\n && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;\n\n // Render tick marks for each beat in the snap-target list. Convert\n // sample → fraction → percent for CSS positioning.\n const ticks = useMemo(() => {\n if (!cuePoints) return [];\n const downbeat = cuePoints.beats[0] ?? 0;\n return cuePoints.beats.map((b, i) => {\n const offsetCandidate = b - downbeat;\n const fraction = sampleToFraction(offsetCandidate);\n const isDownbeat = i === 0;\n return { i, fraction, isDownbeat };\n });\n }, [cuePoints, sampleToFraction]);\n\n const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;\n\n return (\n <div data-testid=\"offset-scrubber\" className=\"flex items-center gap-2 w-full\">\n <span className=\"text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0\">\n Align\n </span>\n <div\n ref={trackRef}\n data-testid=\"offset-scrubber-track\"\n onPointerDown={handlePointerDown}\n className={`relative flex-1 min-w-0 rounded-sm select-none ${\n isDisabled\n ? 'bg-sas-panel cursor-not-allowed opacity-40'\n : 'bg-sas-bg cursor-pointer'\n }`}\n style={{ height: SLIDER_HEIGHT_PX }}\n title={\n isDisabled\n ? 'Generate audio first to enable offset alignment'\n : 'Drag to align beat 1. Hold Shift for free, no-snap movement.'\n }\n role=\"slider\"\n aria-label=\"Audio offset alignment\"\n aria-valuemin={-rangeSamples}\n aria-valuemax={rangeSamples}\n aria-valuenow={draftOffset}\n aria-disabled={isDisabled}\n >\n {/* Center marker — bar 1 / beat 1 reference line */}\n <div\n aria-hidden=\"true\"\n className=\"absolute top-0 bottom-0 w-px bg-sas-accent/40\"\n style={{ left: '50%' }}\n />\n {/* Beat ticks */}\n {ticks.map((t) => (\n <div\n key={t.i}\n data-testid={t.isDownbeat ? 'offset-tick-downbeat' : 'offset-tick'}\n aria-hidden=\"true\"\n className={t.isDownbeat ? 'absolute bg-sas-accent' : 'absolute bg-sas-muted/50'}\n style={{\n left: `${(t.fraction * 100).toFixed(2)}%`,\n top: (SLIDER_HEIGHT_PX - (t.isDownbeat ? DOWNBEAT_TICK_HEIGHT_PX : TICK_HEIGHT_PX)) / 2,\n width: 1,\n height: t.isDownbeat ? DOWNBEAT_TICK_HEIGHT_PX : TICK_HEIGHT_PX,\n }}\n />\n ))}\n {/* Thumb */}\n <div\n data-testid=\"offset-scrubber-thumb\"\n aria-hidden=\"true\"\n className={`absolute top-0 bottom-0 rounded-sm ${\n isDragging ? 'bg-sas-accent' : 'bg-sas-accent/80'\n }`}\n style={{\n left: thumbLeftPct,\n width: THUMB_WIDTH_PX,\n transform: 'translateX(-50%)',\n pointerEvents: 'none',\n }}\n />\n </div>\n {/* Numeric readout — samples + millisecond equivalent */}\n <span\n data-testid=\"offset-scrubber-readout\"\n className=\"text-[10px] text-sas-muted/70 tabular-nums flex-shrink-0 min-w-[64px] text-right\"\n >\n {formatOffset(draftOffset, sampleRate)}\n </span>\n {/* Reset button (snap back to 0) */}\n <button\n type=\"button\"\n data-testid=\"offset-scrubber-reset\"\n onClick={handleResetToZero}\n disabled={isDisabled || draftOffset === 0}\n className={`text-[10px] px-1 py-0.5 rounded-sm border transition-colors flex-shrink-0 ${\n isDisabled || draftOffset === 0\n ? 'border-sas-border text-sas-muted/30 cursor-not-allowed'\n : 'border-sas-border text-sas-muted/70 hover:border-sas-accent hover:text-sas-accent'\n }`}\n title=\"Reset offset to 0 (bar 1)\"\n >\n ⌖\n </button>\n {bpmMismatch && (\n <span\n data-testid=\"offset-bpm-mismatch\"\n className=\"text-[9px] px-1 py-0.5 rounded-sm bg-amber-500/15 text-amber-400 border border-amber-500/30 flex-shrink-0\"\n title={`Detected ${detectedBpm.toFixed(1)} BPM — beats may not align with project ${projectBpm} BPM grid`}\n >\n BPM ≠\n </span>\n )}\n </div>\n );\n}\n\n/** Format an offset in samples as `+12345 spl (+279 ms)` for the readout. */\nfunction formatOffset(samples: number, sampleRate: number): string {\n const sign = samples > 0 ? '+' : samples < 0 ? '-' : '';\n const abs = Math.abs(samples);\n const ms = Math.round((abs / sampleRate) * 1000);\n return `${sign}${abs} spl (${sign}${ms} ms)`;\n}\n\nexport default OffsetScrubber;\n","/**\n * WAV peak analyzer (Phase 8.10).\n *\n * Reads a WAV file via the plugin host, decodes it via Web Audio,\n * scans every channel for the absolute maximum sample, and returns\n * peak dBFS + a clipped flag (true when the peak >= -1dBFS, matching\n * the engine's hard-limiter ceiling).\n *\n * Used by the recorder's take rows to surface \"this take peaked at\n * -8dB\" or \"this take CLIPPED\" without the user having to click play.\n */\n\nimport type { PluginHost } from '../types/plugin-sdk.types';\n\nexport interface PeakAnalysis {\n peakLinear: number;\n peakDb: number;\n clipped: boolean;\n}\n\n/** Threshold matching the engine's -1dBFS hard limiter ceiling. */\nconst CLIP_THRESHOLD_LINEAR = 0.891;\n\nexport async function analyzeWavPeak(\n host: PluginHost,\n filePath: string\n): Promise<PeakAnalysis> {\n const bytes = await host.getAudioFileBytes(filePath);\n const ContextCtor: typeof AudioContext =\n (window as unknown as { AudioContext?: typeof AudioContext }).AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext!;\n const audioContext = new ContextCtor();\n try {\n const audioBuffer = await audioContext.decodeAudioData(bytes.slice(0));\n let peak = 0;\n for (let c = 0; c < audioBuffer.numberOfChannels; c++) {\n const data = audioBuffer.getChannelData(c);\n for (let i = 0; i < data.length; i++) {\n const a = Math.abs(data[i]);\n if (a > peak) peak = a;\n }\n }\n const peakDb = peak > 1e-6 ? 20 * Math.log10(peak) : -120;\n return {\n peakLinear: peak,\n peakDb,\n clipped: peak >= CLIP_THRESHOLD_LINEAR - 0.005,\n };\n } finally {\n await audioContext.close().catch(() => { /* ignore */ });\n }\n}\n","/**\n * Synthesize a PluginCuePoints object from raw BPM/sample-rate inputs.\n *\n * The OffsetScrubber consumes PluginCuePoints — a beat grid plus\n * per-beat sample positions, normally produced by Lyria's onset\n * detector. The recorder doesn't have detected cue points (live\n * recordings have no detection pass), but it always knows the project\n * BPM, the engine sample rate, and the loop length in bars. That's\n * enough to construct a synthetic grid where every beat sits on a\n * regular interval — which is exactly what the scrubber needs to\n * provide tick marks + snap behavior for nudging the take's offset.\n */\n\nimport type { PluginCuePoints } from '../types/plugin-sdk.types';\n\nexport interface SynthesizeCuePointsOptions {\n bpm: number;\n sampleRate: number;\n /** Total bars in the clip (e.g. 4 for a 4-bar loop). */\n bars: number;\n /** Beats per bar. Defaults to 4 (4/4). */\n meter?: number;\n}\n\nexport function synthesizeCuePoints({\n bpm,\n sampleRate,\n bars,\n meter = 4,\n}: SynthesizeCuePointsOptions): PluginCuePoints {\n const safeBpm = bpm > 0 ? bpm : 120;\n const safeSampleRate = sampleRate > 0 ? sampleRate : 48000;\n const samplesPerBeat = Math.round((60 / safeBpm) * safeSampleRate);\n const totalBeats = Math.max(1, Math.round(bars * meter));\n const beats: number[] = [];\n for (let i = 0; i < totalBeats; i++) {\n beats.push(i * samplesPerBeat);\n }\n return {\n schema: 1,\n sample_rate: safeSampleRate,\n detected_bpm: safeBpm,\n downbeat_sample: 0,\n beats,\n detected_at: new Date().toISOString(),\n };\n}\n","/**\n * useSceneState — Scene-keyed state hook for plugin developers.\n *\n * Works like `useState`, but maintains separate state per scene.\n * When the user switches scenes, the previous scene's state is preserved\n * and restored when they switch back.\n *\n * Returns `[value, setForCurrentScene, setForScene]`:\n * - `value` — state for the currently active scene\n * - `setForCurrentScene(v)` — updates state for whatever scene is active at call time\n * - `setForScene(sceneId, v)` — updates state for a specific scene (for async callbacks)\n *\n * Both setters support the functional updater pattern: `prev => next`.\n *\n * **Important:** For object/array `initialValue`, hoist to a module-level constant\n * to keep the setter callbacks referentially stable:\n * ```ts\n * const EMPTY: string[] = [];\n * const [items, setItems, setItemsForScene] = useSceneState(activeSceneId, EMPTY);\n * ```\n */\n\nimport { useState, useCallback, useRef } from 'react';\n\ntype SetSceneState<T> = (value: T | ((prev: T) => T)) => void;\ntype SetSceneStateForScene<T> = (sceneId: string, value: T | ((prev: T) => T)) => void;\n\nexport function useSceneState<T>(\n activeSceneId: string | null,\n initialValue: T\n): [T, SetSceneState<T>, SetSceneStateForScene<T>] {\n const [stateMap, setStateMap] = useState<Map<string, T>>(() => new Map());\n const activeSceneIdRef = useRef(activeSceneId);\n activeSceneIdRef.current = activeSceneId;\n\n const currentValue = activeSceneId !== null && stateMap.has(activeSceneId)\n ? stateMap.get(activeSceneId)!\n : initialValue;\n\n const setForCurrentScene = useCallback((value: T | ((prev: T) => T)): void => {\n const sid = activeSceneIdRef.current;\n if (sid === null) return;\n setStateMap(prev => {\n const current = prev.has(sid) ? prev.get(sid)! : initialValue;\n const next = typeof value === 'function' ? (value as (prev: T) => T)(current) : value;\n const newMap = new Map(prev);\n newMap.set(sid, next);\n return newMap;\n });\n }, [initialValue]);\n\n const setForScene = useCallback((sceneId: string, value: T | ((prev: T) => T)): void => {\n setStateMap(prev => {\n const current = prev.has(sceneId) ? prev.get(sceneId)! : initialValue;\n const next = typeof value === 'function' ? (value as (prev: T) => T)(current) : value;\n const newMap = new Map(prev);\n newMap.set(sceneId, next);\n return newMap;\n });\n }, [initialValue]);\n\n return [currentValue, setForCurrentScene, setForScene];\n}\n","/**\n * useAnySolo — reactively reports whether ANY track in the project is soloed.\n *\n * Solo is cross-panel: when the user solos a track in ANY panel, the engine's\n * effective-mute model silences every non-soloed track. A panel uses this flag\n * to DIM its own non-soloed rows without lighting their Mute buttons:\n *\n * ```tsx\n * const anySolo = useAnySolo(host);\n * // ...\n * <TrackRow soloedOut={anySolo && !track.runtimeState.solo} ... />\n * ```\n *\n * Refreshes on mount and on every track-state change. `onTrackStateChange`\n * fires for tracks in ALL panels (not just this plugin's), so a solo toggled in\n * another panel updates this flag too.\n */\n\nimport { useEffect, useState } from 'react';\nimport type { PluginHost } from '../types/plugin-sdk.types';\n\nexport function useAnySolo(\n host: Pick<PluginHost, 'isAnySoloActive' | 'onTrackStateChange'>\n): boolean {\n const [anySolo, setAnySolo] = useState(false);\n\n useEffect(() => {\n let active = true;\n const refresh = (): void => {\n host\n .isAnySoloActive()\n .then((v) => {\n if (active) setAnySolo(v);\n })\n .catch(() => {\n /* engine unreachable — leave the flag as-is rather than flicker */\n });\n };\n refresh();\n const unsub = host.onTrackStateChange(() => refresh());\n return () => {\n active = false;\n unsub();\n };\n }, [host]);\n\n return anySolo;\n}\n","/**\n * useSoundHistory — generic, per-track \"what sounds has this track had?\" stack.\n *\n * Powers the drawer \"History\" tab: restore any earlier sound, star favorites,\n * and (via the host plugin) persist across project reopen. The SDK is ignorant\n * of WHAT a sound is — each plugin records an opaque `descriptor` (a drum sample\n * path / an instrument `{ displayName, zones }` / a synth Surge state blob) plus\n * a human `label`, and supplies `applySound` to re-apply a chosen descriptor.\n *\n * Persistence is the plugin's job: pass `opts.onChange` (called after every\n * mutation with the new state) to save, and call `restore()` on load to seed.\n * Favorited entries are never auto-evicted by the cap.\n *\n * Robustness: `applySound` + `onChange` are read through refs, so the returned\n * object is referentially STABLE regardless of whether the caller memoizes them.\n * Plugins list this object in `loadTracks` deps — an unstable return previously\n * caused a render loop, so keep it stable.\n *\n * @since SDK 2.13.0\n */\n\nimport { useCallback, useMemo, useRef, useState } from 'react';\nimport type { SoundHistoryEntry } from '../types/plugin-sdk.types';\n\nexport type { SoundHistoryEntry };\n\n/** A track's ordered sound history plus the index of the currently-applied sound. */\nexport interface TrackSoundHistory {\n entries: readonly SoundHistoryEntry[];\n /** Index into `entries` of the currently-applied sound; -1 when empty. */\n cursor: number;\n}\n\nexport interface UseSoundHistoryOptions {\n /** Max non-favorited entries kept per track (favorites are never evicted). Default 24. */\n max?: number;\n /**\n * Called after every mutation (record/undo/restoreTo/toggleFavorite/clear) with the\n * track's new state — use it to persist. NOT called by `restore()` (that's a load).\n */\n onChange?: (trackId: string, state: TrackSoundHistory) => void;\n}\n\nexport interface UseSoundHistoryResult {\n /** Remember a sound that was just applied (generation, scene-load, or shuffle). */\n record(trackId: string, descriptor: unknown, label: string): void;\n /** Re-apply the sound one step before the current one. Resolves true if it moved. */\n undo(trackId: string): Promise<boolean>;\n /** Re-apply a specific entry by index. Resolves true if it applied. */\n restoreTo(trackId: string, index: number): Promise<boolean>;\n /** The ordered history + cursor for a track (safe empty default). */\n list(trackId: string): TrackSoundHistory;\n /** Whether there is an earlier sound to step back to. */\n canUndo(trackId: string): boolean;\n /** Forget a track's history (e.g. on regenerate). Persists the cleared state. */\n clear(trackId: string): void;\n /** Forget ALL tracks' history in memory (e.g. before re-seeding on scene load). */\n reset(): void;\n /** Seed a track's full history (e.g. from persistence on load). Does NOT fire onChange. */\n restore(\n trackId: string,\n state: { entries?: readonly SoundHistoryEntry[]; cursor?: number } | null | undefined,\n ): void;\n /** Toggle the favorite flag on an entry (favorites survive cap eviction). */\n toggleFavorite(trackId: string, index: number): void;\n}\n\nconst EMPTY: TrackSoundHistory = { entries: [], cursor: -1 };\n\nfunction sameDescriptor(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n try {\n return JSON.stringify(a) === JSON.stringify(b);\n } catch {\n return false;\n }\n}\n\nexport function useSoundHistory(\n applySound: (trackId: string, descriptor: unknown) => Promise<void>,\n opts: UseSoundHistoryOptions = {},\n): UseSoundHistoryResult {\n const max = Math.max(2, opts.max ?? 24);\n\n // Read callbacks through refs so the returned API stays referentially stable\n // even if the caller passes a fresh closure each render.\n const applyRef = useRef(applySound);\n applyRef.current = applySound;\n const onChangeRef = useRef(opts.onChange);\n onChangeRef.current = opts.onChange;\n\n // Authoritative store in a ref (async callbacks read latest); version forces re-render.\n const dataRef = useRef<Record<string, TrackSoundHistory>>({});\n const [, setVersion] = useState(0);\n const bump = useCallback((): void => setVersion((v) => v + 1), []);\n\n // Single writer: update store, re-render, optionally notify for persistence.\n const commit = useCallback(\n (trackId: string, next: TrackSoundHistory, notify: boolean): void => {\n dataRef.current = { ...dataRef.current, [trackId]: next };\n bump();\n if (notify) onChangeRef.current?.(trackId, next);\n },\n [bump],\n );\n\n const record = useCallback(\n (trackId: string, descriptor: unknown, label: string): void => {\n const h = dataRef.current[trackId];\n const current = h && h.cursor >= 0 ? h.entries[h.cursor] : undefined;\n // Ignore re-applying the same sound (no-op shuffles, scene re-seeds).\n if (current && sameDescriptor(current.descriptor, descriptor)) return;\n const entries: SoundHistoryEntry[] = [...(h ? h.entries : []), { descriptor, label }];\n // Cap: evict the OLDEST NON-FAVORITED entry when over the limit (favorites survive).\n while (entries.length > max) {\n const victim = entries.findIndex((e) => !e.favorite);\n if (victim === -1) break; // everything is favorited — keep it all\n entries.splice(victim, 1);\n }\n commit(trackId, { entries, cursor: entries.length - 1 }, true);\n },\n [max, commit],\n );\n\n const restoreTo = useCallback(\n async (trackId: string, index: number): Promise<boolean> => {\n const h = dataRef.current[trackId];\n if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;\n await applyRef.current(trackId, h.entries[index].descriptor);\n commit(trackId, { entries: h.entries, cursor: index }, true);\n return true;\n },\n [commit],\n );\n\n const undo = useCallback(\n (trackId: string): Promise<boolean> => {\n const h = dataRef.current[trackId];\n if (!h || h.cursor <= 0) return Promise.resolve(false);\n return restoreTo(trackId, h.cursor - 1);\n },\n [restoreTo],\n );\n\n const toggleFavorite = useCallback(\n (trackId: string, index: number): void => {\n const h = dataRef.current[trackId];\n if (!h || index < 0 || index >= h.entries.length) return;\n const entries = h.entries.map((e, i) => (i === index ? { ...e, favorite: !e.favorite } : e));\n commit(trackId, { entries, cursor: h.cursor }, true);\n },\n [commit],\n );\n\n const restore = useCallback(\n (\n trackId: string,\n state: { entries?: readonly SoundHistoryEntry[]; cursor?: number } | null | undefined,\n ): void => {\n const entries: SoundHistoryEntry[] = Array.isArray(state?.entries) ? [...state!.entries] : [];\n const raw = typeof state?.cursor === 'number' ? state!.cursor : entries.length - 1;\n const cursor = entries.length === 0 ? -1 : Math.min(Math.max(raw, 0), entries.length - 1);\n commit(trackId, { entries, cursor }, false);\n },\n [commit],\n );\n\n const list = useCallback(\n (trackId: string): TrackSoundHistory => dataRef.current[trackId] ?? EMPTY,\n [],\n );\n\n const canUndo = useCallback((trackId: string): boolean => {\n const h = dataRef.current[trackId];\n return !!h && h.cursor > 0;\n }, []);\n\n const clear = useCallback(\n (trackId: string): void => {\n if (dataRef.current[trackId]) {\n const next = { ...dataRef.current };\n delete next[trackId];\n dataRef.current = next;\n bump();\n }\n onChangeRef.current?.(trackId, EMPTY); // persist the cleared state\n },\n [bump],\n );\n\n const reset = useCallback((): void => {\n dataRef.current = {};\n bump();\n }, [bump]);\n\n // Stable object so consumers can safely list it in useCallback/useEffect deps.\n return useMemo(\n () => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),\n [record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite],\n );\n}\n","/**\n * useTrackReorder — shared drag-and-drop row reordering for generator panels.\n *\n * One hook drives the whole flow so every panel (drums / instruments / synths)\n * behaves identically: HTML5 drag mechanics (zero dependencies), an optimistic\n * local reorder, persistence via {@link PluginHost.reorderTracks}, and an\n * automatic revert if persistence fails. Panels supply their track array + its\n * setter and spread the returned props onto each {@link TrackRow}; the grip\n * handle and drop-target visuals live in TrackRow.\n *\n * Persisted ids should be STABLE (use `getId: t => t.handle.dbId`) — engine\n * track ids are not stable across project reopen.\n */\nimport { useCallback, useRef, useState } from 'react';\nimport type { DragEvent, Dispatch, SetStateAction } from 'react';\nimport type { PluginHost } from '../types/plugin-sdk.types';\n\n/**\n * Props the reorder machinery hands to a single row. Spread `handleProps` on the\n * drag grip and `rowProps` on the row's outer element; `isDragging` /\n * `isDragTarget` drive the visual state.\n */\nexport interface TrackRowDragProps {\n handleProps: {\n draggable: true;\n onDragStart: (e: DragEvent<HTMLElement>) => void;\n onDragEnd: (e: DragEvent<HTMLElement>) => void;\n };\n rowProps: {\n onDragEnter: (e: DragEvent<HTMLElement>) => void;\n onDragOver: (e: DragEvent<HTMLElement>) => void;\n onDragLeave: (e: DragEvent<HTMLElement>) => void;\n onDrop: (e: DragEvent<HTMLElement>) => void;\n };\n /** This row is the one currently being dragged (dim it). */\n isDragging: boolean;\n /** This row is the current drop target (show an insertion accent). */\n isDragTarget: boolean;\n}\n\n/**\n * Pure helper: return a NEW array with the item at `from` moved to `to`.\n * Out-of-range or no-op moves return a shallow copy unchanged. Exported for\n * unit testing the index math without a DOM.\n */\nexport function moveItem<T>(arr: readonly T[], from: number, to: number): T[] {\n const next = arr.slice();\n if (\n from === to ||\n from < 0 ||\n to < 0 ||\n from >= next.length ||\n to >= next.length\n ) {\n return next;\n }\n const [moved] = next.splice(from, 1);\n next.splice(to, 0, moved);\n return next;\n}\n\nexport interface UseTrackReorderOptions<T> {\n /** Host (only {@link PluginHost.reorderTracks} is used). */\n host: Pick<PluginHost, 'reorderTracks'>;\n /** The panel's current track array (also the render order). */\n items: T[];\n /** The panel's state setter for `items` (used for optimistic update + revert). */\n setItems: Dispatch<SetStateAction<T[]>>;\n /** Stable id for persistence — use the track's dbId, not its engine id. */\n getId: (item: T) => string;\n /** Called if persistence fails, after the optimistic update is reverted. */\n onError?: (err: unknown) => void;\n}\n\nexport interface UseTrackReorderResult {\n /** Build the drag props for the row at `index`; spread onto its TrackRow. */\n dragPropsFor: (index: number) => TrackRowDragProps;\n /** Index of the row being dragged, or null. */\n draggingIndex: number | null;\n /** Index of the current drop-target row, or null. */\n dragOverIndex: number | null;\n}\n\n/**\n * Drag-and-drop reordering for a panel's track list. Dropping a row onto another\n * row moves it into that row's position (everything between shifts); the top and\n * bottom are reachable by dropping on the first/last row.\n */\nexport function useTrackReorder<T>({\n host,\n items,\n setItems,\n getId,\n onError,\n}: UseTrackReorderOptions<T>): UseTrackReorderResult {\n const [draggingIndex, setDraggingIndex] = useState<number | null>(null);\n const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);\n // Source index for the in-flight drag; a ref avoids stale-closure reads in the\n // drop handler. itemsRef keeps the freshest array without re-creating handlers.\n const fromRef = useRef<number | null>(null);\n const itemsRef = useRef(items);\n itemsRef.current = items;\n\n const dragPropsFor = useCallback(\n (index: number): TrackRowDragProps => ({\n handleProps: {\n draggable: true,\n onDragStart: (e) => {\n fromRef.current = index;\n setDraggingIndex(index);\n if (e.dataTransfer) {\n e.dataTransfer.effectAllowed = 'move';\n // Required by Firefox to start a drag; the value itself is unused.\n try {\n e.dataTransfer.setData('text/plain', String(index));\n } catch {\n /* some environments disallow setData — drag still works */\n }\n }\n },\n onDragEnd: () => {\n fromRef.current = null;\n setDraggingIndex(null);\n setDragOverIndex(null);\n },\n },\n rowProps: {\n onDragEnter: (e) => {\n if (fromRef.current === null) return;\n e.preventDefault();\n setDragOverIndex(index);\n },\n onDragOver: (e) => {\n if (fromRef.current === null) return;\n e.preventDefault(); // allow drop\n if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';\n setDragOverIndex((cur) => (cur === index ? cur : index));\n },\n onDragLeave: () => {\n setDragOverIndex((cur) => (cur === index ? null : cur));\n },\n onDrop: (e) => {\n e.preventDefault();\n const from = fromRef.current;\n fromRef.current = null;\n setDraggingIndex(null);\n setDragOverIndex(null);\n if (from === null || from === index) return;\n\n const prev = itemsRef.current;\n const next = moveItem(prev, from, index);\n setItems(next);\n const ids = next.map(getId);\n Promise.resolve(host.reorderTracks(ids)).catch((err) => {\n // Persistence failed — roll back to the pre-drag order.\n setItems(prev);\n onError?.(err);\n });\n },\n },\n isDragging: draggingIndex === index,\n isDragTarget: dragOverIndex === index && draggingIndex !== index,\n }),\n [host, setItems, getId, onError, draggingIndex, dragOverIndex]\n );\n\n return { dragPropsFor, draggingIndex, dragOverIndex };\n}\n","/**\n * Plugin SDK Version\n *\n * Semver version of the plugin SDK contract.\n * Plugins declare minSdkVersion in their manifest.\n * Registry checks semver.gte(PLUGIN_SDK_VERSION, manifest.minHostVersion)\n * during activation and marks incompatible plugins accordingly.\n */\nexport const PLUGIN_SDK_VERSION = '2.28.0';\n","/**\n * Format the cross-plugin concurrent-track context into a prose block\n * that's safe to drop straight into an LLM user-prompt. Both the synth\n * and drum builtin panels use this so the rendered prompt stays\n * consistent across generators — and so a single change here propagates\n * to every plugin that calls `host.getGenerationContext()`.\n *\n * Per-track payload follows the user's preferred shape (raw note JSON\n * grouped by chord) so the model sees velocity / start-beat /\n * duration / pitch verbatim and can reason about feel + harmony.\n *\n * Returns the empty string when there are no concurrent tracks — call\n * sites can `if (block) push(block)` rather than baking in a placeholder.\n */\n\nimport type {\n PluginGenerationContext,\n PluginChordSegment,\n PluginMidiNote,\n} from '../types/plugin-sdk.types';\n\nexport function formatConcurrentTracks(ctx: PluginGenerationContext): string {\n const tracks = ctx.concurrentTracks;\n if (!tracks || tracks.length === 0) return '';\n\n const lines: string[] = [`Concurrent tracks in scene (already generated):`];\n\n for (const track of tracks) {\n const promptStr = track.prompt\n ? ` prompt=\"${escapeQuotes(track.prompt)}\"`\n : '';\n lines.push(` - role=${track.role ?? 'unknown'}${promptStr}`);\n\n if (track.notesByChord.length === 0) {\n lines.push(` (no notes)`);\n } else {\n for (const segment of track.notesByChord) {\n if (segment.notes.length === 0) continue;\n lines.push(` ${formatChordSegment(segment)}`);\n }\n }\n\n if (track.truncated && typeof track.originalNoteCount === 'number') {\n const dropped = track.originalNoteCount - sumKeptNotes(track.notesByChord);\n if (dropped > 0) {\n lines.push(` … (${dropped} more notes truncated)`);\n }\n }\n }\n\n if (ctx.truncatedTrackCount && ctx.truncatedTrackCount > 0) {\n lines.push(\n ` … (${ctx.truncatedTrackCount} additional track${ctx.truncatedTrackCount === 1 ? '' : 's'} omitted to fit token budget)`,\n );\n }\n\n return lines.join('\\n');\n}\n\nfunction formatChordSegment(segment: PluginChordSegment): string {\n const [start, end] = segment.chordRangeQn;\n const notesJson = JSON.stringify(segment.notes.map(compactNote));\n return `${segment.chord} (beats ${start}-${end}): ${notesJson}`;\n}\n\n/**\n * Strip channel and other rarely-relevant fields so the LLM sees only\n * the four properties that drive perception: pitch, startBeat,\n * durationBeats, velocity.\n */\nfunction compactNote(n: PluginMidiNote): {\n pitch: number;\n startBeat: number;\n durationBeats: number;\n velocity: number;\n} {\n return {\n pitch: n.pitch,\n startBeat: n.startBeat,\n durationBeats: n.durationBeats,\n velocity: n.velocity,\n };\n}\n\nfunction escapeQuotes(s: string): string {\n return s.replace(/\"/g, '\\\\\"');\n}\n\nfunction sumKeptNotes(segments: PluginChordSegment[]): number {\n let total = 0;\n for (const s of segments) total += s.notes.length;\n return total;\n}\n","/**\n * Lightweight, dependency-free semantic matching for sample selection.\n *\n * Sample generators (drums, instruments) ship a short StableAudio text\n * prompt next to every sample (\"tight 909-style kick one shot, hard click\n * transient, short punchy body, dry, no hi hats, no loop\"). When the user\n * asks for \"a 1950s style boom bap kick\" we want to pick the sample whose\n * prompt is closest to that intent — instead of a uniform random draw —\n * while still preserving variety so a vague \"give me a kick\" doesn't return\n * the identical sample every time.\n *\n * Design notes:\n * - Pure functions, no I/O, no SDK-type dependencies → trivially unit\n * testable with an injected `rng`, and safe to call from either the\n * main or renderer process.\n * - Scoring is IDF-weighted query-coverage (a TF-IDF / BM25-lite). The\n * IDF is derived from the candidate pool itself, so it is STRUCTURAL —\n * no hand-maintained synonym tables. Rare, discriminating tokens in the\n * prompts (\"909\", \"dusty\", \"tube\") dominate; corpus-universal filler\n * (\"one\", \"shot\", \"dry\") washes out to ~zero IDF on its own.\n * - The near-universal negative clauses StableAudio prompts carry\n * (\"no hi hats\", \"no loop\", \"no melody\") are stripped before tokenizing;\n * they are pure noise for matching.\n * - Selection is softmax-weighted random among the top-k. Flat scores →\n * ~uniform (≈ the old random behavior); a clear winner → tight\n * convergence. The all-zero (no-signal) case is intentionally left to\n * the caller to fall back to its existing random path over the full\n * pool — see `scorePromptMatch`'s contract below.\n */\n\n/**\n * Function words + a few imperative-request fillers that should never count\n * as matchable intent. Kept deliberately SMALL — IDF already neutralizes\n * corpus-universal words, and query tokens that appear in no candidate are\n * dropped during scoring, so this list only needs the words that would\n * otherwise be both query-frequent AND coincidentally present in prompts.\n */\nconst STOP_WORDS: ReadonlySet<string> = new Set([\n 'a', 'an', 'the', 'and', 'or', 'but', 'with', 'for', 'to', 'of', 'in', 'on',\n 'at', 'by', 'is', 'it', 'this', 'that', 'i', 'my', 'me', 'make', 'please',\n 'give', 'want', 'need', 'some', 'like', 'get', 'something',\n]);\n\n/**\n * Tokenize a prompt or query into matchable lowercase tokens.\n *\n * 1. Drop comma-delimited negative clauses (\"no hi hats\", \"no loop\").\n * 2. Lowercase, split on any non-alphanumeric run.\n * 3. Drop stop-words and 1–2 digit numeric noise (\"01\", \"02\") while\n * keeping meaningful numerics (\"808\", \"909\", \"1950\").\n */\nexport function tokenizePrompt(text: string): string[] {\n if (!text) return [];\n const withoutNegatives = text\n .split(',')\n .map((clause) => clause.trim())\n .filter((clause) => clause.length > 0 && !/^no\\s/i.test(clause))\n .join(' ');\n\n return withoutNegatives\n .toLowerCase()\n .split(/[^a-z0-9]+/u)\n .filter((tok) => {\n if (!tok) return false;\n if (STOP_WORDS.has(tok)) return false;\n if (/^\\d{1,2}$/.test(tok)) return false; // \"01\", \"02\" — sequence noise\n return true;\n });\n}\n\n/**\n * Score each candidate prompt against the query, returning a parallel array\n * of scores in [0, 1] (1 = the candidate covers all of the query's\n * discriminating intent).\n *\n * Contract: a returned max of 0 means the query shares NO matchable token\n * with any candidate (no signal). Callers should treat that as \"fall back to\n * the existing uniform-random pick over the full pool\" so vague queries keep\n * today's variety rather than biasing toward an arbitrary top-k slice.\n */\nexport function scorePromptMatch(\n query: string,\n candidatePrompts: ReadonlyArray<string>,\n): number[] {\n const n = candidatePrompts.length;\n if (n === 0) return [];\n\n const queryTokens = Array.from(new Set(tokenizePrompt(query)));\n if (queryTokens.length === 0) return candidatePrompts.map(() => 0);\n\n const candidateTokenSets = candidatePrompts.map((p) => new Set(tokenizePrompt(p)));\n\n // IDF for each query token, derived from the candidate pool. Tokens that\n // appear in no candidate are unmatchable → excluded from both the score\n // numerator and the normalization denominator.\n const idf = new Map<string, number>();\n for (const token of queryTokens) {\n let df = 0;\n for (const set of candidateTokenSets) {\n if (set.has(token)) df += 1;\n }\n if (df > 0) idf.set(token, Math.log(1 + n / df));\n }\n\n let denominator = 0;\n for (const weight of idf.values()) denominator += weight;\n if (denominator === 0) return candidatePrompts.map(() => 0);\n\n return candidateTokenSets.map((set) => {\n let numerator = 0;\n for (const [token, weight] of idf) {\n if (set.has(token)) numerator += weight;\n }\n return numerator / denominator;\n });\n}\n\n/** One scored candidate. `key` (if present) is what `excludeKeys` matches on. */\nexport interface ScoredCandidate<T> {\n item: T;\n score: number;\n key?: string;\n}\n\nexport interface PickTopKOptions {\n /** Consider only the top-k by score (default 5). */\n k?: number;\n /**\n * Softmax temperature (default 0.3). Lower → sharper preference for the\n * top match; higher → flatter (more variety). Scores are in [0, 1].\n */\n temperature?: number;\n /** Candidate keys to exclude (e.g. shuffle history). */\n excludeKeys?: ReadonlySet<string>;\n /** Injectable RNG in [0, 1) for deterministic tests (default Math.random). */\n rng?: () => number;\n}\n\n/**\n * Pick one candidate via softmax-weighted random selection among the top-k\n * by score. Returns null only when the pool is empty after exclusion.\n *\n * Equal scores → equal weights → uniform pick among the top-k, so this\n * degrades gracefully toward random when the query gives no preference.\n */\nexport function pickTopKWeighted<T>(\n scored: ReadonlyArray<ScoredCandidate<T>>,\n options: PickTopKOptions = {},\n): T | null {\n const { k = 5, temperature = 0.3, excludeKeys, rng = Math.random } = options;\n\n let pool = scored;\n if (excludeKeys && excludeKeys.size > 0) {\n pool = pool.filter((c) => c.key === undefined || !excludeKeys.has(c.key));\n }\n if (pool.length === 0) return null;\n\n const sorted = [...pool].sort((a, b) => b.score - a.score);\n const top = sorted.slice(0, Math.max(1, k));\n\n // Softmax with a max-subtraction for numerical stability.\n const maxScore = top[0].score;\n const safeTemp = Math.max(1e-6, temperature);\n const weights = top.map((c) => Math.exp((c.score - maxScore) / safeTemp));\n const totalWeight = weights.reduce((sum, w) => sum + w, 0);\n\n let threshold = rng() * totalWeight;\n for (let i = 0; i < top.length; i += 1) {\n threshold -= weights[i];\n if (threshold <= 0) return top[i].item;\n }\n return top[top.length - 1].item;\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime","next","import_jsx_runtime","import_react","import_react","import_jsx_runtime","import_jsx_runtime","import_jsx_runtime","import_react","import_jsx_runtime","import_react","import_jsx_runtime","import_react","import_jsx_runtime","useDebouncedCallback","import_react","import_jsx_runtime","import_jsx_runtime","React","import_react","import_jsx_runtime","React","import_react","import_jsx_runtime","React","import_react","import_jsx_runtime","import_react","import_jsx_runtime","scenes","import_react","import_jsx_runtime","shortId","import_react","import_jsx_runtime","import_jsx_runtime","import_react","import_jsx_runtime","import_react","import_jsx_runtime","import_react","import_jsx_runtime","import_react","import_react","import_react","import_react","compactNote"]}
|