# Android TV Sidebar D-Pad Navigation

## The Build-vs-Patch Rule

When a sidebar/navigation system is fundamentally broken (focus stuck, visual bugs, unreliable D-pad), **rebuild it from scratch** using a proven reference pattern. Do not iteratively patch broken code — each patch adds complexity without fixing root causes. Study the reference implementation closely and adapt it 1:1.

## Pattern A: Homeflix Fixed Icon Rail (RECOMMENDED)

The simplest and most reliable Android TV sidebar pattern. From `homeflix-tv-app` (Netflix-style).

**Structure:**
- Fixed 48dp wide Column, black background, vertically centered
- 5-6 nav items, each a 36dp Box with an icon inside (20dp icon size)
- Items spaced 16dp apart
- No expansion, no labels, no width animation
- Each item is independently focusable

```kotlin
@Composable
fun NetflixSideNavigation(
    selectedRoute: String,
    onNavigate: (String) -> Unit,
    modifier: Modifier = Modifier,
    onNavigateToContent: (() -> Unit)? = null,
) {
    Column(
        modifier = modifier
            .width(48.dp)
            .fillMaxHeight()
            .background(Color.Black.copy(alpha = 0.92f)),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        navItems.forEachIndexed { index, item ->
            NetflixNavIcon(
                item = item,
                isSelected = selectedRoute == item.route,
                onClick = { onNavigate(item.route) },
                onNavigateRight = onNavigateToContent,
            )
            if (index < navItems.size - 1) {
                Spacer(modifier = Modifier.height(16.dp))
            }
        }
    }
}
```

**Nav icon pattern — triple requirement:**
```kotlin
@Composable
private fun NetflixNavIcon(
    item: NavItem,
    isSelected: Boolean,
    onClick: () -> Unit,
    onNavigateRight: (() -> Unit)? = null,
) {
    var isFocused by remember { mutableStateOf(false) }
    val focusRequester = remember { FocusRequester() }
    val scale by animateFloatAsState(
        targetValue = if (isFocused) 1.5f else 1.0f,
        animationSpec = tween(200),
    )

    Box(
        modifier = Modifier
            .size(36.dp)
            .scale(scale)
            .background(
                when {
                    isSelected -> accent
                    isFocused -> Color.White.copy(alpha = 0.20f)
                    else -> Color.Transparent
                },
                RoundedCornerShape(6.dp),
            )
            .focusRequester(focusRequester)
            .onFocusChanged { isFocused = it.isFocused }
            .focusable()
            .onKeyEvent { keyEvent ->
                if (keyEvent.type == KeyEventType.KeyDown) {
                    when {
                        keyEvent.key == Key.DirectionRight -> {
                            onNavigateRight?.invoke(); true
                        }
                        keyEvent.key == Key.Enter ||
                            keyEvent.key == Key.DirectionCenter -> {
                            onClick(); true
                        }
                        else -> false
                    }
                } else false
            }
            .clickable(onClick = onClick),
        contentAlignment = Alignment.Center,
    ) {
        Icon(item.icon, item.description, tint = Color.White, modifier = Modifier.size(20.dp))
    }
}
```

**Home screen layout — Row pattern:**
```kotlin
Row(modifier = Modifier.fillMaxSize()) {
    NetflixSideNavigation(
        selectedRoute = "home",
        onNavigate = { route -> sidebarNavigate(navController, route) },
        onNavigateToContent = {
            try { heroPlayButtonFocusRequester.requestFocus() }
            catch (_: Exception) { try { firstRowFocusRequester.requestFocus() } catch (_: Exception) {} }
        },
        modifier = Modifier.focusRequester(sideNavFocusRequester),
    )
    Box(
        modifier = Modifier
            .fillMaxSize().weight(1f).background(Color.Black)
            .onKeyEvent { event ->
                if (event.type == KeyEventType.KeyDown && event.key == Key.Back) {
                    try { sideNavFocusRequester.requestFocus() } catch (_: Exception) {}
                    true
                } else false
            }
    ) {
        // LazyColumn with hero + catalog rows
        content()
    }
}
```

**Key rules:**
1. Sidebar is a sibling Column in a Row, NOT an overlay
2. Content Box handles Back → focuses sidebar
3. Sidebar items handle Right → transfer focus to content
4. Each item has its own FocusRequester — independent focus
5. `focusable() + onFocusChanged + onKeyEvent` — all three required (Trap 39)
6. `clickable()` is additive for touch, not a replacement for key handling

## Pattern B: Lumera Overlay (AVOID — traps focus)

The Lumera-style 4-layer overlay sidebar with `focusGroup()`, HazeState blur, and expand/collapse animation is prone to focus trapping. It was completely replaced by Pattern A in UNSPOOLED. See the git history of `NetflixSideNavigation.kt` for the before/after comparison.

**Pitfalls of the overlay approach:**
- `focusGroup()` creates a boundary without an exit — focus is trapped
- `onPreviewKeyEvent` blocking ALL events when sidebar is expanded breaks D-pad entirely
- Multiple pending-state booleans (collapsePending, pendingContentFocusTransfer, etc.) create race conditions
- HazeState blur adds visual complexity without UX benefit
- Expand/collapse width animation causes layout recomposition jank
- BackHandler intercepts conflict with content navigation

## Pattern C: Settings Rail+Detail (for settings screens only)

For screens with internal navigation (settings), use a two-column rail+detail pattern:

```kotlin
Row(modifier = Modifier.fillMaxSize()) {
    Column(Modifier.width(220.dp).fillMaxHeight()) {
        LazyColumn { /* grouped rail items */ }
    }
    Spacer(Modifier.width(16.dp))
    Box(Modifier.weight(1f).fillMaxHeight()) { /* detail pane */ }
}
```

Each rail item uses `SettingsRailButton` with FocusRequester + onFocused callback. Detail pane moves focus on LEFT key back to rail. NO global event-blocking needed.

## Anti-Pattern: Global Sidebar Wrapper

Do NOT use a `NavigationRailWrapper` that wraps every screen with a shared sidebar. Each screen should own its own layout. The home screen has the sidebar; other screens (Search, Details, Player) are standalone.
