// // WheelNavBar.swift // tg_test // import SwiftUI struct WheelNavBar: View { @Binding var selectedIndex: Int let tabs: [TabItem] @GestureState private var dragTranslation: CGFloat = 0 @State private var activeDragIndex: Int = 0 // Haptic feedback generator private let feedback = UIImpactFeedbackGenerator(style: .medium) private let itemSpacing: CGFloat = 62 var body: some View { let currentTab = tabs[min(max(selectedIndex, 0), tabs.count - 1)] VStack(spacing: 8) { // MARK: - Wheel Dial Track GeometryReader { geometry in let centerPoint = geometry.size.width / 2 let effectiveIndex = Double(selectedIndex) - Double(dragTranslation / itemSpacing) ZStack { // Central Selector Ring Glow Circle() .fill(currentTab.color.opacity(0.15)) .frame(width: 56, height: 56) .overlay( Circle() .stroke(currentTab.color, lineWidth: 2.5) ) .position(x: centerPoint, y: 32) .shadow(color: currentTab.color.opacity(0.4), radius: 10) // Rotary Wheel Items ForEach(tabs) { tab in let distance = Double(tab.id) - effectiveIndex let xOffset = distance * itemSpacing // Arc wheel curvature (quadratic Y offset for wheel effect) let yOffset = pow(distance, 2) * 2.2 // Scaling factor (center item is largest) let scale = max(0.55, 1.15 - min(abs(distance) * 0.18, 0.6)) // Opacity falloff let opacity = max(0.25, 1.0 - min(abs(distance) * 0.22, 0.75)) // 3D Y Rotation Angle for wheel perspective let rotationY = min(max(distance * -18.0, -60.0), 60.0) let isSelected = tab.id == selectedIndex VStack(spacing: 2) { ZStack { Circle() .fill(isSelected ? tab.color : Color(.tertiarySystemFill)) .frame(width: 44, height: 44) .shadow(color: isSelected ? tab.color.opacity(0.5) : .clear, radius: 6) Image(systemName: tab.icon) .font(.system(size: isSelected ? 20 : 16, weight: isSelected ? .bold : .medium)) .foregroundStyle(isSelected ? .white : .primary) if let badge = tab.badge { Text(badge) .font(.system(size: 9, weight: .bold)) .foregroundStyle(.white) .padding(4) .background(Color.red) .clipShape(Circle()) .offset(x: 14, y: -14) } } } .scaleEffect(scale) .opacity(opacity) .rotation3DEffect( .degrees(rotationY), axis: (x: 0.0, y: 1.0, z: 0.0), perspective: 0.5 ) .position(x: centerPoint + xOffset, y: 32 + yOffset) .onTapGesture { withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) { selectedIndex = tab.id feedback.impactOccurred() } } } } } .frame(height: 64) .contentShape(Rectangle()) .gesture( DragGesture() .updating($dragTranslation) { value, state, _ in state = value.translation.width } .onChanged { value in let calcIndex = Double(selectedIndex) - Double(value.translation.width / itemSpacing) let rounded = min(max(Int(round(calcIndex)), 0), tabs.count - 1) if rounded != activeDragIndex { activeDragIndex = rounded feedback.impactOccurred() } } .onEnded { value in let velocity = value.predictedEndTranslation.width / itemSpacing let calcIndex = Double(selectedIndex) - Double(value.translation.width / itemSpacing) - (velocity * 0.2) let targetIndex = min(max(Int(round(calcIndex)), 0), tabs.count - 1) withAnimation(.spring(response: 0.35, dampingFraction: 0.75)) { selectedIndex = targetIndex } } ) } .padding(.vertical, 6) .background( RoundedRectangle(cornerRadius: 28, style: .continuous) .fill(.ultraThinMaterial) .shadow(color: Color.black.opacity(0.12), radius: 16, x: 0, y: 8) .overlay( RoundedRectangle(cornerRadius: 28, style: .continuous) .stroke(Color.white.opacity(0.2), lineWidth: 1) ) ) .padding(.horizontal, 16) .padding(.bottom, 8) } }