Skip to content
Native Animation
Voice Record Lock animation preview

This preview uses fictional demo content. See the props below for the customization options available in this component.

npx nativeanimation add voice-record-lock

Voice Record Lock

A hold-to-record interface with slide-to-lock and review states, with configurable labels, palette, fonts, and duration limit.

$9 for this animation

Add to your project

Copy the component and its assets into your app with the Native Animation CLI, or paste the agent prompt into Claude Code, Codex or Cursor once the MCP server is connected.

About this animation

A hold-to-record interface with slide-to-lock and review states, with configurable labels, palette, fonts, and duration limit. Your app supplies the recording adapter, audio levels, and playback state.

Why this one

An axis locks after 18 points of movement, so a diagonal cannot both cancel and lock. Visible threshold meters and explicit Record, Lock, Cancel, Stop, preview, and Send actions make the same flow available without holding a gesture. Releasing a hold always requests review. Reaching the duration limit requests Stop, never Send. Controlled hosts retain ownership of every rendered state.

How it works

  1. A tap on Record starts directly in hands-free mode; a 180 ms hold on the waveform’s trailing microphone starts holding.
  2. A pan commits to its dominant axis after 18 points. Sliding left crosses min(96, width * 0.34); sliding up crosses min(80, height * 0.24). Component measurements come from onLayout.
  3. A single signed gesture progress drives microphone travel and the threshold meters. A separate lock transition drives the ascent, waveform expansion, and blue halo. Lucide's closed shackle draws in 180 ms.
  4. The host supplies sanitized level, elapsed time, preview position, and playback status. Voice level smoothing takes 80 ms on the UI thread; React never receives frame callbacks.
  5. A session request ledger rejects duplicate actions, including duplicate releases while controlled props are waiting for acknowledgment. Business callbacks run in JS and never wait for an animation to finish.
  6. Stop/release opens review. Only an explicit Send action can call onSend. Background interruption and unmount discard an active session once.

Install

Copy this folder without demo/. Install compatible packages through Expo:

npx expo install react-native-reanimated react-native-worklets react-native-gesture-handler @shopify/react-native-skia expo-linear-gradient expo-blur

Use a GestureHandlerRootView at the host app root. Configure Reanimated/worklets and Skia for your Expo project. On web, load CanvasKit before rendering the component, as required by Skia. Icons: Lucide (ISC), copied from the local kit with original paths.

Usage

import { VoiceRecordLock, type VoiceRecordLabels } from './voice-record-lock';

const labels: VoiceRecordLabels = {
  hold: 'Hold to record',
  cancelHint: 'Slide left to cancel',
  lockHint: 'Slide up to lock',
  locked: 'Recording locked',
  review: 'Review your message',
  submitted: 'Voice message ready',
  record: 'Record', lock: 'Lock', cancel: 'Cancel', stop: 'Stop',
  play: 'Play', pause: 'Pause', send: 'Send',
};

<VoiceRecordLock
  sessionId="draft-42"
  labels={labels}
  level={0.45}
  elapsedMs={2400}
  accessibilityLabel="Voice recorder"
  onStart={adapter.start}
  onLock={adapter.lock}
  onCancel={adapter.discard}
  onStop={adapter.stop}
  onSend={adapter.requestSend}
/>

The adapter above is supplied by your application. A UI-only host can provide no-op callbacks. The component never imports an audio adapter.

const recorder = useRef<VoiceRecordLockRef>(null);
const [state, setState] = useState<VoiceRecordState>('idle');

<VoiceRecordLock
  ref={recorder}
  sessionId={sessionId}
  state={state}
  onStateChange={setState}
  level={level}
  elapsedMs={elapsedMs}
  previewProgress={previewProgress}
  previewPlaying={previewPlaying}
  onPreviewToggle={setPreviewPlaying}
  labels={labels}
  colorScheme="dark"
  palette={NIGHT}
  motionScale={0.8}
  fontFamily={interfaceFont}
  titleFontFamily={statusFont}
  monoFontFamily={numberFont}
  accessibilityLabel="Voice recorder"
  onStart={adapter.start}
  onLock={adapter.lock}
  onCancel={adapter.discard}
  onStop={adapter.stop}
  onSend={adapter.requestSend}
/>

Import the ref/state types and palette from index.ts. The host must filter asynchronous responses by session ID before updating props. Use a new unique ID for each recording; changing IDs invalidates the previous request ledger and cancels an unfinished old session. Call reset() when opening a new UI session. Do not switch between controlled and uncontrolled modes during one mount.

Props

