Skip to content
Native Animation
Resumable Transfer Queue animation preview

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

npx nativeanimation add resumable-transfer-queue

Resumable Transfer Queue

A transfer queue with pause, resume, retry, and cancel feedback, with configurable transfers, labels, palette, and fonts.

$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 transfer queue with pause, resume, retry, and cancel feedback, with configurable transfers, labels, palette, and fonts. Your app performs transfers and supplies byte progress; the component makes no network requests.

Why this one

Retry affects one transfer without resetting neighboring progress. Paused rows retain their bytes and stop their shimmer clock. Actions are guarded until the host acknowledges a state change, and long queues use a virtualized list. Unknown totals remain indeterminate, without a fabricated percentage.

How it works

  1. Supply immutable transfers snapshots from your upload manager. Bytes and accessibility percentages share the same normalized source.
  2. Each row interpolates its byte fraction on the UI thread over 160 ms. Byte labels display the actual host snapshot with a 160 ms settling fade, rather than an estimated network rate.
  3. Actions request host changes. pauseAll() and resumeAll() use the exact same guarded dispatch as row actions; they never change controlled props.
  4. Running rows own a shimmer animation. Pause, error, completion, virtualization, and unmount cancel its clock.
  5. The last completion draws connecting lines over 280 ms, then reveals the secured-byte summary over 220 ms. Completed rows retain their action footprint.

Install

npx expo install expo-image expo-linear-gradient react-native-reanimated react-native-worklets

Copy this folder without demo/. Use the Reanimated/worklets setup appropriate for your Expo SDK. Lucide paths render as local SVG data sources through expo-image; no GPU canvas or icon font is needed. No remote assets or catalog imports are required. Icons: Lucide (ISC).

Usage

import { useRef, useState } from 'react';
import {
  ResumableTransferQueue,
  type ResumableTransferQueueRef,
  type Transfer,
  type TransferLabels,
} from './resumable-transfer-queue';

const labels: TransferLabels = {
  title: 'Transfers', pause: 'Pause', resume: 'Resume', retry: 'Retry',
  cancel: 'Cancel', pauseAll: 'Pause all', resumeAll: 'Resume all',
  reset: 'Start again', queued: 'Queued', running: 'Uploading',
  paused: 'Paused', error: 'Connection interrupted', complete: 'Complete',
  empty: 'No transfers', invalid: 'Transfer data unavailable',
  unknownTotal: 'Size unknown', secured: 'secured',
  completedAnnouncement: 'All transfers are complete.',
};

function Queue() {
  const queue = useRef<ResumableTransferQueueRef>(null);
  const [transfers, setTransfers] = useState<Transfer[]>([
    { id: 'brief', name: 'Brief.pdf', totalBytes: 12000000,
      sentBytes: 3000000, state: 'running' },
  ]);
  const change = (id: string, state: Transfer['state']) =>
    setTransfers(items => items.map(item => item.id === id ? { ...item, state } : item));
  return (
    <ResumableTransferQueue
      ref={queue}
      transfers={transfers}
      labels={labels}
      formatBytes={bytes => `${(bytes / 1000000).toFixed(1)} MB`}
      accessibilityLabel="Workspace transfers"
      onPause={id => change(id, 'paused')}
      onResume={id => change(id, 'running')}
      onRetry={id => change(id, 'running')}
      onCancel={id => setTransfers(items => items.filter(item => item.id !== id))}
      onReset={() => setTransfers([])}
      style={{ height: 400 }}
    />
  );
}

In a real uploader, pause/resume/retry callbacks must call your transport and then supply confirmed snapshots. Retry preserves sentBytes unless your transport explicitly changes it. queued can resume, running can pause, paused can resume, error can retry, and every unfinished row can cancel. Complete rows have no actions. Cancellation removes a row only after the host changes the list.

For a themed variant, pass colorScheme="light", fontFamily, titleFontFamily, monoFontFamily, and a complete palette:

