import SwiftUI // MARK: - Root tab container with persistent mini-player overlay struct RootTabView: View { @EnvironmentObject var authStore: AuthStore @EnvironmentObject var audioPlayer: AudioPlayerService @State private var selectedTab: Tab = .home @State private var showFullPlayer: Bool = false @State private var readerIsActive: Bool = false @State private var fullPlayerDragOffset: CGFloat = 0 enum Tab: Hashable { case home, library, browse, search, profile } var body: some View { ZStack(alignment: .bottom) { TabView(selection: $selectedTab) { HomeView() .tabItem { Label("Home", systemImage: "house.fill") } .tag(Tab.home) LibraryView() .tabItem { Label("Library", systemImage: "book.pages.fill") } .tag(Tab.library) BrowseView() .tabItem { Label("Discover", systemImage: "sparkles") } .tag(Tab.browse) SearchView() .tabItem { Label("Search", systemImage: "magnifyingglass") } .tag(Tab.search) ProfileView() .tabItem { Label("Profile", systemImage: "person.fill") } .tag(Tab.profile) } // Mini player bar — sits above the tab bar if audioPlayer.isActive && !showFullPlayer && !readerIsActive { MiniPlayerBar(showFullPlayer: $showFullPlayer) .padding(.bottom, 49) .transition(.move(edge: .bottom).combined(with: .opacity)) .animation(.spring(response: 0.35, dampingFraction: 0.8), value: audioPlayer.isActive) } // Full player — slides up from the bottom if showFullPlayer { FullPlayerView(onDismiss: { withAnimation(.spring(response: 0.4, dampingFraction: 0.85)) { showFullPlayer = false fullPlayerDragOffset = 0 } }) .offset(y: max(fullPlayerDragOffset, 0)) .gesture( DragGesture(minimumDistance: 10) .onChanged { value in if value.translation.height > 0 { fullPlayerDragOffset = value.translation.height } } .onEnded { value in let velocity = value.predictedEndTranslation.height - value.translation.height if value.translation.height > 120 || velocity > 400 { withAnimation(.spring(response: 0.4, dampingFraction: 0.85)) { showFullPlayer = false fullPlayerDragOffset = 0 } } else { withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { fullPlayerDragOffset = 0 } } } ) .transition(.move(edge: .bottom)) .animation(.spring(response: 0.45, dampingFraction: 0.85), value: showFullPlayer) .ignoresSafeArea() } } .animation(.spring(response: 0.45, dampingFraction: 0.85), value: showFullPlayer) .onPreferenceChange(HideMiniPlayerKey.self) { hide in readerIsActive = hide } } }