Created
June 13, 2026 04:37
-
-
Save AbodiDawoud/2b1abcca5f7932331705d2feaee949db to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import SwiftUI | |
| struct CommandBloomPicker: View { | |
| private let commands: [BloomCommand] = [ | |
| BloomCommand(title: "Draft"), | |
| BloomCommand(title: "Polish"), | |
| BloomCommand(title: "Send") | |
| ] | |
| @State private var selectedIndex = 0 | |
| @State private var isExpanded = false | |
| @State private var isHovered = false | |
| @State private var pulse = false | |
| private var selected: BloomCommand { | |
| commands[selectedIndex] | |
| } | |
| var body: some View { | |
| ZStack { | |
| ForEach(commands.indices, id: \.self) { index in | |
| BloomChip( | |
| command: commands[index], | |
| isSelected: selectedIndex == index, | |
| isExpanded: isExpanded | |
| ) { | |
| choose(index) | |
| } | |
| .offset(chipOffset(for: index)) | |
| .scaleEffect(isExpanded ? 1 : 0.62) | |
| .blur(radius: isExpanded ? 0 : 8) | |
| .opacity(isExpanded ? 1 : 0) | |
| .animation( | |
| .spring(response: 0.42, dampingFraction: 0.72) | |
| .delay(Double(index) * 0.035), | |
| value: isExpanded | |
| ) | |
| } | |
| Button { | |
| toggleBloom() | |
| } label: { | |
| ZStack { | |
| Capsule() | |
| .fill(Color(white: 0.955)) | |
| .scaleEffect(x: pulse ? 1.018 : 1, y: pulse ? 0.975 : 1) | |
| HStack(spacing: 8) { | |
| Text(isExpanded ? "Choose" : selected.title) | |
| .font(.system(size: 15, weight: .semibold, design: .rounded)) | |
| .foregroundStyle(Color.primary.opacity(0.82)) | |
| .frame(width: 64) | |
| .id(isExpanded ? "choose" : selected.title) | |
| .transition(.blurReplace) | |
| Image(systemName: "chevron.up") | |
| .font(.system(size: 11, weight: .bold)) | |
| .foregroundStyle(Color.primary.opacity(0.34)) | |
| .rotationEffect(.degrees(isExpanded ? 180 : 0)) | |
| .frame(width: 22, height: 22) | |
| } | |
| } | |
| .frame(width: 142, height: 48) | |
| } | |
| .buttonStyle(BloomPressStyle(isHovered: isHovered)) | |
| .offset(y: isExpanded ? 42 : 0) | |
| .onHover { hovered in | |
| withAnimation(.spring(response: 0.26, dampingFraction: 0.78)) { | |
| isHovered = hovered | |
| } | |
| } | |
| } | |
| .frame(width: 340, height: 184) | |
| .animation(.spring(response: 0.44, dampingFraction: 0.70), value: selectedIndex) | |
| .animation(.spring(response: 0.42, dampingFraction: 0.72), value: isExpanded) | |
| .animation(.spring(response: 0.30, dampingFraction: 0.62), value: pulse) | |
| } | |
| private func chipOffset(for index: Int) -> CGSize { | |
| guard isExpanded else { | |
| return CGSize(width: 0, height: 16) | |
| } | |
| switch index { | |
| case 0: | |
| return CGSize(width: -92, height: -48) | |
| case 1: | |
| return CGSize(width: 0, height: -76) | |
| default: | |
| return CGSize(width: 92, height: -48) | |
| } | |
| } | |
| private func toggleBloom() { | |
| withAnimation(.spring(response: 0.38, dampingFraction: 0.68)) { | |
| isExpanded.toggle() | |
| pulse = true | |
| } | |
| runMomentaryPulse() | |
| } | |
| private func choose(_ index: Int) { | |
| withAnimation(.spring(response: 0.40, dampingFraction: 0.68)) { | |
| selectedIndex = index | |
| isExpanded = false | |
| pulse = true | |
| } | |
| runMomentaryPulse() | |
| } | |
| private func runMomentaryPulse() { | |
| Task { @MainActor in | |
| try? await Task.sleep(nanoseconds: 230_000_000) | |
| withAnimation(.spring(response: 0.32, dampingFraction: 0.72)) { | |
| pulse = false | |
| } | |
| } | |
| } | |
| } | |
| private struct BloomCommand: Identifiable { | |
| let id = UUID() | |
| let title: String | |
| } | |
| private struct BloomChip: View { | |
| let command: BloomCommand | |
| let isSelected: Bool | |
| let isExpanded: Bool | |
| let action: () -> Void | |
| var body: some View { | |
| Button(action: action) { | |
| Text(command.title) | |
| .font(.system(size: 13, weight: .semibold, design: .rounded)) | |
| .foregroundStyle(Color.primary.opacity(0.78)) | |
| .padding(.horizontal, 14) | |
| .frame(height: 36) | |
| .background(Color(white: isSelected ? 0.942 : 0.965).opacity(isSelected ? 1 : 0.78), in: Capsule()) | |
| } | |
| .buttonStyle(.plain) | |
| .disabled(!isExpanded) | |
| } | |
| } | |
| private struct BloomPressStyle: ButtonStyle { | |
| let isHovered: Bool | |
| func makeBody(configuration: Configuration) -> some View { | |
| configuration.label | |
| .scaleEffect(configuration.isPressed ? 0.972 : isHovered ? 1.012 : 1) | |
| .animation(.spring(response: 0.24, dampingFraction: 0.66), value: configuration.isPressed) | |
| .animation(.spring(response: 0.28, dampingFraction: 0.78), value: isHovered) | |
| } | |
| } | |
| #Preview { | |
| CommandBloomPicker() | |
| .padding(80) | |
| .background(Color.white.opacity(0.95)) | |
| .preferredColorScheme(.light) | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
ScreenRecording.mov