# Netflix-Style Sidebar Simplification — Remove HazeState & Pending-State Bugs

## Problem

A 502-line Netflix-style sidebar (`NetflixSideNavigation.kt`) with Haze blur effects, `focusGroup()`, `BackHandler` intercepts, and 4 pending-state booleans (`collapsePending`, `pendingContentFocusTransfer`, `pendingSidebarFocusRequest`, `focusedDrawerIndex`) becomes unmaintainable. D-pad navigation is broken because content-area `onPreviewKeyEvent` blocks ALL key events when the sidebar is expanded. Race conditions between the 4 state variables cause focus to jump unpredictably.

## Root Causes

1. **HazeState** (`dev.chrisbanes.haze`) adds visual complexity with zero UX benefit on TV — the blur tint is invisible on most TV panels.
2. **`focusGroup()`** on the sidebar column restricts natural D-pad flow between sidebar and content.
3. **4 pending-state booleans** create a combinatorial explosion of possible states. `collapsePending`, `pendingContentFocusTransfer`, `pendingSidebarFocusRequest`, and `focusedDrawerIndex` interact via `LaunchedEffect` blocks that race each other.
4. **`BackHandler` intercepts** steal the Back key from content navigation. Two separate `BackHandler` instances fight for control.
5. **Content-area `onPreviewKeyEvent`** uses a `derivedStateOf { isExpanded && !collapsePending }` guard to eat ALL D-pad events when expanded — including UP/DOWN/ENTER, not just LEFT/RIGHT.
6. **`CompositionLocalProvider`** with `LocalSidebarExpanded` and `LocalContentFocusRequester` leaks sidebar state into content composables that don't need it.

## Fix: Simple Row Layout (278 lines, from 502)

Replace the entire sidebar with a basic `Row` containing a sidebar `Column` and content `Box`:

```kotlin
@Composable
fun NetflixSideNavigation(
    selectedRoute: String,
    onNavigate: (String) -> Unit,
    onNavigateToContent: (() -> Unit)? = null,
    content: @Composable () -> Unit,
) {
    var isExpanded by remember { mutableStateOf(false) }

    val actualWidth by animateDpAsState(
        targetValue = if (isExpanded) ExpandedWidth else CollapsedWidth,
        animationSpec = tween(280)
    )
    val labelAlpha by animateFloatAsState(
        targetValue = if (isExpanded) 1f else 0f,
        animationSpec = tween(180)
    )

    LaunchedEffect(selectedRoute) { isExpanded = false }

    Row(modifier = Modifier.fillMaxSize()) {
        // ── Sidebar rail (left) ──
        Column(
            modifier = Modifier
                .width(actualWidth)
                .fillMaxHeight()
                .background(backgroundColor)
                .padding(vertical = 20.dp, horizontal = 10.dp),
            verticalArrangement = Arrangement.spacedBy(6.dp)
        ) {
            if (isExpanded) {
                Text("LOGO", style = titleLarge, color = primary)
                Spacer(Modifier.height(8.dp))
            }
            NAV_ITEMS.forEach { item ->
                NetflixNavItem(
                    item = item,
                    isSelected = item.route == selectedRoute,
                    isExpanded = isExpanded,
                    labelAlpha = labelAlpha,
                    onFocus = { if (!isExpanded) isExpanded = true },
                    onClick = { onNavigate(item.route) },
                    onNavigateRight = {
                        isExpanded = false
                        onNavigateToContent?.invoke()
                    },
                )
            }
        }

        // ── Content area (right) — plain Box, NO key interception ──
        Box(Modifier.weight(1f).fillMaxHeight()) {
            content()
        }
    }
}
```

## Nav Item Pattern — Triple Requirement

Each nav item uses the TV D-pad triple requirement (Trap 39):
```kotlin
@Composable
private fun NetflixNavItem(
    item: NavDestination,
    isSelected: Boolean,
    isExpanded: Boolean,
    labelAlpha: Float,
    onFocus: () -> Unit,
    onClick: () -> Unit,
    onNavigateRight: () -> Unit,
) {
    var isFocused by remember { mutableStateOf(false) }

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .height(56.dp)
            .focusable()                          // 1. make D-pad reachable
            .onFocusChanged { state ->            // 2. track focus for visuals
                isFocused = state.isFocused
                if (state.isFocused) onFocus()
            }
            .onPreviewKeyEvent { event ->         // 3. handle key events
                if (event.type == KeyEventType.KeyDown) {
                    when (event.key) {
                        Key.DirectionCenter, Key.Enter -> { onClick(); true }
                        Key.DirectionRight -> { onNavigateRight(); true }
                        else -> false
                    }
                } else false
            },
        verticalAlignment = Alignment.CenterVertically,
    ) {
        // Icon circle + label
        Box(Modifier.size(42.dp).clip(CircleShape).background(iconBg)) {
            Icon(item.icon, tint = contentColor, modifier = Modifier.size(20.dp))
        }
        if (isExpanded || isFocused) {
            Text(item.label, color = contentColor.copy(alpha = labelAlpha))
        }
    }
}
```

## Style Rules

- **Collapsed width**: 80dp — shows icons only
- **Expanded width**: 260dp — shows icons + labels
- **Width animation**: `tween(280)` 
- **Label fade**: `tween(180)` on alpha 0→1
- **Selected item**: White background (`Color.White`), dark text
- **Focused item**: White background at 16% alpha
- **Accent bar**: 4dp wide, 32dp tall, left edge, shown when selected AND expanded
- **Background**: Dark elevated background with 96% opacity

## What to REMOVE

| Removed | Reason |
|---------|--------|
| `HazeState` + `haze()` + `hazeChild()` | Visual overhead, zero TV benefit |
| `focusGroup()` | Blocks natural D-pad cross-column flow |
| `collapsePending` boolean | Race condition with `isExpanded` |
| `pendingContentFocusTransfer` | Race condition with collapse |
| `pendingSidebarFocusRequest` | Race condition with expand |
| `focusedDrawerIndex` + `lastActivityTime` | Auto-collapse timer complexity |
| Two `BackHandler` instances | Steals Back from content |
| Content-area `onPreviewKeyEvent` eating all keys | Breaks content navigation |
| `CompositionLocalProvider` with sidebar state | Leaks state into content |
| `derivedStateOf` for key blocking | Unnecessary derived computation |
| `graphicsLayer` clip + transform | Standard `clip()` is sufficient |
| `Brush.verticalGradient` background | Solid background works better on TV |

## Build Verification

After simplifying, the sidebar file should drop from ~500 lines to ~280 lines. The key test: navigating UP/DOWN within a settings rail should move between rail items without jumping to the sidebar.

## Related Patterns

- **`references/android-tv-netflix-sidebar-pattern.md`** — Preferred approach: fixed 48dp icon rail with NO expansion. Simpler, fewer lines, zero race conditions. Use this for new builds.
- `android-project-build` Trap 39: D-pad focus TV triple requirement  
- `references/settings-focus-containment.md`: Settings D-pad flow within parent sidebar
- `references/android-tv-focus-navigation-patterns.md`: Sidebar→content focus handoff

## Preference

**For new implementations, prefer the fixed-rail pattern over expand/collapse.** The expand/collapse approach documented above (80dp→260dp with labels) adds complexity for marginal UX benefit. Multiple Android TV apps have shipped fixed 48dp rails successfully. Only use expand/collapse if the user explicitly requests labeled nav items.
