# JamTrack React Component Design

Comprehensive component architecture for JamTrack functionality in jam-ui, following established Backing Track patterns with extensions for JamTrack-specific complexity.

## 1. JKSessionJamTrackPlayer Component (NEW - Primary Deliverable)

### Purpose
Full-featured JamTrack player with playback controls, mixdown selection, download/sync progress, and error handling.

### Rendering Modes
- **Modal mode**: Player rendered in modal dialog (default)
- **Popup mode**: Player rendered in separate popup window (follows Backing Track pattern)

### Props Interface

```typescript
interface JKSessionJamTrackPlayerProps {
  // Visibility control
  isOpen: boolean;
  onClose: () => void;
  isPopup?: boolean;  // Default: false

  // JamTrack data
  jamTrack: JamTrack | string;  // Object (modal) or fqId string (popup)

  // Dependencies (passed as props, not Redux)
  jamClient: JamClient;  // Non-serializable
  session: Session;
  currentUser: User;

  // Optional configuration
  initialMixdownId?: number;  // Pre-select specific mixdown
  autoPlay?: boolean;         // Auto-play on load (default: false)
}

interface JamTrack {
  id: number;
  name: string;
  artist: string;
  jmep?: JMEPData;  // Tempo/pitch modifications
  mixdowns: Mixdown[];
  albumArt?: string;
}

interface JMEPData {
  tempo: number;   // BPM or percentage
  pitch: number;   // Semitones shift
}

interface Mixdown {
  id: number;
  name: string;
  type: 'master' | 'custom' | 'stem';
  packages: MixdownPackage[];
}

interface MixdownPackage {
  id: number;
  file_type: 'ogg';
  encrypt_type: 'jkz';
  sample_rate: 44 | 48;
  signing_state: 'SIGNING_TIMEOUT' | 'QUEUED_TIMEOUT' | 'QUIET_TIMEOUT' | 'ERROR' | 'SIGNED';
  packaging_steps?: number;
  current_packaging_step?: number;
}
```

### State Management

**Local State (Component-specific UI):**
```javascript
// Playback state (mirrors jamClient, updated via polling)
const [isPlaying, setIsPlaying] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const [currentPositionMs, setCurrentPositionMs] = useState(0);
const [durationMs, setDurationMs] = useState(0);
const [currentTime, setCurrentTime] = useState('0:00');
const [duration, setDuration] = useState('0:00');

// Mixdown selection
const [selectedMixdownId, setSelectedMixdownId] = useState(null);
const [availableMixdowns, setAvailableMixdowns] = useState([]);

// Error handling
const [error, setError] = useState(null);
const [errorType, setErrorType] = useState(null); // 'file' | 'network' | 'playback' | 'sync' | 'general'

// Loading states
const [isLoadingDuration, setIsLoadingDuration] = useState(true);
const [isOperating, setIsOperating] = useState(false); // Prevent rapid clicks
const [isSyncing, setIsSyncing] = useState(false);     // Download/keying in progress

// Performance optimization
const [isVisible, setIsVisible] = useState(true);  // Tab visibility

// Refs (non-reactive state)
const pendingSeekPositionRef = useRef(null);           // UAT-003 fix (seek while paused)
const consecutivePollingErrorsRef = useRef(0);         // Network resilience
const pollingIntervalRef = useRef(null);               // Cleanup tracking
```

**Redux State (Shared across components):**
```javascript
// From mediaSlice
const jamTrackState = useSelector(selectJamTrackState);  // Real-time WebSocket updates
const downloadState = useSelector(selectDownloadState);  // Sync progress

// From activeSessionSlice
const selectedJamTrack = useSelector(selectSelectedJamTrack);  // Full JamTrack object
const jamTrackStems = useSelector(selectJamTrackStems);        // After load

// From sessionUISlice
const jamTrackPlayerOpen = useSelector(selectJamTrackPlayerOpen);
```

### Sub-Components

#### 1. MixdownSelector
```jsx
<MixdownSelector
  mixdowns={availableMixdowns}
  selectedMixdownId={selectedMixdownId}
  onSelect={handleMixdownChange}
  disabled={isOperating || isSyncing}
/>
```

**Features:**
- Dropdown showing: Master, Custom Mixes, Individual Stems
- Visual indicator for currently playing mixdown
- Disabled during operations to prevent mid-playback switches
- Groups mixdowns by type (Master / Custom / Stems)

#### 2. PlaybackControls
```jsx
<PlaybackControls
  isPlaying={isPlaying}
  isPaused={isPaused}
  onPlay={handlePlay}
  onPause={handlePause}
  onStop={handleStop}
  disabled={isOperating || isSyncing || !!error}
/>
```

