# Settings Two-Pane Focus Pattern (NuvioTV Reference)

The focus architecture for the two-pane settings screen.
Based on NuvioTV's `SettingsScreen.kt` (line 202-525).

## Files

- `app/src/main/java/com/nexstream/app/ui/screens/settings/SettingsScreen.kt` — NexStream implementation
- `app/src/main/java/com/nuvio/tv/ui/screens/settings/SettingsScreen.kt` — NuvioTV reference
- `app/src/main/java/com/nexstream/app/ui/screens/settings/SettingsDesignSystem.kt` — Shared components

## Architecture

```
┌──────────────┐  ┌─────────────────────────────────┐
│  Rail (L)    │  │  Detail Pane (R)                │
│  ┌────────┐  │  │  ┌───────────────────────────┐  │
│  │ Debrid  │──┼──▶  │ SettingsGroupCard         │  │
│  │ Trakt   │  │  │  │  SettingsToggleRow (1st)  │  │
│  │ TMDB    │  │  │  │  SettingsToggleRow        │  │
│  │ Addons  │  │  │  │  SettingsActionRow        │  │
│  │ Playback│  │  │  └───────────────────────────┘  │
│  │ Sources │  │  │                                 │
│  │ Subtitles│ │  │                                 │
│  │ Nav     │  │  │                                 │
│  └────────┘  │  │                                 │
└──────────────┘  └─────────────────────────────────┘
```

## Focus Flow

1. **Initial load**: `LaunchedEffect(Unit) { railFocusRequesters[selected]?.requestFocus() }`
2. **Rail UP/DOWN**: LazyColumn handles naturally
3. **RIGHT from rail**: `onPreviewKeyEvent` on each rail item sets `allowDetailFocus = true`
4. **Content focus**: `LaunchedEffect(pendingContentFocusRequestId)` → 120ms delay → `contentFocusRequesters[category]?.requestFocus()`
5. **LEFT from detail**: `onKeyEvent` on detail Box → `focusManager.moveFocus(Left)` first, then `railFocusRequesters[selected]?.requestFocus()`
6. **Bounce-back guard**: `onFocusChanged` on detail Box → if `!allowDetailFocus`, sends focus back to rail

## Critical Rules

### RULE 1: Detail pane Box MUST NOT be `.focusable()`

The detail pane wrapper Box should never consume focus. It's a layout container,
not a focus target. If it has `.focusable()`, child controls (toggle rows,
action rows, text fields) can never receive focus.

**THE BUG**: The original NexStream implementation had:
```kotlin
Box(
    modifier = Modifier
        .focusRequester(detailFocusRequester)
        .focusable()  // ← THIS IS THE BUG
) { /* content */ }
```

This traps all D-pad events. Pressing DOWN after RIGHT from the rail does nothing
because focus is on the wrapper Box, not on the child content.

**THE FIX**: Remove `.focusable()` and use per-panel `FocusRequester`s:
```kotlin
// In SettingsScreen:
val contentFocusRequesters = remember { panels.associateWith { FocusRequester() } }

// Request focus on the SPECIFIC panel's FocusRequester:
LaunchedEffect(pendingContentFocusRequestId) {
    val requester = contentFocusRequesters[selected]
    val requested = requester?.let { runCatching { it.requestFocus() }.isSuccess } ?: false
    if (!requested) focusManager.moveFocus(FocusDirection.Right) // NuvioTV fallback
}
```

### RULE 2: Per-panel FocusRequesters, not one shared

Each settings panel needs its own `FocusRequester`. The sub-screen attaches it
to its first focusable element:

```kotlin
// In sub-screen:
fun PlaybackSettingsScreen(
    contentFocusRequester: FocusRequester? = null,
) {
    SettingsChoiceChip(
        modifier = if (contentFocusRequester != null) {
            Modifier.focusRequester(contentFocusRequester)
        } else Modifier
    )
}
```

### RULE 3: NuvioTV fallback with `moveFocus(Right)`

If the content FocusRequester fails (sub-screen hasn't wired it yet),
use `focusManager.moveFocus(FocusDirection.Right)` as fallback. This
ensures focus still enters the detail pane even for unwired screens.

### RULE 4: LEFT from detail → try `moveFocus` first

```kotlin
.onKeyEvent { event ->
    if (event.key == Key.DirectionLeft) {
        allowDetailFocus = false
        if (!focusManager.moveFocus(FocusDirection.Left)) {
            railFocusRequesters[selected]?.requestFocus()
        }
        true
    }
}
```

This allows internal horizontal navigation (e.g., sub-rail within Integration
settings) before returning to the main rail.

### RULE 5: Bounce-back guard

```kotlin
.onFocusChanged { state ->
    if (state.hasFocus && !allowDetailFocus) {
        railFocusRequesters[selected]?.requestFocus()
    }
}
```

If focus somehow lands on the detail pane without the user pressing RIGHT
(e.g., programmatic focus), send it back to the rail immediately.

## Sub-Screen Patterns

### Simple screens (single focus chain)

Attach `contentFocusRequester` to the first `SettingsChoiceChip` or
`SettingsActionRow` via modifier.

### List-based screens (forEach/forEachIndexed)

```kotlin
items.forEachIndexed { index, item ->
    Card(
        modifier = if (index == 0 && contentFocusRequester != null) {
            Modifier.focusRequester(contentFocusRequester)
        } else Modifier
    ) { ... }
}
```

### Complex screens (private composables)

Add a `modifier: Modifier = Modifier` parameter to the private composable,
then attach the FocusRequester at the callsite:
```kotlin
DebridProviderCard(
    item = item,
    modifier = if (index == 0 && contentFocusRequester != null) {
        Modifier.focusRequester(contentFocusRequester)
    } else Modifier
)
```

## Dynamic Sizing

Settings rail width and content padding should use `TvLayoutMetrics`:

```kotlin
val metrics = rememberTvLayoutMetrics()
// Use metrics.settingsRailWidth instead of hardcoded 240.dp
// Use metrics.contentPadding instead of hardcoded 32.dp
```

`TvLayoutMetrics` adjusts based on screen width:
- < 960dp → rail 200dp, padding 16dp
- < 1280dp → rail 220dp, padding 24dp
- ≥ 1280dp → rail 260dp, padding 28dp
