| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667 |
- import SwiftUI
- // MARK: - PulsingRecordButton
- /// A minimalist, line-drawn record button suitable for a business productivity app.
- /// Completely avoids heavy neon glows, replacing them with thin outlines, a clean scale animation,
- /// and an outline mic icon.
- struct PulsingRecordButton: View {
- /// Closure triggered on tap.
- let action: () -> Void
- var body: some View {
- TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { context in
- let time = context.date.timeIntervalSinceReferenceDate
- let breathingPhase = (sin(time * .pi) + 1) / 2
- let ripplePhase = time.truncatingRemainder(dividingBy: 2.5) / 2.5
- ZStack {
- // These transforms are derived directly from time instead of
- // using a repeating implicit animation. That keeps SwiftUI from
- // accidentally animating parent layout changes after a cover or
- // app lifecycle transition.
- Circle()
- .stroke(Color.primary.opacity(0.1), lineWidth: 1)
- .frame(width: 120, height: 120)
- .scaleEffect(0.98 + breathingPhase * 0.10)
- Circle()
- .stroke(Color.primary.opacity(0.08), lineWidth: 1)
- .frame(width: 100, height: 100)
- .scaleEffect(1.0 + ripplePhase * 0.25)
- .opacity(0.8 * (1.0 - ripplePhase))
- Circle()
- .fill(Color.primary.opacity(0.03))
- .frame(width: 90, height: 90)
- .overlay(
- Circle()
- .stroke(Color.primary.opacity(0.2), lineWidth: 1)
- )
- Image(systemName: "mic")
- .font(.system(size: 24, weight: .light))
- .foregroundColor(.primary)
- .scaleEffect(0.98 + breathingPhase * 0.04)
- }
- }
- .frame(width: 120, height: 120)
- .contentShape(Circle())
- .onTapGesture {
- HapticManager.trigger(.recordStart)
- action()
- }
- }
- }
- // MARK: - Preview
- #Preview {
- ZStack {
- Color.spaceBlack.ignoresSafeArea()
- PulsingRecordButton {
- print("Record tapped")
- }
- }
- }
|