import SwiftUI /// A custom seek bar with independent drag state to prevent timer/thumb fighting during scrubs. struct SiriSeekBar: View { @Binding var value: Double // 0.0 to 1.0, bound to playback progress var onSeek: (Double) -> Void // Called when drag ends with final position var onDragChanged: ((Double) -> Void)? = nil // Called continuously during drag @State private var isDragging: Bool = false @State private var dragValue: Double = 0.0 var body: some View { GeometryReader { geo in let width = geo.size.width let currentVal = isDragging ? dragValue : value let fillWidth = max(0, min(width, CGFloat(currentVal) * width)) ZStack(alignment: .leading) { // 1. Background track Capsule() .fill(Color.white.opacity(0.2)) .frame(height: isDragging ? 8 : 4) // 2. Fill track Capsule() .fill(Color.white) .frame(width: fillWidth, height: isDragging ? 8 : 4) // 3. Scrubber thumb (grows when dragging) Circle() .fill(Color.white) .frame(width: isDragging ? 18 : 8, height: isDragging ? 18 : 8) .offset(x: fillWidth - (isDragging ? 9 : 4)) .shadow(color: .black.opacity(0.4), radius: 3, x: 0, y: 2) } .frame(height: 30) .contentShape(Rectangle()) .gesture( DragGesture(minimumDistance: 0) .onChanged { gesture in let pct = max(0, min(1, Double(gesture.location.x / width))) if !isDragging { isDragging = true dragValue = value } dragValue = pct onDragChanged?(pct) } .onEnded { gesture in let pct = max(0, min(1, Double(gesture.location.x / width))) dragValue = pct isDragging = false onSeek(pct) } ) .animation(.easeInOut(duration: 0.15), value: isDragging) } .frame(height: 30) } }