ai-dispatch 10.38.0

Multi-AI CLI team orchestrator
// Theme texture overlays — scanlines, dither, accent grid, sweep.
// Exports: TextureOverlay.

import SwiftUI

struct TextureOverlay: View {
    @Environment(\.theme) private var theme
    @State private var sweepOffset: CGFloat = 0
    @Environment(\.accessibilityReduceMotion) private var reduceMotion

    var body: some View {
        GeometryReader { geo in
            ZStack {
                switch theme.overlay {
                case .scanline:
                    scanlines(in: geo.size)
                    accentGrid(in: geo.size)
                    if !reduceMotion { sweep(in: geo.size) }
                case .dither:
                    dither(in: geo.size)
                case .none:
                    EmptyView()
                }
            }
        }
        .allowsHitTesting(false)
        .onAppear { startSweep() }
    }

    private func scanlines(in size: CGSize) -> some View {
        Canvas { context, canvasSize in
            var y: CGFloat = 0
            while y < canvasSize.height {
                let rect = CGRect(x: 0, y: y, width: canvasSize.width, height: 1)
                context.fill(Path(rect), with: .color(.black.opacity(0.22)))
                y += 4
            }
        }
    }

    private func accentGrid(in size: CGSize) -> some View {
        Canvas { context, canvasSize in
            let step: CGFloat = 72
            var x: CGFloat = 0
            while x < canvasSize.width {
                var y: CGFloat = 0
                while y < canvasSize.height {
                    let rect = CGRect(x: x, y: y, width: 1, height: 1)
                    context.fill(Path(rect), with: .color(theme.accent.opacity(0.06)))
                    y += step
                }
                x += step
            }
        }
    }

    private func sweep(in size: CGSize) -> some View {
        LinearGradient(
            colors: [.clear, theme.accent.opacity(0.06), .clear],
            startPoint: .top,
            endPoint: .bottom
        )
        .frame(height: 80)
        .offset(y: sweepOffset)
        .frame(height: size.height, alignment: .top)
        .clipped()
    }

    private func dither(in size: CGSize) -> some View {
        Canvas { context, canvasSize in
            var y: CGFloat = 0
            while y < canvasSize.height {
                var x: CGFloat = (Int(y) / 2) % 2 == 0 ? 0 : 2
                while x < canvasSize.width {
                    let rect = CGRect(x: x, y: y, width: 2, height: 2)
                    context.fill(Path(rect), with: .color(.white.opacity(0.06)))
                    x += 4
                }
                y += 2
            }
        }
    }

    private func startSweep() {
        guard theme.overlay == .scanline, !reduceMotion else { return }
        withAnimation(.linear(duration: 7).repeatForever(autoreverses: false)) {
            sweepOffset = 900
        }
    }
}