## Error with shake animation

- Use case: Wrong password or invalid input: the field shakes while the error haptic buzzes — the lock-screen pattern.
- Feel: Three sharp buzzes synchronized with a horizontal shake. Unmistakably negative.
- APIs: .sensoryFeedback(.error), UINotificationFeedbackGenerator(.error)
- Minimum OS: iOS 17+ / iOS 10+

### SwiftUI (iOS 17+)

```swift
import SwiftUI

struct PasscodeField: View {
    @State private var code = ""
    @State private var failedAttempts = 0

    var body: some View {
        SecureField("Passcode", text: $code)
            .textFieldStyle(.roundedBorder)
            .modifier(Shake(trigger: failedAttempts))
            .sensoryFeedback(.error, trigger: failedAttempts)
            .onSubmit {
                if code != "1234" {
                    withAnimation(.default) { failedAttempts += 1 }
                    code = ""
                }
            }
    }
}

/// Horizontal shake, synced to the same trigger as the haptic.
struct Shake: ViewModifier, Animatable {
    var trigger: Int
    var animatableData: CGFloat { get { CGFloat(trigger) } set { phase = newValue } }
    private var phase: CGFloat = 0

    init(trigger: Int) { self.trigger = trigger; self.phase = CGFloat(trigger) }

    func body(content: Content) -> some View {
        content.offset(x: sin(phase * .pi * 4) * 8)
    }
}
```

### UIKit

```swift
func showLoginError(on field: UITextField) {
    UINotificationFeedbackGenerator().notificationOccurred(.error)

    let shake = CAKeyframeAnimation(keyPath: "transform.translation.x")
    shake.values = [0, -10, 10, -8, 8, -4, 4, 0]
    shake.duration = 0.4
    field.layer.add(shake, forKey: "shake")
}
```

### Notes
- Haptic and animation must share the same trigger/timeline — the multisensory sync is what makes it feel like one event.