palette={{
  background: '#F7F7F9', surface: '#FFFFFF', text: '#0E0E12',
  muted: '#5B5D6B', border: '#E9E9EF', accent: '#7C5CFF',
  success: '#11825E', error: '#C83228',
}}

Props

PropTypeDefaultPurpose
transfersreadonly Transfer[]requiredStable id, name, totalBytes, sentBytes, state, optional error
onPause / onResume / onRetry / onCancel(id: string) => voidrequiredHost requests, guarded per current row state
onReset() => voidundefinedHandles ref reset; no implicit data mutation
formatBytes(bytes: number) => stringrequiredBytes and completion-total formatting
labelsTransferLabelsrequiredAll structural copy and announcements
colorSchemelight / dark / autoautoComplete theme; follows device in auto
paletteTransferPalettetheme defaultsOverrides all eight semantic colors
fontFamily / titleFontFamily / monoFontFamilystringsystemInterface, heading, and byte typography
onInteraction() => voidundefinedReal input; demo cancels its timers
scrollEnabledbooleantrueDisable when the host fits every row; scroll indicators are always hidden
styleStyleProp<ViewStyle>undefinedFlexible outer layout
testIDstringresumable-transfer-queueRoot and action prefix
accessibilityLabelstringrequiredAccessible region name

labels keys: title, pause, resume, retry, cancel, pauseAll, resumeAll, reset, queued, running, paused, error, complete, empty, invalid, unknownTotal, secured, completedAnnouncement. secured is appended to the formatted total. reset labels the host-owned demo reset control. Row-specific error text overrides labels.error.

Ref methods: pauseAll(), resumeAll(), reset(). A repeated action on a row is ignored until its state changes or it is removed; cancellation remains available while a pause request is pending, and byte-only updates do not unlock it. Reset is guarded until the next host snapshot. Unknown ids are ignored. If the transport rejects a request while preserving its state, replace/remount the row or supply an intervening confirmed state before requesting again.

Edge cases and layout

Empty input shows labels.empty. The first occurrence of an id wins; empty ids are omitted. Nonfinite or nonpositive totals produce labels.unknownTotal, while negative/nonfinite sent bytes produce labels.invalid and disable progress actions. Sent bytes clamp to a known total and the safe integer limit. A completed row with an unknown total still never claims a percentage. Formatters receive finite, normalized counts; the aggregate also clamps to the safe integer limit.

The root uses flex: 1 and measures its own width with onLayout. Give it a bounded parent height. It has no safe area; the host supplies insets. Short containers, landscape, long names, and 200% text use wrapping plus scrolling. Controls are at least 48 × 48. Place this virtualized list in a bounded view, rather than nesting it inside a vertical ScrollView for long queues. The demo contains only three rows and limits width to 480 pt. It disables queue scrolling and fits the full composition without a ScrollView at 320 × 694 and 390 × 844. Below 694 pt tall, one full-screen outer ScrollView provides access to the entire demo without visible indicators. Narrow queue layouts reduce spacing and typography while retaining 48 × 48 action targets; retry uses the same icon footprint as pause and resume.

Identifiers: ${testID}-pause-all, -resume-all, -row-${id}, -row-${id}-pause, -resume, -retry, -cancel.

Accessibility

Every action has a button role and a caller-provided label. Progress bars expose normalized percentages only for known totals. Error, paused, and complete changes are announced; all-complete has its own announcement. Text scaling remains enabled. Reduced motion removes shimmer, connector travel, and pressure scaling; state changes and the summary use a 200 ms fade.

Demo timeline

A 5000 ms host-owned cycle: idle to 600 ms; pause at 600, resume at 850; one row errors at 1100 and retries at 1450; all complete at 2250; connectors finish at 2530 and summary at 2750. Reset is requested at 4200. Actual input cancels every pending autoplay timer and switches to ordinary simulated chunks. Unmount clears all timers. Light and dark controls live below the queue.

Compatibility

Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, and expo-image 58.0.17. Uses installed Expo Go-compatible modules.

The component contains no photos, illustrations, or external artwork.