**Features:**
- Play/pause/stop buttons (FontAwesome icons: faPlay, faPause, faStop)
- Disabled states during loading/error
- Visual feedback for current playback state
- Follows Backing Track button styling

#### 3. SeekBar
```jsx
<SeekBar
  currentPositionMs={currentPositionMs}
  durationMs={durationMs}
  onSeek={handleSeek}
  disabled={isOperating || !durationMs || !!error}
/>
```

**Features:**
- Range slider (0 to durationMs)
- Drag-to-position with preview tooltip
- Time display: current / duration (MM:SS format)
- UAT-003 fix: Pending seek while paused pattern
- Visual progress bar

#### 4. DownloadSyncProgress
```jsx
<DownloadSyncProgress
  downloadState={downloadState}
  onRetry={handleRetryDownload}
  onCancel={handleCancelDownload}
/>
```

**Features:**
- Progress bar for download (0-100%)
- State indicators: Checking, Downloading, Keying, Synchronized
- Current step display: "Downloading... 45%" or "Requesting encryption keys..."
- Cancel button for long downloads
- Error state with retry button

#### 5. ErrorBanner
```jsx
<ErrorBanner
  error={error}
  errorType={errorType}
  onDismiss={clearError}
  onRetry={retryOperation}
/>
```

**Features:**
- Color-coded by type: Red (file/network/sync), Yellow (playback/general)
- Dismiss button (×)
- Retry button (only for file/network/sync errors)
- Icon indicators (⚠️)
- Follows Backing Track error styling

### Component Lifecycle

```javascript
useEffect(() => {
  // 1. Initialize on open
  if ((isOpen || isPopup) && jamTrack && jamClient) {
    initializePlayer();
  }

  // 2. Cleanup on close/unmount
  return () => {
    if (jamClient) {
      jamClient.JamTrackStopPlay();
      clearPolling();
    }
  };
}, [isOpen, isPopup, jamTrack, jamClient]);

const initializePlayer = async () => {
  try {
    // Reset state
    setIsPlaying(false);
    setError(null);
    setIsLoadingDuration(true);

    // Build fqId
    const sampleRate = await jamClient.GetSampleRate(); // 44 or 48
    const sampleRateStr = sampleRate === 48 ? '48' : '44';
    const fqId = `${jamTrack.id}-${sampleRateStr}`;

    // Check sync state
    const trackDetail = await jamClient.JamTrackGetTrackDetail(fqId);
    if (!trackDetail.key_state || trackDetail.key_state !== 'AVAILABLE') {
      // Not synchronized - show download UI
      setIsSyncing(true);
      await initiateDownload(jamTrack, fqId);
      return;
    }

    // Load JMEP if present
    if (jamTrack.jmep) {
      await jamClient.JamTrackLoadJmep(fqId, jamTrack.jmep);
    }

    // Load JamTrack
    const result = await jamClient.JamTrackPlay(fqId);
    if (!result) {
      throw new Error('Failed to load JamTrack');
    }

    // Fetch duration
    const durationMs = parseInt(await jamClient.SessionGetJamTracksPlayDurationMs());
    setDurationMs(durationMs);
    setDuration(formatTime(durationMs));
    setIsLoadingDuration(false);

    // Start polling
    startPolling();

  } catch (err) {
    handleError('file', 'Failed to load JamTrack', err);
    setIsLoadingDuration(false);
  }
};
```

### Playback Controls Implementation

