# TV App - AI Architecture Rules

## Overview
Multi-provider video player application supporting YouTube (with future Vimeo/Rumble support).
Uses React for UI, custom state machine for coordination, and localStorage for progress persistence.

## Global Component Boundaries

### Provider Wrappers (YouTube.js, future: Vimeo.js, Rumble.js)
**Purpose:** Simple API wrappers for video providers
**Responsibilities:**
- Load and configure provider's iframe API
- Expose standardized methods: play(), pause(), seekTo(), getCurrentTime(), getDuration(), mute(), unmute()
- Emit events for state changes (onReady, onStateChange, onError)
- Load videos with startSeconds parameter
- Track user mute preference (userHasUnmutedRef)

**Limits:**
- ❌ Must NOT calculate resume positions (Player.js handles this)
- ❌ Must NOT manage clip boundaries (startTs/endTs) (Player.js handles this)
- ❌ Must NOT directly write to localStorage for progress (Player.js handles this)
- ❌ Must NOT contain business logic beyond provider API interaction
- ✅ Should ONLY wrap provider-specific API calls
- ✅ Can subscribe to tvStateMachine for mute/unmute events only
- ✅ Can use tvStateMachine.transition() to notify of player state changes

### Player.js (Playback Coordinator)
**Purpose:** Coordinates playback between state machine and provider wrappers
**Responsibilities:**
- Calculate resume positions from localStorage progress
- Manage clip boundaries (startTs/endTs) for video segments
- Poll player for current time and update progress in localStorage
- Auto-advance when reaching endTs
- Expose seekTo() and getCurrentTime() via ref
- Pass calculated startSeconds to provider wrapper

**Limits:**
- ❌ Must NOT contain provider-specific API calls (use wrappers)
- ❌ Must NOT render UI (returns provider component only)
- ❌ Must NOT handle keyboard/mouse events (Controls.js handles this)
- ✅ Should ONLY coordinate between state machine and provider wrapper
- ✅ Should calculate progress as percentage of clip length (not full video)
- ✅ Should delay progress updates 3 seconds after load to prevent overwriting resume position

### ChannelItems.js (Playlist UI)
**Purpose:** Display channel playlist with progress tracking and item selection
**Responsibilities:**
- Render playlist items with thumbnails
- Display progress bars synced from localStorage
- Handle item selection clicks
- Handle progress bar clicks for seeking
- Poll localStorage every 1000ms to update progress display
- Reset progress button for individual items

**Limits:**
- ❌ Must NOT handle video playback logic (Player.js does this)
- ❌ Must NOT call YouTube/provider APIs directly (use tvRef methods)
- ❌ Must NOT calculate resume positions (Player.js does this)
- ❌ Must NOT modify tvStateMachine state (read-only except via exported hooks)
- ✅ Should ONLY handle UI rendering and user interactions
- ✅ Progress bar clicks call tvRef.players.youtube.seekTo() or set desiredStartTimeRef
- ✅ Exports custom hooks: useChannelApi, useChannelStateSync, useChannelFetch

### TvContainer.js (Main Application Container)
**Purpose:** Root component coordinating all UI and state
**Responsibilities:**
- Initialize and manage channel/channels state
- Coordinate Player, ChannelItems, ChannelList, Controls components
- Handle fullscreen API integration
- Manage lower third overlays
- Subscribe to tvStateMachine for UI updates
- Handle video end events and auto-advance logic

**Limits:**
- ❌ Must NOT contain playback logic (delegate to Player.js)
- ❌ Must NOT contain provider API calls (delegate to wrappers)
- ✅ Should ONLY coordinate between components and state machine
- ✅ Can call tvStateMachine.transition() for high-level actions

### Controls.js (Playback Controls UI)
**Purpose:** Play/pause, mute/unmute, fullscreen buttons and keyboard shortcuts
**Responsibilities:**
- Render control buttons
- Handle keyboard shortcuts via Keys component
- Call tvRef methods (playVideo, pauseVideo, mute, unMute)
- Trigger tvStateMachine transitions
- Persist mute preference to localStorage

