# Android TV Two-Pane Settings — Focus Architecture

## The Bug: Focusable Wrapper Consumes D-Pad

**Symptom:** Pressing RIGHT from the rail moves focus to the detail pane, but pressing DOWN/UP does nothing — focus stays on an invisible wrapper, never reaching individual settings controls inside.

**Root cause:** A `Box(modifier = Modifier.focusRequester(r).focusable().onFocusChanged{...})` as a "focus wrapper" for the detail pane. `.focusable()` makes the Box a focus target that intercepts all subsequent D-Pad events. The child composables (toggle rows, action rows, text fields) never receive focus.

**Fix:** Remove `.focusable()` from the detail pane wrapper entirely. The detail pane Box should be a plain layout container — focus must land directly on *elements inside* the content.

## NuvioTV Proven Pattern (v0.7.0-beta)

### Architecture
```
SettingsScreen
├── Left Rail (220dp LazyColumn)
│   ├── SettingsRailButton × N  (each has own FocusRequester)
│   └── onPreviewKeyEvent: RIGHT → sets allowDetailAutofocus=true
│
└── Right Detail Pane (weight=1f Box)
    ├── onKeyEvent: LEFT → returns focus to rail
    ├── onFocusChanged: bounce-back guard (!allowDetailAutofocus → back to rail)
    └── Content sub-screens accept initialFocusRequester: FocusRequester?
```

### Key State Variables
```kotlin
var selectedCategory: SettingsCategory          // current panel
var allowDetailAutofocus: Boolean                // gate: RIGHT was pressed
var pendingContentFocusRequestId: Long           // counter triggers LaunchedEffect
val railFocusRequesters: Map<Category, FocusRequester>      // one per rail item
val contentFocusRequesters: Map<Category, FocusRequester>   // one per content pane
```

### Focus Flow

**1. Initial focus (screen entry):**
```kotlin
LaunchedEffect(Unit) {
    runCatching { railContainerFocusRequester.requestFocus() }
}
// railContainer onFocusChanged handler requests railFocusRequesters[selectedCategory]
```

**2. RIGHT key → enter detail pane:**
```kotlin
// Rail item onPreviewKeyEvent:
if (event.key == Key.DirectionRight) {
    allowDetailAutofocus = true   // allow detail to accept focus
    false                         // don't consume — let system handle movement
}

// User click/select:  
onClick = {
    allowDetailAutofocus = true
    selectedCategory = section.category
    pendingContentFocusCategory = section.category
    pendingContentFocusRequestId += 1L   // triggers LaunchedEffect
}

// LaunchedEffect (120ms delay):
LaunchedEffect(pendingContentFocusRequestId) {
    delay(120)
    val requester = contentFocusRequesters[category]
    val requested = requester?.let { runCatching { it.requestFocus() }.isSuccess } ?: false
    if (!requested) {
        focusManager.moveFocus(FocusDirection.Right)  // FALLBACK
    }
}
```

**3. LEFT key → return to rail:**
```kotlin
// Detail pane onKeyEvent:
if (event.key == Key.DirectionLeft) {
    focusManager.moveFocus(FocusDirection.Left)          // try relative movement first
    if (!movedLeft) {
        allowDetailAutofocus = false
        railFocusRequesters[selectedCategory]?.requestFocus()  // absolute fallback
    }
    true
}
```

**4. Bounce-back guard (detail pane onFocusChanged):**
```kotlin
.onFocusChanged { state ->
    if (state.hasFocus && !allowDetailAutofocus) {
        // Focus landed here without RIGHT key intent → bounce back to rail
        railFocusRequesters[selectedCategory]?.requestFocus()
    }
}
```

### Content Sub-Screen Contract
Each sub-screen accepts `initialFocusRequester: FocusRequester? = null`:
```kotlin
@Composable
fun ThemeSettingsContent(
    initialFocusRequester: FocusRequester? = null
) {
    // First focusable element gets it:
    SettingsChoiceChip(
        modifier = if (index == 0 && initialFocusRequester != null) {
            Modifier.focusRequester(initialFocusRequester)
        } else {
            Modifier
        }
    )
}
```

### Design Tokens
| Token | Value |
|---|---|
| Rail width | 220dp |
| Pill radius | 999.dp (fully rounded) |
| Container radius | 28dp |
| Secondary card radius | 18dp |
| Rail item height | 56dp min |
| Content focus delay | 120ms |
| Rail gap | 16dp |
| Container padding | 20dp |
| Screen padding | 32dp horizontal, 24dp vertical |

### Rail Button States
```
Normal:      transparent bg, TextSecondary icon/text, no border
Focused:     BackgroundCard bg, TextPrimary icon/text, 2dp FocusRing border
Selected:    BackgroundCard bg, TextPrimary icon/text, 1dp FocusRing border
```

### Pitfalls
- **NEVER use `.focusable()` on a detail pane wrapper.** It blocks D-Pad from reaching children. Use `onKeyEvent` for key interception and `onFocusChanged` for focus tracking instead.
- **Always include the `moveFocus(Right)` fallback.** If a sub-screen hasn't wired its FocusRequester yet, focus still moves into the content area.
- **The 120ms delay is load-bearing.** Without it, focus requests before recomposition completes are silently dropped.
- **`onPreviewKeyEvent` for RIGHT, `onKeyEvent` for LEFT.** Preview is needed on the rail to set `allowDetailAutofocus` before system focus movement fires. On the detail pane, regular `onKeyEvent` works for LEFT return.
- **One FocusRequester per panel, not one shared.** Sharing creates race conditions when switching panels quickly.

## NexStream Implementation (2026-05-31)

Applied the NuvioTV pattern to `SettingsScreen.kt` (8 panels). Key differences from NuvioTV:
- 240dp rail (vs 220dp)
- Grouped rail items with section headers
- Uses `SettingsPanel` enum (vs `SettingsCategory`)
- `pendingContentFocusRequestId` counter (simpler than `pendingContentFocusCategory` pair)