```javascript
const handlePlay = useCallback(async () => {
  if (!jamClient || isOperating) return;

  try {
    setIsOperating(true);
    clearError();

    // Apply pending seek if paused (UAT-003 fix)
    if (isPaused && pendingSeekPositionRef.current !== null) {
      await jamClient.SessionStopPlay();
      await jamClient.SessionJamTrackSeekMs(pendingSeekPositionRef.current);
      pendingSeekPositionRef.current = null;
    }

    await jamClient.SessionStartPlay();
    setIsPlaying(true);
    setIsPaused(false);
    startPolling();

  } catch (err) {
    handleError('playback', 'Failed to start playback', err);
  } finally {
    setIsOperating(false);
  }
}, [jamClient, isOperating, isPaused]);

const handlePause = useCallback(async () => {
  if (!jamClient || isOperating) return;

  try {
    setIsOperating(true);
    await jamClient.SessionPausePlay();
    setIsPlaying(false);
    setIsPaused(true);
    stopPolling();
  } catch (err) {
    handleError('playback', 'Failed to pause playback', err);
  } finally {
    setIsOperating(false);
  }
}, [jamClient, isOperating]);

const handleStop = useCallback(async () => {
  if (!jamClient || isOperating) return;

  try {
    setIsOperating(true);
    await jamClient.SessionStopPlay();
    setIsPlaying(false);
    setIsPaused(false);
    setCurrentPositionMs(0);
    setCurrentTime('0:00');
    pendingSeekPositionRef.current = null;
    stopPolling();
  } catch (err) {
    handleError('playback', 'Failed to stop playback', err);
  } finally {
    setIsOperating(false);
  }
}, [jamClient, isOperating]);

const handleSeek = useCallback(async (positionMs) => {
  if (!jamClient) return;

  try {
    await jamClient.SessionJamTrackSeekMs(positionMs);
    setCurrentPositionMs(positionMs);
    setCurrentTime(formatTime(positionMs));

    // UAT-003: Verify seek applied
    if (isPaused) {
      const actualPosition = parseInt(await jamClient.SessionCurrrentJamTrackPlayPosMs());
      if (Math.abs(actualPosition - positionMs) > 100) {
        // Seek didn't apply while paused - store for resume
        pendingSeekPositionRef.current = positionMs;
      }
    }
  } catch (err) {
    handleError('playback', 'Failed to seek', err);
  }
}, [jamClient, isPaused, formatTime]);
```

### Polling Pattern (Performance Optimized)

```javascript
const startPolling = useCallback(() => {
  stopPolling(); // Clear any existing interval

  const poll = async () => {
    if (!jamClient) return;

    try {
      // Check if still playing
      const playing = await jamClient.JamTrackIsPlaying();
      if (!playing) {
        setIsPlaying(false);
        stopPolling();
        return;
      }

      // Get current position
      const position = parseInt(await jamClient.SessionCurrrentJamTrackPlayPosMs());

      // Only update if changed (lazy state update)
      if (position !== currentPositionMs) {
        setCurrentPositionMs(position);
        setCurrentTime(formatTime(position));
      }

      // Check if reached end of track
      if (durationMs > 0 && position >= durationMs - 100) {
        // End of track - stop playback
        await jamClient.SessionStopPlay();
        setIsPlaying(false);
        setCurrentPositionMs(0);
        setCurrentTime('0:00');
        stopPolling();
      }

      // Reset error counter on success
      consecutivePollingErrorsRef.current = 0;

    } catch (err) {
      consecutivePollingErrorsRef.current += 1;

      if (consecutivePollingErrorsRef.current >= 3) {
        handleError('network', 'Lost connection to jamClient', err);
        stopPolling();
      }
    }
  };

  // Visibility-aware polling: 500ms visible, 2000ms hidden
  const interval = isVisible ? 500 : 2000;
  pollingIntervalRef.current = setInterval(poll, interval);
}, [jamClient, currentPositionMs, durationMs, isVisible, formatTime]);

const stopPolling = useCallback(() => {
  if (pollingIntervalRef.current) {
    clearInterval(pollingIntervalRef.current);
    pollingIntervalRef.current = null;
  }
}, []);

// Update polling interval on visibility change
useEffect(() => {
  if (isPlaying && pollingIntervalRef.current) {
    startPolling(); // Restart with new interval
  }
}, [isVisible, isPlaying]);
```

### Mixdown Selection

```javascript
const handleMixdownChange = useCallback(async (mixdownId) => {
  if (!jamClient || isOperating) return;

  try {
    setIsOperating(true);
    const wasPlaying = isPlaying;

    // Stop current playback
    if (wasPlaying) {
      await jamClient.SessionStopPlay();
      setIsPlaying(false);
    }

    // Unload current JamTrack
    await jamClient.JamTrackStopPlay();

    // Update selected mixdown in Redux
    dispatch(setActiveMixdown(mixdownId));
    setSelectedMixdownId(mixdownId);

    // Build fqId
    const sampleRate = await jamClient.GetSampleRate();
    const sampleRateStr = sampleRate === 48 ? '48' : '44';
    const fqId = `${jamTrack.id}-${sampleRateStr}`;

    // Reload JamTrack with new mixdown
    const result = await jamClient.JamTrackPlay(fqId);
    if (!result) {
      throw new Error('Failed to load JamTrack with new mixdown');
    }

    // Resume playback if was playing
    if (wasPlaying) {
      await jamClient.SessionStartPlay();
      setIsPlaying(true);
      startPolling();
    }

  } catch (err) {
    handleError('playback', 'Failed to switch mixdown', err);
  } finally {
    setIsOperating(false);
  }
}, [jamClient, isOperating, isPlaying, jamTrack, dispatch]);
```