**Limits:**
- ❌ Must NOT handle progress tracking (Player.js does this)
- ❌ Must NOT handle item selection (ChannelItems does this)
- ✅ Should ONLY handle play/pause/mute/fullscreen controls
- ✅ Accesses tvRef.defaultPlayer or tvRef.players.youtube for control methods

### tvStateMachine.js (Global State Manager)
**Purpose:** Centralized state management for player and UI
**Responsibilities:**
- Maintain single source of truth for: playerState, currentItem, playlistLength, userIntent, fullscreen, overlay
- Provide transition() method for state changes
- Provide subscribe() method for state listeners
- Calculate first/next/previous qualifying items (progress < 95%, remaining >= 30s)

**Limits:**
- ❌ Must NOT contain React code
- ❌ Must NOT directly manipulate DOM or localStorage
- ❌ Must NOT make API calls
- ✅ Should be pure state management only
- ✅ React components subscribe and sync FROM state machine
- ✅ State machine is source of truth, React state follows it

### helpers.js (Pure Utilities)
**Purpose:** Reusable utility functions
**Responsibilities:**
- getInSeconds(): Parse timestamp strings (HH:MM:SS:FF, HH:MM:SS, MM:SS) to seconds
- formatDuration(): Format seconds as HH:MM:SS or MM:SS
- parseDuration(): Parse ISO 8601 duration (PT1H2M3S) to seconds
- Cookie utilities: getTvCookieName, getCookie, setCookie

**Limits:**
- ❌ Must NOT contain side effects
- ❌ Must NOT manipulate DOM
- ❌ Must NOT manage state
- ✅ Should be pure functions only
- ✅ All exports must be stateless

## State Management Rules

1. **tvStateMachine is source of truth**
   - React state syncs FROM state machine, never the reverse
   - Components subscribe to state machine changes
   - Components call transition() to update state machine

2. **localStorage usage**
   - Progress: `tv_progress_${itemId}` stores 0-100 percentage
   - Progress is relative to clip boundaries (startTs to endTs), not full video
   - Mute preference: `tv_muted` stores 'true'/'false'
   - Channel: Cookie stores last selected channel

3. **Progress calculation**
   - Stored as percentage of clip length: `((currentTime - startTs) / (endTs - startTs)) * 100`
   - Videos with progress >= 95% are considered "complete"
   - Resume if 0 < progress < 95%, otherwise start from startTs

4. **Clip boundaries**
   - startTs: Start time for video segment (HH:MM:SS or HH:MM:SS:FF)
   - endTs: End time for video segment (optional, defaults to video duration)
   - All seeking, progress, and playback constrained to [startTs, endTs]

## Data Flow

```
User Action (keyboard/click)
  ↓
Controls.js / ChannelItems.js
  ↓
tvStateMachine.transition('ACTION')
  ↓
tvStateMachine notifies subscribers
  ↓
TvContainer.js updates React state
  ↓
Re-render UI / Player
  ↓
Player.js coordinates with YouTube.js
  ↓
YouTube.js calls provider API
  ↓
Provider fires state change event
  ↓
YouTube.js calls tvStateMachine.transition()
  ↓
Loop continues...
```

## Naming Conventions

- **Custom hooks:** `use` prefix (useChannelFetch, useChannelApi, useChannelStateSync)
- **Event handlers:** `handle` prefix (handleResetProgress, handleVideoEnd)
- **Provider wrappers:** Provider name + .js (YouTube.js, Vimeo.js, Rumble.js)
- **Refs:** `xxxRef` suffix (tvRef, playerRef, userHasUnmutedRef)
- **State machine functions:** Exported from tvStateMachine.js (transition, subscribe, getState)