PropTypeDefaultPurpose
sessionIdstringRequiredUnique stable host session identity; forwarded unchanged to callbacks.
stateVoiceRecordStateUncontrolledRendered host state. Refs request callbacks without changing this prop.
defaultStateVoiceRecordStateidleInitial uncontrolled state only.
levelnumberRequiredHost level, clamped to 0–1; invalid input becomes 0.
elapsedMsnumberRequiredHost clock, clamped to recording limit; invalid input becomes 0.
previewProgressnumber0Simulated waveform playback position, clamped to 0–1.
previewPlayingbooleanfalseHost preview state; no sound is played.
maxDurationMsnumber60000Stop limit, clamped to 1–86,400,000 ms; nonfinite input uses 60,000.
onStateChange(state) => void—Requests a transition or reset; controlled host must acknowledge it.
onStart/onLock/onCancel/onStop/onSend(sessionId: string) => voidRequiredBusiness requests, deduplicated within a session.
onPreviewToggle(playing: boolean) => void—Requests host preview playback.
formatDuration(ms: number) => stringMM:SSCustom output flows as plain text; default uses one fixed-width, single-line timer.
labelsVoiceRecordLabelsRequiredEvery instruction, state, and action string; see table below.
colorSchemelight | dark | autoautoFull theme selection.
paletteVoiceRecordPaletteTheme paletteFull override: background, surface, text, muted, border, accent, onAccent, success, error.
fontFamilystringSystemControls and instructions.
titleFontFamilystringfontFamilyStatus text.
monoFontFamilystringSystemTimer text; use a monospaced font.
styleStyleProp<ViewStyle>—Root style; default flex: 1. Host owns safe areas.
testIDstringvoice-record-lockRoot and stable control prefix.
accessibilityLabelstringRequiredContextual timer/recorder description.
motionScalenumber1Clamped to 0–1.5. Zero removes movement while preserving actions.
Label keyDemo valueUsage
holdHold to recordIdle state instruction.
cancelHintSlide left to cancelHolding state instruction.
lockHintSlide up to lockVertical gesture instruction.
lockedRecording lockedHands-free status and announcement.
reviewReview your messagePreview status and announcement.
submittedVoice message readySend-request confirmation, not delivery confirmation.
recordRecordAccessible tap alternative.
lockLockAccessible lock action.
cancelCancelDiscard action.
stopStopReview action.
playPlaySimulated preview action.
pausePauseSimulated preview action.
sendSendExplicit send request.

Empty label strings render no text and reserve no text gap. Supply meaningful action labels for screen readers even when your host uses icon-only visual controls.

Ref

MethodEffect
start()Requests holding from idle.
lock()Requests locked from holding.
cancel()Discards holding, locked, or preview once.
stop()Requests preview from holding or locked.
requestSend()Requests submitted only from preview.
reset()Requests idle, clears the ledger and gesture position; no business callbacks.

In controlled mode, none of these methods changes rendered remote state. Invalid or repeated requests are ignored. A canceled or submitted session must be reset or given a new ID before reuse. Tap Record calls Start and Lock once and requests locked directly. Releasing holding calls Stop once and requests preview.

State transitions

FromTriggerToBusiness callback
idleHold / start()holdingStart
idleTap RecordlockedStart, Lock
holdingUp threshold / LocklockedLock
holdingReleasepreviewStop
holding / lockedStop / duration limitpreviewStop
holding / locked / previewCancel / interruptionidleCancel
previewSendsubmittedSend
anyResetidleNone

A gesture cancellation before confirmation discards the hold. Cancellation after locking preserves locked state. A diagonal fixes one axis, and threshold confirmation happens once. No callback is attached to spring completion. onSend means the host was asked to send; network success and failure remain host responsibilities.

Compatibility

Target stack: Expo 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0, Gesture Handler 3.3, Skia 2.14.0. Expo Go uses the modules bundled with this SDK. Web mouse/keyboard use the same explicit buttons; hold/drag uses Gesture Handler.

Accessibility

The root measures its embedding slot and adds no safe-area padding. The demo contains no ScrollView, caps its width at 480 points, removes its exterior title/brand on compact or large-text layouts, and places Replay and theme selection at the bottom. The upper-right 56-point region is reserved for the catalog viewer. All action targets are at least 48 points. Action buttons share the same height, with an icon above a single-line label; use short labels such as Play and Pause for compact layouts.

Screen readers use labeled action buttons instead of the visual microphone target. Status announcements occur only on discrete changes, never each timer tick. The default duration is a single monospaced line with a fixed width sized for the recording limit and font scale. The draggable microphone stays inside the waveform stage, away from every action label, and disappears immediately when recording locks. Status changes use a 90 ms exit, 20 ms gap, then 90 ms entry, with one mounted label. Native announcements are guarded on web. Reduced motion removes microphone travel, waveform movement, halo, and lock drawing; all states and actions remain available.

At 200% text, actions stack vertically and decorative context disappears. The host must offer an expanded presentation when its embedding slot cannot contain the complete scaled action text; do not clip the component or disable font scaling.

The rounded control band uses BlurView intensity 45 on iOS/web, with its own radius and overflow clipping. Android uses the opaque themed fallback. No blur target or additional native module is required. The material behind the band is deliberately subdued for contrast.