# NuvioTV Focus State Machine (Ported to NexStream)

The focus state machine that governs sidebar ↔ content navigation.
Originally from NuvioTV's `ModernSidebarScaffold`, now in NexStream's
`NetflixSideNavigation.kt`.

## Files

- `app/src/main/java/com/nexstream/app/ui/components/netflix/NetflixSideNavigation.kt` — Main implementation
- `app/src/main/java/com/nuvio/tv/MainActivity.kt` — NuvioTV reference (lines 1162-1562)
- `app/src/main/java/com/nuvio/tv/ModernSidebarBlurPanel.kt` — NuvioTV reference

## State Variables

```
isExpanded               — sidebar fully open
collapsePending          — animation flag, collapse after 95ms delay
pendingContentFocusTransfer — move focus to content after collapse
pendingSidebarFocusRequest  — move focus to sidebar item on expand
focusedDrawerIndex       — which item is focused (for boundary detection)
sidebarBlocksContentKeys — derived: blocks D-pad in content when expanded
```

## Focus Flow

```
CONTENT (focused)
  │
  ├─ LEFT → focusManager.moveFocus(Left) FIRST
  │         ├─ success → moves to left element in content
  │         └─ fail → isExpanded=true, pendingSidebarFocusRequest=true
  │
  ├─ BACK (collapsed) → isExpanded=true, pendingSidebarFocusRequest=true
  └─ BACK (expanded) → exit (parent handles)

SIDEBAR (expanded)
  │
  ├─ RIGHT/BACK → collapsePending=true → 95ms → isExpanded=false, focus→content
  ├─ UP (at first) → event eaten (returns true from onPreviewKeyEvent)
  ├─ DOWN (at last) → event eaten
  ├─ LEFT → event eaten (nothing to left of sidebar)
  ├─ CLICK item → navigate, collapse, pendingContentFocusTransfer=true
  └─ idle 4s → auto-collapse (timer resets on focusedDrawerIndex change)
```

## Key Implementation Details

### Content Box MUST NOT be `.focusable()`

The content wrapper Box should host the Compose Navigation content but
NOT consume focus itself. If it has `.focusable()`, it traps D-pad events
and prevents focus from reaching actual content controls.

**WRONG (causes the bug):**
```kotlin
Box(
    modifier = Modifier.focusRequester(contentFocusRequester).focusable()
) { content() }
```

**CORRECT (NuvioTV pattern):**
```kotlin
Box(
    modifier = Modifier
        .onPreviewKeyEvent { /* block D-pad when sidebar expanded */ }
        .onKeyEvent { /* LEFT → try moveFocus + expand */ }
) { content() }
```

### Content handles LEFT via `onKeyEvent`

The content Box intercepts LEFT key in `onKeyEvent` (not `onPreviewKeyEvent`)
and tries `focusManager.moveFocus(Left)` FIRST. Only if nothing is to the left
does it expand the sidebar. This allows internal horizontal navigation
(like settings rail→detail) to work before sidebar expansion.

### Sidebar blocks content D-pad via `onPreviewKeyEvent`

When sidebar is expanded, the content Box's `onPreviewKeyEvent` returns `true`
for all D-pad keys, preventing them from leaking through to the content behind.

### Dual BackHandler pattern (NuvioTV)

```
BackHandler(enabled = !isExpanded) { expand sidebar }
BackHandler(enabled = isExpanded)  { exit }
```

First BACK press expands sidebar. Second BACK exits app.
NexStream uses `onNavigateToContent` for exit (parent handles).

### Auto-collapse timer

4s after last `focusedDrawerIndex` change. Timer resets on focus change,
so the sidebar stays open as long as the user is actively navigating it.

### CompositionLocals

```kotlin
val LocalSidebarExpanded = compositionLocalOf { false }
val LocalContentFocusRequester = compositionLocalOf { FocusRequester() }
```

Screens can read these to react to sidebar state:
- `LocalSidebarExpanded.current` → adjust layout/spacing
- `LocalContentFocusRequester.current` → attach to first focusable element

## Pitfalls

- **Content Box `.focusable()`**: The #1 bug. Never make the content wrapper
  focusable — it blocks all D-pad events. Use `onKeyEvent`/`onPreviewKeyEvent`
  for key interception instead.

- **`moveFocus()` BEFORE `requestFocus()`**: Always try `moveFocus(Direction)`
  first. Only fall back to `requestFocus()` if `moveFocus` fails. This preserves
  internal focus chains within screens.

- **`onKeyEvent` vs `onPreviewKeyEvent`**: Content uses `onKeyEvent` for LEFT
  (allows child composables to handle it first). Sidebar uses `onPreviewKeyEvent`
  for boundary detection (eats event before child sees it).

- **State timing**: Use `LaunchedEffect` with state gates (e.g., `if (!pending ||
  isExpanded || collapsePending) return`) rather than direct calls. This avoids
  race conditions between animation frames and state changes.

- **`derivedStateOf` for thresholds**: Use `derivedStateOf { progress > 0.2f }`
  instead of reading animated floats directly. Prevents per-frame recomposition.
