# Android TV Sidebar/Navbar Patterns

Source implementations: Lumera-0.1.8-beta (NavDrawer.kt, TopNavigationBar.kt), NuvioTV-0.7.0-beta (ModernSidebarBlurPanel.kt, SidebarNavigation.kt)

## Lumera Z-Index Layer Stack (NavDrawer)

Four layers stacked with zIndex:

```
Box(fillMaxSize) {
    // LAYER 1 (z=0): Main content area
    Box(Modifier.fillMaxSize().zIndex(0f)) { content() }
    
    // LAYER 2 (z=1): Static gradient mask — always visible on left edge
    Box(Modifier.width(400.dp).fillMaxHeight().zIndex(1f)
        .background(Brush.horizontalGradient(...)))
    
    // LAYER 3 (z=1.5): Dynamic expansion shadow — visible when nav focused
    AnimatedVisibility(visible = isMenuFocused, Modifier.zIndex(1.5f))
    
    // LAYER 4 (z=2): Interactive nav items — permanent thin rail + expanded drawer
    Box(Modifier.width(width).fillMaxHeight().zIndex(2f)) { navItems() }
}
```

## Permanent thin rail → expanded drawer

Collapsed: 80dp rail with icons only. Expanded: 260dp with icons + labels + accent bars.

```kotlin
val width by animateDpAsState(
    targetValue = if (isExpanded) 260.dp else 80.dp,
    animationSpec = tween(280))
val labelAlpha by animateFloatAsState(
    targetValue = if (isExpanded) 1f else 0f,
    animationSpec = tween(180))
```

Content area must reserve space for the permanent rail:
```kotlin
.padding(start = CollapsedWidth)  // 80dp — rail is always visible
```

## Focus handling

When collapsed and LEFT is pressed on content with nowhere left to go → expand sidebar:
```kotlin
.onKeyEvent { event ->
    if (!isExpanded && event.key == Key.DirectionLeft) {
        if (focusManager.moveFocus(FocusDirection.Left)) true  // moved within content
        else { isExpanded = true; ...; true }  // open sidebar
    }
}
```

Block content D-pad when sidebar expanded via `onPreviewKeyEvent`:
```kotlin
.onPreviewKeyEvent { event ->
    if (sidebarBlocksContentKeys && event.key in blockedKeys) true  // eat
    else false
}
```

Settings screens must add their own `onPreviewKeyEvent` to prevent sidebar from intercepting when rail has focus:
```kotlin
// On settings outer Box:
.onPreviewKeyEvent { event ->
    when (event.key) {
        Key.DirectionLeft, Key.DirectionDown, Key.DirectionUp -> !allowDetailFocus
        else -> false
    }
}
// When rail has focus (allowDetailFocus=false), LEFT/DOWN/UP are eaten
// When detail pane has focus (allowDetailFocus=true), events pass through
```

## Nav item styling (Lumera + NuvioTV combined)

```kotlin
// Selected: white bg, dark text
// Focused: white 0.16 alpha bg, white 0.40 border
// Accent bar: 4dp colored bar on left edge, animated width 0→4dp on selection
// Text: slide-in from left (-12f→0) + fade (0→1) on focus
// Icon circle: 42dp, dark bg when selected, gray when normal
```

## Auto-collapse timer

```kotlin
LaunchedEffect(isExpanded, focusedDrawerIndex, lastActivityTime) {
    if (!isExpanded || collapsePending) return@LaunchedEffect
    delay(3_000L)  // 3s idle
    collapsePending = true
}
```

## Focus transfer on collapse

```kotlin
LaunchedEffect(pendingContentFocusTransfer, isExpanded, collapsePending) {
    delay(48L)
    runCatching { contentFocusRequester.requestFocus() }
    pendingContentFocusTransfer = false
}
```

## D-Pad repeat gating (for LazyRow smooth scrolling)

```kotlin
class DpadRepeatGate(
    horizontalRepeatIntervalMs: Long = 120L,
    verticalRepeatIntervalMs: Long = 200L
) {
    fun shouldConsume(event: KeyEvent): Boolean { ... }
}
```
