# Material3 → tv.material3 Hybrid Cleanup Guide

## When to convert

**The rule:** if the user can D-pad onto it and press Select — it MUST be `tv.material3.Surface` with `ClickableSurfaceDefaults`. If it's non-interactive (decor, text, icon, loader) — `material3` is fine.

## Audit methodology

1. Grep all material3 imports: `grep -rn "import androidx.compose.material3" --include='*.kt'`
2. Classify each file by whether it contains D-pad-interactive components
3. Interactive: `Button`, `TextButton`, `OutlinedTextField`, `Switch`, `Checkbox`, `clickable` on Surfaces not using tv.material3
4. Non-interactive: `Text`, `Icon`, `CircularProgressIndicator`, `MaterialTheme`, `AlertDialog` (modal-specific), `LocalTextStyle`

## Component replacement map

| Mobile (`material3`) | TV replacement (`tv.material3`) | Notes |
|----------------------|--------------------------------|-------|
| `Button(onClick=...)` | `Surface(onClick=..., colors=ClickableSurfaceDefaults.colors(...), scale=..., border=...)` | No `Button` in tv-material — use Surface |
| `TextButton(onClick=...)` | Same as above | Use accent container color |
| `OutlinedTextField` | Keep material3, wrap in Box with manual focus border | Spec Phase 10F pattern |
| `TextField` (internal) | Use available `TvTextField` which wraps `BasicTextField` with focus border | Already built in `TvFormComponents.kt` |

## Correct TV Surface pattern for interactive elements

```kotlin
androidx.tv.material3.Surface(
    onClick = { /* action */ },
    colors = ClickableSurfaceDefaults.colors(
        containerColor = NuvioTheme.colors.Accent,
        focusedContainerColor = NuvioTheme.colors.AccentPulse,
    ),
    scale = ClickableSurfaceDefaults.scale(focusedScale = 1.05f),
    border = ClickableSurfaceDefaults.border(
        focusedBorder = Border(
            border = BorderStroke(2.dp, NuvioTheme.colors.FocusRing),
            shape = RoundedCornerShape(8.dp),
        )
    ),
    shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(8.dp)),
) {
    Text("Button Label", color = NuvioTheme.colors.OnSecondary)
}
```

**Do NOT** use `Modifier.clickable` + manual `isFocused` tracking on interactive TV surfaces.

## Files fixed (2026-06-03, expanded Phase 10)

### P0 — Structural (Box+clickable+focusable → tv.material3.Surface)
- **GlassSettingsRailItem.kt**: Entirely rebuilt. Box+clickable+focusable → `tv.material3.Surface` + `ClickableSurfaceDefaults`. Removed `glassFocusGlow`, `glassSurface`, `rememberTvFocusState`, manual scale animation. Min height 56dp. Focus tracking via `Modifier.onFocusChanged`.
- **DashboardEditor.kt**: DashboardRowItem (outer Row), toggle ON/OFF, move buttons ▲▼, Back button — all Box/Row/Text with `.clickable()` → `tv.material3.Surface`.

### P1 — Import conflicts
- **SearchScreen.kt**: Replaced `import androidx.compose.material3.*` wildcard with explicit imports: `OutlinedTextField`, `OutlinedTextFieldDefaults`, `CircularProgressIndicator` (material3) + `Text`, `Icon` (tv.material3).
- **GlassSidebar.kt**: `material3.Icon/Text/MaterialTheme` → `tv.material3` equivalents. Nav item labels/icons now use correct library inside existing `tv.material3.Surface`.

### P2 — Minor fixes
- **SettingsDesignSystem.kt**: Removed explicit `material3.Icon` import — `Icon` now resolves via `import androidx.tv.material3.*` wildcard.
- **ProfileCreateScreen.kt**: `PinDigitInput` wrapped in Box with manual focus border: `3.dp FocusRing` focused, `1.dp Border` unfocused. OutlinedTextField internal borders set to `Color.Transparent`.

### P3 — Cosmetic + cleanup
- **SharedTrailerOverlay.kt**: `material3.Button` → `tv.material3.Surface` + `ClickableSurfaceDefaults` (accent container, scale 1.05).
- **UnspooledHomeScreen.kt**: Removed stale `import material3.TextButton`.
- **CatalogManagerScreen.kt**: AlertDialog `confirmButton`/`dismissButton`: `Text+Modifier.clickable` → `tv.material3.Surface`.

