# Circular Rail Navigation for Android TV

Patterns for implementing a curved, animated left-side navigation rail in Jetpack Compose for TV.

## Component Architecture

### Core Files
- `CircularRailNavigation.kt` — Shape-drawn curved rail with animated items
- `NavigationRailWrapper.kt` — Conditional renderer (Classic vs Circular)
- `NavigationStyle.kt` — Enum: `CLASSIC` vs `CIRCULAR_EXPERIMENTAL`
- `PreferencesManager.kt` — DataStore key for `navigationStyle`

### Key Design Decisions

**1. Shape-based curved paths (NOT Canvas)**
- Use `Shape.createOutline()` with `cubicTo()` for curved left edge
- Path coordinates calculated from `Size` parameter in `createOutline()`
- Apply via `Modifier.clip(CurvedRailShape)` + `Modifier.background()`
- Canvas composable (`androidx.compose.ui.graphics.Canvas`) is for bitmap drawing, NOT Compose layout

**2. State management**
- `navigationStyle` preference in DataStore: `CLASSIC` (default) vs `CIRCULAR_EXPERIMENTAL`
- `SettingsViewModel` exposes `navigationStyle` in UI state
- `NavigationRailWrapper` reads preference and conditionally renders rail

**3. Focus handling**
- Each rail item needs: `focusRequester` + `focusProperties` + `onFocusChanged`
- Selected item gets visual emphasis (scale + glow)
- D-pad safe with explicit `focusProperties` for up/down neighbors

**4. Animation**
- Width transitions use `animateDpAsState` for smooth expand/collapse
- Labels fade via `animateFloatAsState` + `AnimatedVisibility`
- Item scale via `animateFloatAsState`

## Implementation Pattern

```kotlin
// Curved rail shape
class CurvedRailShape(private val expanded: Boolean) : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val curveDepth = with(density) { 40f.dp.toPx() } * if (expanded) 2.5f else 1.5f
        val path = Path().apply {
            moveTo(0f, 0f)
            lineTo(size.width, 0f)
            lineTo(size.width, size.height)
            lineTo(0f, size.height)
            cubicTo(curveDepth, size.height * 0.3f,
                    curveDepth, size.height * 0.7f,
                    0f, size.height * 0.5f)
            close()
        }
        return Outline.Generic(path)
    }
}

// Conditional wrapper
@Composable
fun NavigationRailWrapper(
    navController: NavController,
    selectedRoute: String,
    sidebarItems: List<NexSidebarItem>,
    circularItems: List<CircularNavItem>,
    content: @Composable () -> Unit,
) {
    val useCircular = /* read from SettingsViewModel */
    if (useCircular) {
        CircularRailNavigation(items = circularItems, ...)
    } else {
        NexSidebar(items = sidebarItems, ...)
    }
    // content in weight(1f) Box
}
```

## Pitfalls

**1. Shape.createOutline() signature**
- `(Size, LayoutDirection, Density) -> Outline` — NOT `(Size, LayoutDirection, PaddingValues)`
- Use `with(density) { dpValue.toPx() }` for dp→px conversion
- Return `Outline.Generic(path)`, NOT `Outline.Rectangle`

**2. LocalNavController does not exist**
- This Compose Navigation version has no `LocalNavController`
- Pass `NavController` as explicit parameter to composables
- Default parameter values with `LocalNavController.current` will NOT compile

**3. Canvas composable is for bitmap drawing**
- `androidx.compose.ui.graphics.Canvas` draws on bitmaps, NOT Compose layout
- For Compose layout drawing, use `Shape.createOutline()` + `Modifier.clip()`
- `Modifier.drawWithContent {}` is for DrawScope, not Box

**4. Modifier.weight() is internal in Compose TV**
- Use `androidx.compose.foundation.layout.weight` import
- Or use `androidx.compose.foundation.layout.Row` with `weight` parameter

**5. Feature flag wiring**
- Toggle must be in Settings screen via `SettingsViewModel`
- `PreferencesManager` needs `navigationStyle` DataStore key
- Wrapper must swap `NexSidebar` ↔ `CircularRailNavigation` based on flag
- Default is `CLASSIC` — `CIRCULAR_EXPERIMENTAL` is opt-in

**6. Hide during fullscreen playback**
- When player is fullscreen, always show `NexSidebar` (or hide rail entirely)
- Check `isPlaying` state before rendering circular rail

## NexStream-Specific Implementation
For the NexStream (UNSPOOLED) implementation with full integration across 6 screens, see `nexstream-android-tv-build/references/circular-rail-navigation.md`. That reference includes the complete integration status, `NavigationRailWrapper` pattern, and all wiring details.
