# Android TV Netflix-Style Fixed Sidebar Pattern

## The Pattern

A **fixed 48dp icon rail** on the left side of every main screen. No expansion, no labels, no width animation. Each icon is an independently focusable 36dp box. The sidebar sits inside a `Row{}` alongside the content `Box{}`.

This is the battle-tested pattern from the homeflix-tv-app reference. All expand/collapse sidebar implementations (80dp→260dp, HazeState blur, auto-collapse timers, pending-state booleans) have proven unreliable across multiple Android TV apps. The fixed rail is simpler, never breaks focus, and matches the Netflix TV UX exactly.

## Architecture

```
Row(modifier = Modifier.fillMaxSize()) {
    // ── Fixed sidebar rail (48dp, always visible) ──
    NetflixSideNavigation(
        selectedRoute = "home",
        onNavigate = { route -> /* navigate */ },
        onNavigateToContent = { /* focus hero/content */ },
    )
    
    // ── Content area ──
    Box(
        modifier = Modifier
            .weight(1f)
            .fillMaxSize()
            .onKeyEvent { event ->
                // Back → focus sidebar
                if (event.key == Key.Back) { sideNavFocusRequester.requestFocus(); true }
                else false
            }
    ) {
        // Screen content (LazyColumn, etc.)
    }
}
```

## Sidebar Composable (173 lines)

```kotlin
@Composable
fun NetflixSideNavigation(
    selectedRoute: String,
    onNavigate: (String) -> Unit,
    modifier: Modifier = Modifier,
    onNavigateToContent: (() -> Unit)? = null,
) {
    val navItems = listOf(
        NavItem(Icons.Default.Search, "search", "Search"),
        NavItem(Icons.Default.Home, "home", "Home"),
        NavItem(Icons.Default.Movie, "movies", "Movies"),
        NavItem(Icons.Default.Tv, "tv-shows", "TV Shows"),
        NavItem(Icons.Default.BookmarkBorder, "my-list", "My List"),
        NavItem(Icons.Default.Settings, "settings", "Settings"),
    )

    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))
            }
        }
    }
}
```

## Individual Nav Icon

```kotlin
@Composable
private fun NetflixNavIcon(
    item: NavItem,
    isSelected: Boolean,
    onClick: () -> Unit,
    onNavigateRight: (() -> Unit)? = null,
) {
    var isFocused by remember { mutableStateOf(false) }
    val focusRequester = remember { FocusRequester() }

    // Scale animation on focus (Netflix-style 1.0→1.5)
    val scale by animateFloatAsState(
        targetValue = if (isFocused) 1.5f else 1.0f,
        animationSpec = tween(200),
    )

    val accent = NuvioTheme.colors.Accent

    Box(
        modifier = Modifier
            .size(36.dp)
            .scale(scale)
            .background(
                color = when {
                    isSelected -> accent          // accent bg when selected
                    isFocused -> Color.White.copy(alpha = 0.20f)  // subtle highlight when focused
                    else -> Color.Transparent
                },
                shape = RoundedCornerShape(6.dp),
            )
            .focusRequester(focusRequester)
            .onFocusChanged { isFocused = it.isFocused }
            .focusable()                          // TRIPLE PATTERN: 1. focusable
            .onKeyEvent { keyEvent ->             // 2. handle keys
                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),        // 3. clickable for non-D-pad input
        contentAlignment = Alignment.Center,
    ) {
        Icon(
            imageVector = item.icon,
            contentDescription = item.contentDescription,
            tint = Color.White,
            modifier = Modifier.size(20.dp),
        )
    }
}
```

## D-pad Navigation Rules

| Action | Behavior |
|--------|----------|
| **Down/Up** on sidebar item | Move between nav icons |
| **Center/Enter** on sidebar item | Navigate to that screen |
| **Right** on sidebar item | Exit to content (focus hero/play button) |
| **Back** on content | Return focus to sidebar |
| **Left** on content (when no focusable to left) | Focus sidebar |

## Screens Without Sidebar

Detail screens (Player, Details, Settings sub-panels, Profiles) do NOT use the sidebar. They are full-screen composables with their own Back handling. The NavHost backstack handles returning to the previous screen.

## Key Differences from Expand/Collapse Pattern

| Aspect | Fixed Rail (correct) | Expand/Collapse (broken) |
|--------|---------------------|--------------------------|
| Width | Always 48dp | 80dp→260dp animated |
| Labels | None | Shown when expanded |
| Focus states | 2 (selected, focused) | 3+ (collapsed focused, expanded focused, selected) |
| State variables | 0 | 4+ (isExpanded, collapsePending, ...) |
| Race conditions | Impossible | Probable |
| Content key blocking | None | Required to prevent focus leaks |
| Lines of code | ~173 | ~280-500 |

## Back Key Behavior on Home Screen

The content Box handles Back to focus the sidebar:

```kotlin
Box(
    modifier = Modifier
        .weight(1f)
        .onKeyEvent { keyEvent ->
            if (keyEvent.type == KeyEventType.KeyDown && keyEvent.key == Key.Back) {
                sideNavFocusRequester.requestFocus()
                true
            } else false
        }
)
```

This lets the user: Home → press Back → sidebar gets focus → press Center on another nav item → navigate. Standard Netflix TV behavior.

## When NOT to Use This Pattern

- For screens that need a persistent settings rail (like Settings with 10+ items): use the rail+detail master/detail pattern instead
- For profiles/onboarding: use the two-column layout pattern
- For player: full-screen, no sidebar