### Import conflict that was already correct
SearchScreen (OutlinedTextField uses NuvioTheme colors, no change needed), 56 files already using tv.material3, SharedTrailerOverlay Button in Dialog (touch-only, but converted per spec).

## Conversion patterns (Phase 10 — all patterns used in 2026-06-03 cleanup)

### Pattern 1: Box+clickable+focusable → tv.material3.Surface

**Diagnostic:** Grep for `.clickable` and `.focusable` on the same modifier chain, or Box/Row with both.
**Apply to:** Settings rail items, dashboard rows, filter chips, any interactive element not yet on tv.material3.

```kotlin
// BEFORE (banned — bypasses TV focus system)
Box(
    modifier = Modifier
        .focusable(interactionSource = interactionSource)
        .clickable(interactionSource = interactionSource, indication = null) { onClick() }
) { ... }

// AFTER
Surface(
    onClick = onClick,
    colors = ClickableSurfaceDefaults.colors(
        containerColor = NuvioTheme.colors.BackgroundCard,
        focusedContainerColor = NuvioTheme.colors.FocusBackground,
    ),
    scale = ClickableSurfaceDefaults.scale(focusedScale = 1.03f),
    border = ClickableSurfaceDefaults.border(
        focusedBorder = Border(
            border = BorderStroke(2.dp, NuvioTheme.colors.FocusRing),
            shape = RoundedCornerShape(12.dp),
        ),
    ),
    shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(12.dp)),
) { /* content */ }
```

**Note:** Focus callbacks (`onFocused`) need explicit `Modifier.onFocusChanged { isFocused = it.isFocused }` + `LaunchedEffect(isFocused)` since ClickableSurfaceDefaults manages its own focus internally and doesn't expose `rememberTvFocusState()`.

### Pattern 2: AlertDialog button conversion

**Diagnostic:** `confirmButton/dismissButton` using `Text + Modifier.clickable`.
**Apply to:** Any AlertDialog with D-pad-reachable buttons.

```kotlin
// BEFORE
confirmButton = {
    Text("Reset", modifier = Modifier.clickable { ... }.padding(12.dp))
}

// AFTER
confirmButton = {
    Surface(
        onClick = { ... },
        colors = ClickableSurfaceDefaults.colors(
            containerColor = NuvioTheme.colors.Error.copy(alpha = 0.15f),
            focusedContainerColor = NuvioTheme.colors.Error.copy(alpha = 0.3f),
        ),
        scale = ClickableSurfaceDefaults.scale(focusedScale = 1.05f),
        shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(8.dp)),
    ) {
        Text("Reset", color = NuvioTheme.colors.Error, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp))
    }
}
```

### Pattern 3: OutlinedTextField focus border wrapping (Phase 10F)

**Diagnostic:** Any screen with text input that uses `OutlinedTextField` (no TV equivalent).
**Apply to:** SearchScreen, ProfileCreateScreen PinDigitInput, any future text input screen.

```kotlin
var isFocused by remember { mutableStateOf(false) }
val focusBorderColor = if (isFocused) NuvioTheme.colors.FocusRing else NuvioTheme.colors.Border
val focusBorderWidth = if (isFocused) 3.dp else 1.dp

Box(
    modifier = modifier
        .clip(RoundedCornerShape(12.dp))
        .border(focusBorderWidth, focusBorderColor, RoundedCornerShape(12.dp)),
) {
    OutlinedTextField(
        modifier = Modifier.onFocusChanged { isFocused = it.isFocused },
        colors = OutlinedTextFieldDefaults.colors(
            focusedBorderColor = Color.Transparent,
            unfocusedBorderColor = Color.Transparent,
            // ... other colors
        ),
        // ...
    )
}
```

### Pattern 4: Wildcard import replacement

**Diagnostic:** `import androidx.compose.material3.*` on any file.
**Apply to:** Any screen with a material3 wildcard import.

1. List every material3 component actually used in the file
2. For interactive components (TextField, Button): keep material3 explicit import (no TV equivalent)
3. For Text, Icon, MaterialTheme: switch to `androidx.tv.material3`
4. For non-interactive (CircularProgressIndicator): keep material3 or use tv equivalent