## 2. JKSessionJamTrackModal Component (EXISTS - Minor Updates)

### Current State
- ✓ Functional selection UI with search/autocomplete
- ✓ Pagination (10 items per page)
- ✓ REST API integration (`getPurchasedJamTracks`)

### Potential Updates for Phase 5

**Add Download State Indicators:**
```jsx
<JamTrackListItem
  jamTrack={jamTrack}
  onSelect={handleSelect}
  downloadState={getDownloadState(jamTrack.id)}  // NEW
/>

// Show sync status icon
{downloadState === 'synchronized' && <CheckIcon />}
{downloadState === 'downloading' && <DownloadIcon />}
{downloadState === 'error' && <ErrorIcon />}
```

**Add Mixdown Preview:**
```jsx
<MixdownPreview
  jamTrack={jamTrack}
  availableMixdowns={jamTrack.mixdowns}
  onMixdownSelect={handleMixdownPreview}
/>
```

**Integration Pattern:**
```jsx
const handleJamTrackSelect = useCallback((jamTrack) => {
  // Close modal
  toggle();

  // Dispatch loadJamTrack thunk
  dispatch(loadJamTrackThunk({
    jamTrack,
    mixdownId: selectedMixdownId,  // If pre-selected in preview
    jamClient
  }));
}, [toggle, dispatch, jamClient, selectedMixdownId]);
```

## 3. JKSessionJamTrackStems Component (EXISTS - Verify Completeness)

### Purpose
Display JamTrack stem tracks in session mixer view with individual controls.

### Expected Structure
```jsx
const JKSessionJamTrackStems = ({ jamTrackStems, mixerHelper }) => {
  return (
    <div className="jamtrack-stems-container">
      {jamTrackStems.map(stem => (
        <SessionTrack key={stem.track.id}>
          <SessionTrackVU mixer={stem.mixer} />
          <SessionTrackGain
            mixer={stem.mixer}
            onVolumeChange={handleVolumeChange}
            onPanChange={handlePanChange}
          />
          <SessionTrackControls
            mixer={stem.mixer}
            onMute={handleMute}
            onSolo={handleSolo}
          />
        </SessionTrack>
      ))}
    </div>
  );
};
```

### Verification Needed
- [ ] Supports all mixer operations (volume, pan, mute, solo)
- [ ] Handles stem track updates from WebSocket MIXER_CHANGES
- [ ] Visual indication of active stems
- [ ] Integration with useMixerHelper hook

## 4. Component Hierarchy

```
JKSessionScreen
├── Header
│   └── Open Menu
│       └── "Open JamTrack" → dispatch(setShowJamTrackModal(true))
│
├── JKSessionJamTrackModal (selection)
│   ├── JKJamTracksAutoComplete (search)
│   ├── JamTrackList (purchased tracks)
│   └── onJamTrackSelect → dispatch(loadJamTrack())
│
├── JKSessionJamTrackPlayer (playback - NEW)
│   ├── MixdownSelector
│   │   └── Dropdown (Master / Custom Mixes / Stems)
│   ├── DownloadSyncProgress
│   │   ├── Progress bar
│   │   ├── State indicator
│   │   └── Cancel/Retry buttons
│   ├── PlaybackControls
│   │   ├── Play button
│   │   ├── Pause button
│   │   └── Stop button
│   ├── SeekBar
│   │   ├── Range slider
│   │   └── Time display (current / duration)
│   └── ErrorBanner
│       ├── Error message
│       ├── Dismiss button
│       └── Retry button (conditional)
│
└── Mixer Section
    └── JKSessionJamTrackStems (stems view)
        └── SessionTrack[] (per stem)
            ├── SessionTrackVU (VU meter)
            ├── SessionTrackGain (volume/pan)
            └── SessionTrackControls (mute/solo)
```

## 5. Props vs Redux Decision Matrix

| Data | Storage | Rationale |
|------|---------|-----------|
| **jamClient** | Props | Non-serializable, can't be in Redux |
| **session** | Props | Parent context, passed down |
| **currentUser** | Props | Parent context, passed down |
| **isOpen** | Props | Parent controls visibility |
| **isPopup** | Props | Parent determines render mode |
| **jamTrack** | Redux (`activeSession.selectedJamTrack`) | WebSocket updates, shared state |
| **jamTrackState** | Redux (`media.jamTrackState`) | Real-time playback state from WebSocket |
| **downloadState** | Redux (`media.downloadState`) | Shared across components (modal + player) |
| **jamTrackStems** | Redux (`activeSession.jamTrackStems`) | Loaded after JamTrackPlay, used in mixer |
| **availableMixdowns** | Redux (`activeSession.availableMixdowns`) | User's custom mixes, persisted |
| **selectedMixdownId** | Local state | Player-specific UI state |
| **error** | Local state | Player-specific errors |
| **isPlaying** | Local state | Mirrors jamClient, updated via polling |
| **currentPositionMs** | Local state | High-frequency updates, no global need |

