Optional adapters and installation
Issues #22–#24. Base imports do not load adapters. Individual web-sound, browser-haptic, web-haptic, native and react entries preserve dependency boundaries. The composite web entry needs the Haptic package; sound-only consumers should import web-sound.
Web sound
Install @bloxwap/hig/feedback@0.1.0 @bloxwap/sfx@^0.2.0. createWebSoundAdapter() loads SFX only for enabled requests after an accepted interaction. It calls the documented unlock/play APIs, passes per-call volume and uses the same presets as native. New requests replace pending startup, so delayed loading does not create a backlog. It avoids upstream global enabled/volume stores because HIG preferences are root-scoped. @bloxwap/sfx/react owns global preference helpers; using those alongside HIG would create competing preference owners.
HIG passes one adapter-local AbortSignal to each play. Muting, backgrounding, explicit cancellation and disposal abort that root's plays; the next accepted request gets a fresh signal. This stops buffered and live voices while other roots and host-owned voices continue. Generations still discard superseded startup/unlock requests. HIG reference-counts default adapters and disposes the shared engine after the last user. A host that also owns that engine should inject/own its lifecycle.
Browser and physical-tap haptics
createBrowserHapticAdapter() maps accepted semantic events to browser vibration when navigator.vibrate exists. Its existing default and {vibrate} injection retain their patterns; neither requires importing Haptic. Device intensity/pattern quality varies and may be suppressed. iOS WebKit without vibration cannot provide imperative async outcome feedback; the adapter reports unavailable and remains silent.
The optional ImperativeHapticBackend injection accepts the canTriggerHaptic and triggerHaptic functions from @bloxwap/haptic (^0.2.0 || ^0.3.0): createBrowserHapticAdapter({canTriggerHaptic, triggerHaptic}). HIG probes capability at request time, so activation and document visibility are current, and delegates the semantic cue to upstream's presets. Rejection/failure cannot change the primary action. observed stays false. The adapter adds no eager import, helper DOM or physical-tap simulation.
The imperative API reports iOS unavailable, including during transient activation. It cannot replace eligible physical targets or express iOS async outcome patterns. Neither the browser Vibration API nor this upstream API provides a per-root cancellation handle: cancel()/dispose() retain the existing navigator.vibrate(0) stop when available, which is document-wide. This is distinct from SFX's per-root voice cancellation.
For approved iOS physical action targets, install @bloxwap/haptic@^0.3.0 and wrap a single ref-capable HIG button in HapticFeedbackTarget eligible. The wrapper uses upstream HapticTarget, preserves the target/handler/ref, enables only the physical-tap route and disables debug audio/helper switch visibility. Thus browser vibration comes from the semantic adapter and is not duplicated by the wrapper. HIG preferences still gate the wrapper.
Do pre-approve the target with eligible after disabled/busy/policy checks. Don't wrap an action whose eligibility is decided only by an async click callback, or promise a programmable iOS success/error pattern. The OS physical tick occurs before delegated handlers; a later preventDefault cannot retract that tick. That cancellation limitation, physical-tap intensity limits and device/VoiceOver checks remain upstream haptic#9 / haptic#7. The missing imperative iOS API for semantic controllers is haptic#27 (upstream follow-up to #70). No wrapper is required for basic actions.
Native audio
Install the feedback package plus expo-audio (~1.1.1 on Expo SDK 54, ~55.0.18 on SDK 55) only in the native host setup. expoAudioBackend(Audio) supplies a thin playback bridge; createNativeSoundAdapter(backend, assets) accepts Metro asset ids. The Expo gallery imports only the four committed WAV cues. Configure ambient/silent-mode behavior with playsInSilentMode:false, no background playback and doNotMix interruption mode. Host AppState calls setBackground; backgrounding pauses cached players. Call preload() at startup: it configures the audio session and caches players once. After that, play() dispatches synchronously, with no awaited bridge round trip before player.play(). It stops only the previous cue, issues seekTo(0) without awaiting it (native players apply calls in order) and plays. A cue requested before preload settles waits for it, and only the newest such request plays. Concurrency is limited to one active cue, volume/mute are honored, missing cues are silent, and disposal pauses/removes every cached player.
Playback completion: HIG does not listen to playbackStatusUpdate and never relies on didJustFinish. On Android, expo-audio 1.1 (Expo SDK 54) reports didJustFinish twice for one finished cue; SDK 55 (expo-audio 55.0) reports it once (#89). In the Android emulator run, SDK 54 had 4 play() calls, 4 playing starts and 8 didJustFinish statuses: the same finish reported twice, not a second play (evidence/native-sim/issue-86-88-89/). In expo-audio 1.1, every status sent while the player is ended carries didJustFinish: true, and two listeners send one when a cue ends. A host or future adapter that acts on completion must de-duplicate it, for example by ignoring a didJustFinish that has no play() or playing: true status since the last one. Device timing for both SDKs is tracked in #55.
Regenerate with npm run generate:cues. assets/manifest.json pins package/recipe/export flags, 48kHz mono PCM16, durations, peaks/RMS, bytes and reviewed hashes. Noise recipes vary; do not assert regenerated WAV bytes are deterministic. Inspect duration/envelope/level tolerances and review the actual artifact hashes. Offline generation dependencies are dev-only; Metro never imports them or browser synthesis.
Native haptics
Install expo-haptics (~15.0.7 on Expo SDK 54, ~55.0.18 on SDK 55) in the native host. expoHapticBackend(Haptics) bridges selection, impact light/medium/heavy and notification success/warning/error. createNativeHapticAdapter reports API availability separately from observation. Unsupported hardware, backgrounding, preferences and failed backend calls are no-ops that do not change primary actions. Android notification/impact approximation and OS suppression must be observed on devices before support is certified.
Simulated lifecycle regression evidence
The SFX tests use the lockfile's published @bloxwap/sfx package with its recording AudioContext fake to cover buffered/live root isolation, controller mute/background, cancellation during unlock, reuse and last-default-user engine disposal. Haptic tests use the lockfile's published @bloxwap/haptic capability/trigger functions with fake browser globals, cover changing activation/visibility, rejection, SSR and iOS unavailability, and preserve the released fallback. These tests request no physical sound or haptics.
The automated tests inject native player and haptic backends. Pending configure/seek requests are superseded by the newest request; cancellation, backgrounding, zero volume and disposal stop pending playback. Failed configuration can retry. Explicit impact intent reaches the native backend even for an otherwise silent press preset. Pause/remove failures cannot prevent other cached players from being released. A dispatch test drives 400 cues through FeedbackController with simulated slow bridge calls (configure 60 ms, seek 40 ms). It asserts that every cue reaches player.play() inside emit() and that p95 stays under one 16 ms frame. The native gallery stamps each emit and logs dispatch/p95 on every play for simulator and emulator runs. These tests establish dispatch/lifecycle behavior, not observed sound, tactile output, acoustic latency (#55) or OS audio-session behavior.
Menu disclosure and dismissal are intentionally silent. Web/native Menu commits emit a single selection intent after the host handler; a host can cancel that effect with the selection event's preventDefault(). Native accordion disclosure emits one silent disclosure intent rather than duplicate activation/selection effects. The Chromium and WebKit fixture also checks switch, range, menu selection and canceled menu actions with injected adapters.