### Pattern 5: Material3 → tv.material3 import replacement (interactive decor inside Surface)

**Diagnostic:** Material3 `Text`/`Icon` imports on files that also use `tv.material3.Surface`.
**Apply to:** Any file where Text/Icon are used INSIDE a tv.material3.Surface (part of touch target).

```kotlin
// BEFORE
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
// ... tv.material3.Surface, tv.material3.ClickableSurfaceDefaults

// AFTER
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
// ... tv.material3.Surface, tv.material3.ClickableSurfaceDefaults
```

### Pattern 6: Import deduplication after partial conversions

**Diagnostic:** Compile errors after adding tv.material3 imports to a file that already had some.
**Fix:** Always `grep -n 'import androidx.tv.material3\.'` after editing to detect duplicates. Remove the lower (newer) duplicates, keep the originals if they're at the correct position in the import block.

## Full audit table (2026-06-03 — 23 files active, 9 D-pad-interactive, 14 decorative)

| File | Material3 Import | D-pad? | Verdict |
|------|-----------------|--------|---------|
| CardMetadataOverlay.kt | MaterialTheme, Text | No | Leave (decor) |
| DebridCacheBadge.kt | Text | No | Leave (decor) |
| PremiumContentCard.kt | Icon, MaterialTheme, Text | No | Leave (overlay, labels) |
| LoadingIndicator.kt | CircularProgressIndicator | No | Leave (spinner) |
| MarqueeText.kt | LocalTextStyle, Text | No | Leave (scrolling) |
| PremiumHeroSection.kt | MaterialTheme, Text | No | Leave (hero text) |
| SourceStatusFilterChip.kt | CircularProgressIndicator | No | Leave (inline spinner) |
| DebridStreamTopBar.kt | Icon | No | Leave (chrome decor) |
| FocusDebugOverlay.kt | Text | No | Leave (debug) |
| AnimeScreen.kt | Text | No | Leave (placeholder) |
| ProfileSelectScreen.kt | Text | No | Leave (display) |
| GlassButton.kt | Text | No | Leave (inside tv.material3.Surface) |
| GlassChipButton.kt | Text | No | Leave (inside tv.material3.Surface) |
| GlassFilterChip.kt | Text | No | Leave (inside tv.material3.Surface) |
| **SharedTrailerOverlay.kt** | Button | No* | CONVERTED — Surface (dialog with root key capture) |
| **UnspooledHomeScreen.kt** | TextButton | Yes | CONVERTED — Surface (P0, prior session) |
| **ProfileCreateScreen.kt** | OutlinedTextField, OutlinedTextFieldDefaults | Yes | CONVERTED — focus border Box (P2) |
| **SearchScreen.kt** | material3.* (wildcard) | Yes | CONVERTED — explicit imports (P1) |
| **GlassSidebar.kt** | Icon, MaterialTheme, Text | Yes | CONVERTED — tv equivalents (P1) |
| **GlassSettingsRailItem.kt** | Icon, MaterialTheme, Text | Yes | CONVERTED — full Surface rebuild (P0) |
| **SettingsDesignSystem.kt** | Icon | Yes | CONVERTED — tv.material3.* resolution (P2) |
| **DashboardEditor.kt** | MaterialTheme, Text | Yes | CONVERTED — full Surface rebuild (P0) |
| **CatalogManagerScreen.kt** | AlertDialog | Yes | CONVERTED — dialog buttons → Surface (P3) |

\* Touch-only inside Dialog, converted per spec requirement.

## Build-after-each enforcement

Do not batch Phase 10 conversions. Build after every file:
```bash
JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 ./gradlew :app:compileDebugKotlin :app:testDebugUnitTest 2>&1 | tail -5
```
If build fails, fix immediately before moving to next file. Common failures: missing `clip` import, duplicate imports after conversion, removed imports still needed.

- All interactive TV surfaces use `NuvioTheme.colors.FocusRing` (theme-aware)
- Focus background uses `NuvioTheme.colors.FocusBackground`
- Never hardcode `Color.White` or `rgba(...)` for focus

## Touch target

- TV minimum focus target: **56dp** (not 48dp mobile default)
- For inputs/buttons: use `metrics.inputHeight` (44-52dp per resolution tier)