**Reasoning:**
- **Props for context and control:** Non-serializable objects and parent-controlled visibility
- **Redux for shared data:** WebSocket updates, multi-component access
- **Local state for UI:** High-frequency polling data, component-specific errors

## 6. Comparison to Backing Track Player

| Feature | Backing Track | JamTrack | Notes |
|---------|---------------|----------|-------|
| **File selection** | Native dialog (`ShowSelectBackingTrackDialog`) | React modal (`JKSessionJamTrackModal`) | JamTrack: existing modal ready |
| **Load API** | `SessionOpenBackingTrackFile(path)` | `JamTrackPlay(fqId)` | JamTrack: requires fqId format |
| **Before load** | Immediate | Check sync state, download if needed | JamTrack: async download/keying flow |
| **JMEP support** | No | Yes (`JamTrackLoadJmep` before play) | JamTrack: tempo/pitch modifications |
| **Mixdown selection** | N/A | Dropdown selector (master/custom/stems) | JamTrack: NEW feature |
| **Playback controls** | play/pause/stop/seek | Same + mixdown switcher | JamTrack: additional dropdown |
| **Duration loading** | Single async call | Same (`SessionGetJamTracksPlayDurationMs`) | Same pattern |
| **Position polling** | `SessionCurrentPlayPosMs()` | `SessionCurrrentJamTrackPlayPosMs()` | Similar APIs |
| **Seek API** | `SessionTrackSeekMs(ms)` | `SessionJamTrackSeekMs(ms)` | Similar APIs |
| **Volume control** | `SessionSetTrackVolumeData(mixer, vol)` | Same (per stem) | Same mixer integration |
| **Loop control** | `SessionSetBackingTrackFileLoop(path, bool)` | TBD if supported | JamTrack: API unclear |
| **Error types** | file, network, playback, general | Same + download, sync | JamTrack: +2 error types |
| **Loading states** | isLoadingDuration, isOperating | Same + isSyncing | JamTrack: +1 loading state |
| **Performance** | Visibility-aware polling (500ms/2s) | Same pattern | Apply Phase 3 optimizations |
| **UAT-003 fix** | Pending seek while paused | Same pattern | Apply Phase 3 fix |
| **Cleanup** | Stop on unmount | Stop JamTrack on unmount | Same pattern |
| **Modal/Popup modes** | Both supported | Both supported | Apply Phase 3 fixes |

**Key Differences:**
1. **Mixdown selection:** JamTrack has dropdown, Backing Track does not
2. **Download/sync flow:** JamTrack requires async download, Backing Track is immediate
3. **JMEP loading:** JamTrack supports tempo/pitch, Backing Track does not
4. **Error types:** JamTrack adds download/sync errors
5. **API surface:** JamTrack has 15+ methods vs Backing Track's 8 methods

**Reusable Patterns from Phase 3:**
- ✓ Error banner with type-based colors and retry
- ✓ Loading states (isLoadingDuration, isOperating)
- ✓ Visibility-aware polling (500ms visible, 2s hidden)
- ✓ useCallback for all handlers (prevent re-renders)
- ✓ Conditional state updates (only update if changed)
- ✓ Consecutive error tracking with refs
- ✓ UAT-003 fix (pending seek while paused)
- ✓ Cleanup on unmount (stop playback)
- ✓ Modal and popup mode support

---

## Summary

**Primary Component:** JKSessionJamTrackPlayer (NEW)
- Based on JKSessionBackingTrackPlayer.js with extensions
- Adds: Mixdown selector, download/sync progress, JMEP support
- Reuses: Playback controls, seek bar, error handling, polling patterns

**Supporting Components:**
- JKSessionJamTrackModal (existing, minor updates)
- JKSessionJamTrackStems (existing, verify completeness)

**Design Principles:**
1. Follow Phase 3 Backing Track patterns wherever possible
2. Handle JamTrack-specific complexity (mixdowns, download, JMEP) with clear separation
3. Props for control, Redux for shared data, local state for UI
4. Performance optimizations from Phase 3 (visibility-aware polling, useCallback, lazy updates)
5. Type-specific error handling with retry capabilities
6. Comprehensive loading states for all async operations
