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
- Supply immutable
transferssnapshots from your upload manager. Bytes and accessibility percentages share the same normalized source. - 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.
- Actions request host changes.
pauseAll()andresumeAll()use the exact same guarded dispatch as row actions; they never change controlled props. - Running rows own a shimmer animation. Pause, error, completion, virtualization, and unmount cancel its clock.
- 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-workletsCopy 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
| Prop | Type | Default | Purpose |
|---|---|---|---|
| transfers | readonly Transfer[] | required | Stable id, name, totalBytes, sentBytes, state, optional error |
| onPause / onResume / onRetry / onCancel | (id: string) => void | required | Host requests, guarded per current row state |
| onReset | () => void | undefined | Handles ref reset; no implicit data mutation |
| formatBytes | (bytes: number) => string | required | Bytes and completion-total formatting |
| labels | TransferLabels | required | All structural copy and announcements |
| colorScheme | light / dark / auto | auto | Complete theme; follows device in auto |
| palette | TransferPalette | theme defaults | Overrides all eight semantic colors |
| fontFamily / titleFontFamily / monoFontFamily | string | system | Interface, heading, and byte typography |
| onInteraction | () => void | undefined | Real input; demo cancels its timers |
| scrollEnabled | boolean | true | Disable when the host fits every row; scroll indicators are always hidden |
| style | StyleProp<ViewStyle> | undefined | Flexible outer layout |
| testID | string | resumable-transfer-queue | Root and action prefix |
| accessibilityLabel | string | required | Accessible 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.




