## Textured drag (velocity-driven rumble)

- Use case: Dragging across a surface that should feel textured — the faster the finger moves, the stronger the rumble.
- Feel: A continuous low hum whose intensity and sharpness track finger velocity in real time. Feels like friction.
- APIs: CHHapticAdvancedPatternPlayer, sendParameters, DragGesture
- Minimum OS: iOS 13+

### Core Haptics + SwiftUI

```swift
import SwiftUI
import CoreHaptics

struct TexturedSurface: View {
    @State private var engine: CHHapticEngine?
    @State private var player: CHHapticAdvancedPatternPlayer?

    var body: some View {
        RoundedRectangle(cornerRadius: 20)
            .fill(.gray.opacity(0.3))
            .frame(height: 220)
            .gesture(
                DragGesture(minimumDistance: 0)
                    .onChanged { value in
                        if player == nil { startLoop() }
                        // Map velocity (pts/s) to 0–1
                        let speed = min(1, abs(value.velocity.width) / 2000
                                         + abs(value.velocity.height) / 2000)
                        update(intensity: Float(0.2 + 0.8 * speed),
                               sharpness: Float(0.3 * speed))
                    }
                    .onEnded { _ in stopLoop() }
            )
            .onAppear {
                guard CHHapticEngine.capabilitiesForHardware().supportsHaptics
                else { return }
                engine = try? CHHapticEngine()
                try? engine?.start()
            }
    }

    private func startLoop() {
        guard let engine else { return }
        let event = CHHapticEvent(
            eventType: .hapticContinuous,
            parameters: [
                CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.2),
                CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2),
            ],
            relativeTime: 0, duration: 100) // long-lived; we stop it manually
        guard let pattern = try? CHHapticPattern(events: [event], parameters: [])
        else { return }
        player = try? engine.makeAdvancedPlayer(with: pattern)
        try? player?.start(atTime: CHHapticTimeImmediate)
    }

    private func update(intensity: Float, sharpness: Float) {
        try? player?.sendParameters([
            CHHapticDynamicParameter(parameterID: .hapticIntensityControl,
                                     value: intensity, relativeTime: 0),
            CHHapticDynamicParameter(parameterID: .hapticSharpnessControl,
                                     value: sharpness, relativeTime: 0),
        ], atTime: CHHapticTimeImmediate)
    }

    private func stopLoop() {
        try? player?.stop(atTime: CHHapticTimeImmediate)
        player = nil
    }
}
```

### Notes
- sendParameters on an advanced player is the only way to modulate a haptic live — rebuilding patterns per frame is too slow.
- Keep base intensity low (~0.2): texture should sit under the threshold of attention until the finger accelerates.