## Magic Numbers Documentation

- **3000ms** - Delay before starting progress updates (prevents overwriting resume position)
- **1000ms** - Progress display polling interval in ChannelItems
- **500ms** - Player progress update interval, state polling interval
- **250ms** - Retry delay for YouTube API initialization
- **200ms** - Retry delay for video loading
- **2000ms** - Delay before seeking after video load
- **95%** - Progress threshold for "complete" videos
- **30 seconds** - Minimum remaining time for "qualifying" items

## Common Patterns

### Adding a new provider (Vimeo, Rumble)
1. Create `Embed/Vimeo/Vimeo.js` mirroring YouTube.js structure
2. Export same interface: load video with startSeconds, expose methods, emit events
3. Update Player.js to conditionally render provider based on item.url
4. Keep all progress/clip logic in Player.js (don't duplicate)

### Progress bar click handling
1. ChannelItems calculates seekSeconds from click position and clip boundaries
2. Updates localStorage with new progress percentage
3. Calls tvRef.players.youtube.seekTo(seekSeconds) directly
4. OR sets desiredStartTimeRef and calls triggerSeekFromProgressBar()

### Resume position calculation
1. Player.js reads localStorage on item change
2. If 0 < progress < 95%, calculates resumeSeconds = startTs + (progress% * clipLength)
3. Passes as startSeconds prop to provider wrapper
4. Provider loads video with startSeconds parameter (smooth start)

## Testing Guidelines

When modifying components, verify:
- Progress tracking works within clip boundaries
- Resume positions load correctly
- Mute state persists across video changes
- Auto-advance works at endTs (not video end)
- Progress bar seeking respects clip boundaries
- State machine stays in sync with UI

## Future Considerations

- Vimeo.js wrapper (same interface as YouTube.js)
- Rumble.js wrapper (same interface as YouTube.js)
- Playlist queue management (currently sequential)
- Chapter/segment support (multiple clips per video)
- Analytics/tracking (video completion, watch time)

## Architecture Refactoring TODOs

These improvements would enhance maintainability and prepare for multi-provider support:

### 1. TODO: Create Progress Management Hook/Utility
**Current Issue:** Progress tracking is scattered across multiple files
- Player.js polls and updates localStorage
- ChannelItems.js polls localStorage and updates UI
- Progress calculations duplicated in multiple places

**Proposed Solution:**
```javascript
// hooks/useProgress.js
export function useProgress(item, clipStart, clipEnd) {
  const [progress, setProgress] = useState(0);

  // Centralized logic for:
  // - Reading from localStorage
  // - Calculating percentage based on clip boundaries
  // - Updating localStorage
  // - Triggering parent callbacks

  return {
    progress,
    updateProgress: (currentTime) => { /* ... */ },
    resetProgress: () => { /* ... */ },
    getResumePosition: () => { /* ... */ }
  };
}
```

**Files to modify:**
- Create new `hooks/useProgress.js`
- Refactor Player.js to use hook
- Refactor ChannelItems.js to use hook
- Remove duplicate progress logic

**Benefits:**
- Single source of truth for progress calculations
- Consistent clip boundary handling
- Easier testing
- Reduces code duplication

### 2. TODO: Simplify YouTube.js to Pure Wrapper
**Current Issue:** YouTube.js contains too many responsibilities
- Manages mute state (userHasUnmutedRef)
- Calculates clip boundaries from startTs/endTs
- Direct tvStateMachine interactions beyond simple notifications
- Forever-loop detection
- Complex seeking logic

**Proposed Solution:**
YouTube.js should ONLY:
```javascript
// Minimal interface
export default function YouTube({ videoId, startSeconds, autoplay, onReady, onStateChange, onError }) {
  // Initialize YouTube iframe API
  // Load video with parameters
  // Expose methods: play(), pause(), seekTo(), getCurrentTime(), mute(), unmute()
  // Emit events: onReady, onStateChange(state), onError
  return <div id="youtube-player"></div>;
}
```

**Move to Player.js:**
- Mute state management (userHasUnmutedRef)
- Forever-loop detection
- Clip boundary calculations
- Seeking coordination

**Files to modify:**
- Refactor YouTube.js to minimal wrapper (~200 lines max)
- Move business logic to Player.js
- Create clear prop interface

**Benefits:**
- Easy to add Vimeo.js, Rumble.js with same interface
- YouTube.js becomes reusable in other projects
- Clearer separation of concerns
- Easier to test provider wrappers

### 3. TODO: Extract Magic Numbers to Constants
**Current Issue:** Hardcoded delays throughout codebase make it hard to understand timing behavior

**Proposed Solution:**
```javascript
// constants.js
export const TIMING = {
  PROGRESS_UPDATE_INTERVAL: 500,        // Player progress polling
  PROGRESS_UI_UPDATE_INTERVAL: 1000,    // ChannelItems UI refresh
  PROGRESS_UPDATE_DELAY: 3000,          // Delay before tracking progress (prevents overwriting resume)
  YOUTUBE_SEEK_DELAY: 2000,             // Delay before seeking after load
  YOUTUBE_AUTOPLAY_DELAY: 3000,         // Delay before auto-playing
  YOUTUBE_RETRY_DELAY: 200,             // Retry delay for API initialization
  STATE_POLLING_INTERVAL: 500,          // State machine sync polling
  STATE_RETRY_INTERVAL: 250,            // Retry interval for player availability
};

export const THRESHOLDS = {
  COMPLETE_PROGRESS_PERCENT: 95,        // Consider video "complete" at this %
  QUALIFYING_REMAINING_SECONDS: 30,     // Minimum time remaining for "qualifying" items
  MAX_LOADS_IN_WINDOW: 10,              // Forever-loop detection: max loads
  FOREVER_LOOP_WINDOW_MS: 5000,         // Forever-loop detection: time window
};

export const PLAYBACK = {
  DEFAULT_FRAME_RATE: 30,               // Assumed FPS for timestamp parsing
};
```

**Files to modify:**
- Create `constants.js`
- Replace all magic numbers in Player.js, YouTube.js, ChannelItems.js
- Add inline comments explaining why each delay exists

**Benefits:**
- Easy to tune timing behavior
- Self-documenting code
- Easier to test with different timings
- Clear understanding of why delays exist

### 4. TODO: Split ChannelItems.js into Smaller Components
**Current Issue:** ChannelItems.js (~381 lines) does too much
- Exports 4 different custom hooks
- Handles UI rendering
- Manages progress state
- Implements progress bar seeking
- Handles reset progress

**Proposed Solution:**
```
Components/
├── ChannelItems/
│   ├── index.js                 // Main component (UI only, ~100 lines)
│   ├── ChannelItem.js           // Single item component
│   ├── ProgressBar.js           // Progress bar with seek handling
│   └── hooks/
│       ├── useChannelApi.js     // API calls
│       ├── useChannelStateSync.js // State machine sync
│       └── useChannelFetch.js   // Fetch on ID change
```

**Files to create:**
- `ChannelItems/index.js` - Presentation component only
- `ChannelItems/ChannelItem.js` - Single item with thumbnail, duration, progress
- `ChannelItems/ProgressBar.js` - Reusable progress bar with click handling
- Move hooks to separate files

**Benefits:**
- Each file has single responsibility
- Components become reusable
- Easier to test in isolation
- Clearer import statements

### 5. TODO: Standardize Logging
**Current Issue:** Inconsistent logging with different prefixes
- `[YouTube]`, `[Player]`, `[ProgressBar]`, `[TvContainer]`
- Mix of console.log, console.error, console.warn
- No way to disable logging in production
- Hard to trace execution flow

**Proposed Solution:**
```javascript
// utils/logger.js
const LOG_LEVELS = {
  ERROR: 0,
  WARN: 1,
  INFO: 2,
  DEBUG: 3
};

let currentLevel = LOG_LEVELS.INFO;

export const logger = {
  error: (component, message, ...args) => {
    if (currentLevel >= LOG_LEVELS.ERROR) {
      console.error(`[TV:${component}]`, message, ...args);
    }
  },
  warn: (component, message, ...args) => {
    if (currentLevel >= LOG_LEVELS.WARN) {
      console.warn(`[TV:${component}]`, message, ...args);
    }
  },
  info: (component, message, ...args) => {
    if (currentLevel >= LOG_LEVELS.INFO) {
      console.log(`[TV:${component}]`, message, ...args);
    }
  },
  debug: (component, message, ...args) => {
    if (currentLevel >= LOG_LEVELS.DEBUG) {
      console.debug(`[TV:${component}]`, message, ...args);
    }
  },
  setLevel: (level) => {
    currentLevel = level;
  }
};

// Usage: logger.info('Player', 'Seeking to position:', seekTime);
```

**Files to modify:**
- Create `utils/logger.js`
- Replace all console.log/warn/error calls
- Add production config to disable debug logs

**Benefits:**
- Consistent log format
- Easy to disable logging in production
- Can filter by component
- Better debugging experience

### 6. TODO: Move Player State Polling to Custom Hook
**Current Issue:** YouTube.js has 500ms polling interval to sync state with tvStateMachine
- Ties provider wrapper to state management
- Makes it harder to add other providers
- Duplicates state change notifications

**Proposed Solution:**
```javascript
// hooks/usePlayerStateSync.js
export function usePlayerStateSync(playerRef, onStateChange) {
  useEffect(() => {
    // Poll player state and call onStateChange
    // Let parent handle tvStateMachine updates
  }, [playerRef]);
}

// YouTube.js just emits events
function YouTube({ onStateChange }) {
  // Call onStateChange('playing') when YouTube fires events
  // No direct tvStateMachine interaction
}

// Player.js coordinates
function Player() {
  const handleStateChange = (state) => {
    tvStateMachine.transition(mapStateToTransition(state));
  };

  return <YouTube onStateChange={handleStateChange} />;
}
```

**Benefits:**
- Provider wrappers become stateless
- Easier to add Vimeo/Rumble
- State management stays in Player.js
- Clear event flow

### 7. TODO: Consolidate Multiple useEffect Hooks
**Current Issue:** Components have many useEffect hooks with overlapping concerns
- TvContainer.js has 6+ useEffect hooks
- Player.js has 5+ useEffect hooks
- Many effects have complex dependency arrays
- Hard to understand lifecycle and ordering

**Proposed Solution:**
```javascript
// Instead of multiple effects:
useEffect(() => { /* handle channel change */ }, [channelId]);
useEffect(() => { /* handle item change */ }, [currentItem]);
useEffect(() => { /* fetch data */ }, [channelId]);
useEffect(() => { /* subscribe to state */ }, []);

// Consolidate related effects:
useEffect(() => {
  // Channel initialization logic (once)
}, []); // Mount only

useEffect(() => {
  // Channel/item change logic (combined)
  if (channelId) {
    fetchChannel(channelId);
    updateCurrentItem();
  }
}, [channelId, currentItem]); // Related changes

useEffect(() => {
  // Cleanup only
  return () => { /* cleanup */ };
}, []); // Unmount only
```

**Files to modify:**
- TvContainer.js - Consolidate 6 effects to 3-4
- Player.js - Consolidate 5 effects to 2-3
- Review all components for effect consolidation

**Benefits:**
- Easier to understand component lifecycle
- Fewer dependency array bugs
- Clearer separation of concerns
- Better performance (fewer effect runs)

### 8. TODO: Replace playbackEvents.js with Context Provider
**Current Issue:** playbackEvents.js uses factory pattern with callbacks passed through props
- updateProgress callback created in TvContainer
- Passed through multiple component layers
- Callback recreation causes unnecessary re-renders
- Progress management split between playbackEvents.js and components

**Proposed Solution:**
```javascript
// contexts/ProgressContext.js
const ProgressContext = createContext();

export function ProgressProvider({ children }) {
  const updateProgress = useCallback((itemId, progress) => {
    localStorage.setItem(`tv_progress_${itemId}`, progress);
    // Notify subscribers
  }, []);

  const getProgress = useCallback((itemId) => {
    return localStorage.getItem(`tv_progress_${itemId}`) || 0;
  }, []);

  const resetProgress = useCallback((itemId) => {
    localStorage.removeItem(`tv_progress_${itemId}`);
  }, []);

  return (
    <ProgressContext.Provider value={{ updateProgress, getProgress, resetProgress }}>
      {children}
    </ProgressContext.Provider>
  );
}

export function useProgress() {
  return useContext(ProgressContext);
}

// Usage in any component:
const { updateProgress, getProgress, resetProgress } = useProgress();
```

**Files to modify:**
- Create `contexts/ProgressContext.js`
- Wrap TvContainer with ProgressProvider
- Remove playbackEvents.js factory functions
- Update Player.js, ChannelItems.js to use context

**Benefits:**
- No prop drilling for progress callbacks
- Stable references (no recreation on re-render)
- Centralized progress management
- Easier testing with mock provider
- Could add progress change subscriptions

### 9. TODO: Eliminate Dual State Management (React + tvStateMachine)
**Current Issue:** Redundant state in both React state and tvStateMachine
- currentItem stored in both TvContainer state and tvStateMachine
- playerState stored in both Player state and tvStateMachine
- Frequent synchronization needed with useEffect
- Source of truth unclear
- Causes unnecessary re-renders

**Proposed Solution:**
```javascript
// Option A: Make tvStateMachine the ONLY source
function TvContainer() {
  // Instead of: const [currentItem, setCurrentItem] = useState(null);
  // Use: const currentItem = tvStateMachine.getState().currentItem;
  const [, forceUpdate] = useReducer(x => x + 1, 0);

  useEffect(() => {
    return tvStateMachine.subscribe(() => {
      forceUpdate(); // Re-render on state changes
    });
  }, []);

  // Components read directly from tvStateMachine.getState()
}

// Option B: Use React Context wrapping tvStateMachine
const TvStateContext = createContext();

export function TvStateProvider({ children }) {
  const [state, setState] = useState(tvStateMachine.getState());

  useEffect(() => {
    return tvStateMachine.subscribe(() => {
      setState(tvStateMachine.getState());
    });
  }, []);

  return <TvStateContext.Provider value={state}>{children}</TvStateContext.Provider>;
}

// Components use: const { currentItem, playerState } = useContext(TvStateContext);
```

**Files to modify:**
- Create `contexts/TvStateContext.js` or refactor components to read directly
- Remove duplicate state from TvContainer, Player
- Remove synchronization useEffects
- Update all components to read from single source

**Benefits:**
- Single source of truth (eliminates sync bugs)
- Fewer useEffect hooks needed
- Clearer data flow
- Better performance (targeted re-renders with context)
- Easier debugging (one state object to inspect)

### 10. TODO: Remove Polling, Use Event-Driven Architecture
**Current Issue:** Multiple polling intervals creating performance overhead
- Player.js polls every 500ms for progress updates
- ChannelItems.js polls localStorage every 1000ms
- YouTube.js polls player state every 500ms
- State polling in multiple places with 250ms/500ms intervals

**Proposed Solution:**
```javascript
// Create event bus for progress updates
// utils/eventBus.js
class EventBus {
  constructor() {
    this.listeners = {};
  }

  on(event, callback) {
    if (!this.listeners[event]) this.listeners[event] = [];
    this.listeners[event].push(callback);
    return () => this.off(event, callback);
  }

  emit(event, data) {
    (this.listeners[event] || []).forEach(cb => cb(data));
  }
}

export const progressEvents = new EventBus();

// Player.js emits on progress change
useEffect(() => {
  const interval = setInterval(() => {
    const progress = calculateProgress();
    progressEvents.emit('progress', { itemId, progress });
  }, 1000); // Can be less frequent now
  return () => clearInterval(interval);
}, [itemId]);

// ChannelItems.js subscribes
useEffect(() => {
  return progressEvents.on('progress', ({ itemId, progress }) => {
    if (itemId === currentItem.id) {
      setDisplayProgress(progress);
    }
  });
}, [currentItem]);

// YouTube.js: Replace polling with YouTube API event listeners
// YouTube API already has onStateChange - use it!
```

**Files to modify:**
- Create `utils/eventBus.js`
- Player.js: Emit progress events instead of polling localStorage
- ChannelItems.js: Subscribe to progress events, remove polling
- YouTube.js: Use YouTube API events, remove polling
- Remove all unnecessary setInterval calls

**Benefits:**
- Better performance (no constant polling)
- Real-time updates (no 500ms-1000ms delay)
- Lower CPU usage
- Scales better with multiple components
- Easier to debug (event names are descriptive)

### 11. TODO: Simplify Navigation with React Router (or similar)
**Current Issue:** Complex manual navigation state management
- Keys.js builds dynamic navigation map from accordion state
- Manual focus index tracking (currentFocusIndex)
- Complex keyboard nav logic with up/down/left/right
- Tight coupling between Keys, Accordion, and ChannelList

**Proposed Solution:**
```javascript
// Simplify to section-based navigation
const SECTIONS = {
  CHANNELS: 'channels',
  PLAYLIST: 'playlist',
  CONTROLS: 'controls'
};

function useSimpleNav() {
  const [activeSection, setActiveSection] = useState(SECTIONS.PLAYLIST);
  const [focusIndex, setFocusIndex] = useState(0);

  const handleKey = useCallback((key) => {
    switch(key) {
      case 'ArrowLeft':
        setActiveSection(SECTIONS.CHANNELS);
        break;
      case 'ArrowRight':
        setActiveSection(SECTIONS.PLAYLIST);
        break;
      case 'ArrowDown':
        setActiveSection(SECTIONS.CONTROLS);
        break;
      // Simple up/down within section
      case 'ArrowUp':
      case 'ArrowDown':
        // Increment/decrement focusIndex within active section
        break;
    }
  }, [activeSection]);

  return { activeSection, focusIndex, handleKey };
}
```

**Files to modify:**
- Simplify Keys.js navigation logic (remove dynamic map building)
- Use data attributes for focusable elements instead of manual map
- Consider using `roving tabindex` pattern
- Reduce coupling with Accordion component

**Benefits:**
- Simpler keyboard navigation logic
- No dynamic map rebuilding
- Standard accessibility patterns
- Easier to add new sections
- Less code to maintain

### 12. TODO: Extract Clip Boundary Logic to Utility Module
**Current Issue:** Clip boundary calculations (startTs/endTs) scattered across components
- getInSeconds() is centralized but clip math is not
- Progress percentage calculations duplicated
- Resume position calculations duplicated
- Qualifying item logic (progress < 95%, remaining > 30s) in multiple places

**Proposed Solution:**
```javascript
// utils/clipBoundaries.js
export const clipUtils = {
  /**
   * Parse clip boundaries from item
   * @returns {{ startSeconds, endSeconds, clipDuration }}
   */
  parseClipBoundaries(item) {
    const startSeconds = getInSeconds(item.startTs || '00:00:00');
    const endSeconds = item.endTs
      ? getInSeconds(item.endTs)
      : item.duration || Infinity;
    const clipDuration = endSeconds - startSeconds;

    return { startSeconds, endSeconds, clipDuration };
  },

  /**
   * Calculate progress percentage within clip boundaries
   */
  calculateProgress(currentTime, item) {
    const { startSeconds, clipDuration } = this.parseClipBoundaries(item);
    const progress = ((currentTime - startSeconds) / clipDuration) * 100;
    return Math.max(0, Math.min(100, progress));
  },

  /**
   * Calculate resume position from saved progress
   */
  getResumePosition(savedProgress, item) {
    const { startSeconds, endSeconds, clipDuration } = this.parseClipBoundaries(item);

    if (savedProgress <= 0 || savedProgress >= 95) {
      return startSeconds;
    }

    const resumeSeconds = startSeconds + (savedProgress / 100) * clipDuration;
    return Math.min(resumeSeconds, endSeconds);
  },

  /**
   * Check if item qualifies for auto-advance
   */
  isQualifying(item, savedProgress) {
    if (savedProgress >= 95) return false;

    const { clipDuration } = this.parseClipBoundaries(item);
    const remaining = clipDuration * (1 - savedProgress / 100);

    return remaining >= 30; // THRESHOLDS.QUALIFYING_REMAINING_SECONDS
  },

  /**
   * Check if current time is at clip end
   */
  isAtClipEnd(currentTime, item, tolerance = 0.5) {
    const { endSeconds } = this.parseClipBoundaries(item);
    return currentTime >= (endSeconds - tolerance);
  }
};
```

**Files to modify:**
- Create `utils/clipBoundaries.js`
- Player.js: Use clipUtils for all calculations
- ChannelItems.js: Use clipUtils for progress bar
- tvStateMachine.js: Use clipUtils for qualifying items
- Remove duplicate calculations everywhere

**Benefits:**
- Single source of truth for clip math
- Consistent behavior across components
- Easier to add features (e.g., chapter markers)
- Better test coverage (pure functions)
- Self-documenting with clear function names

## Refactoring Priority Order

If implementing these improvements, suggested order:

**Quick Wins (Low Risk, High Impact):**
1. **Magic Numbers → Constants** (TODO #3) - 1 hour, instant clarity
2. **Standardize Logging** (TODO #5) - 2 hours, better debugging
3. **Extract Clip Boundaries** (TODO #12) - 3 hours, removes duplication

**Foundation Improvements (Medium Risk, High Impact):**
4. **Remove Polling → Events** (TODO #10) - 4 hours, major performance gain
5. **Context for Progress** (TODO #8) - 3 hours, eliminates prop drilling
6. **Consolidate useEffects** (TODO #7) - 4 hours, clearer lifecycle

**Architecture Overhauls (Higher Risk, Highest Impact):**
7. **Progress Hook** (TODO #1) - 5 hours, consolidates scattered logic
8. **Simplify YouTube.js** (TODO #2) - 6 hours, prepares for multi-provider
9. **Single State Source** (TODO #9) - 8 hours, eliminates sync bugs
10. **Player State Hook** (TODO #6) - 4 hours, completes multi-provider prep

**Nice-to-Haves (Lower Priority):**
11. **Split ChannelItems** (TODO #4) - 6 hours, improves maintainability
12. **Simplify Navigation** (TODO #11) - 8 hours, reduces complexity

**Estimated Total:** ~52 hours for complete refactor
**Recommended MVP:** Items 1-6 (~17 hours) for 70% of the benefits

## Notes for AI Agents

When asked to "clean up" or "refactor" this codebase:
1. Check this TODO list first
2. Follow the proposed solutions as guidelines
3. Update this document when completing TODOs
4. Mark items as ✅ COMPLETED when done
5. Maintain the architecture boundaries defined above
6. If implementing multiple TODOs, follow the priority order
7. Consider dependencies (e.g., do #3 before #10 so events can use constants)
