diff --git a/NucleicRemote/NucleicRemote/Views/AdaptiveRootView.swift b/NucleicRemote/NucleicRemote/Views/AdaptiveRootView.swift index e8f692b..753e2fd 100644 --- a/NucleicRemote/NucleicRemote/Views/AdaptiveRootView.swift +++ b/NucleicRemote/NucleicRemote/Views/AdaptiveRootView.swift @@ -175,6 +175,10 @@ struct NucleicTabBar: View { @Binding var selection: Int /// Sessions blocking on a human — the red badge on the Sessions item (was the tab item badge). var needsYou: Int + @Environment(\.accessibilityReduceMotion) private var reduceMotion + /// Ties every item's selected-capsule to one shared geometry, so the "bubble" interpolates its + /// frame from the old tab to the new one instead of cross-fading — the native bar's slide. + @Namespace private var bubble private struct Item { let index: Int; let title: String; let icon: String } private let items = [ @@ -193,6 +197,10 @@ struct NucleicTabBar: View { .padding(.horizontal, 6) .padding(.vertical, 8) .glassBar() + // Drive the bubble's slide (and the label/tint crossfade) off the selection itself, so any + // change — tap here or a programmatic jump from a notification — springs across. + .animation(reduceMotion ? nil : .spring(response: 0.32, dampingFraction: 0.78), + value: selection) } private func itemButton(_ item: Item) -> some View { @@ -220,11 +228,14 @@ struct NucleicTabBar: View { .foregroundStyle(selected ? Palette.accent : Color.secondary) .frame(maxWidth: .infinity) .padding(.vertical, 4) - // The selected item rides a soft accent-tinted capsule, echoing the system floating - // bar's highlighted tab. + // The selected item rides a soft accent-tinted capsule that slides between tabs — only + // the selected item mounts it, and the shared `bubble` geometry makes the single capsule + // travel to the new tab, echoing the system floating bar's highlighted-tab indicator. .background { if selected { - Capsule().fill(Palette.accent.opacity(0.15)) + Capsule() + .fill(Palette.accent.opacity(0.15)) + .matchedGeometryEffect(id: "selectionBubble", in: bubble) } } .contentShape(Rectangle())