About this animation
An arc scrubber with a responsive audio aura and configurable track copy, labels, colors, and fonts.
Why this one
The progress arc embraces the artwork instead of separating it into a linear bar. Scrubbing pulls the aura toward the handle; playback breathes and pausing freezes the breath at 55% colored light. No people, downloaded artwork, or overlay blocks are needed.
How it works
- One shared seconds value drives the continuous semicircle, handle, clock, and pull direction.
- A pan projects the finger onto the lower semicircle. A tap seeks to the same projected angle.
- A spherical Skia shader combines a luminous core, counter-rotating aurora ribbons, and an outer halo. A full breath takes 8 seconds (4 in, 4 out), scaling from 0.96 to 1.04. Dragging accelerates the aurora. Pausing freezes breathing and preserves the palette at 55% brightness.
- Seek springs are interruptible and use explicit mass with near-critical damping. Separate shared values handle button pressure.
- React receives only changed integer seconds and playback state; digit columns settle on exact digits with independently sized separators.
- The demo starts playing, scrubs to the 60% chapter at 2 seconds, pauses at 3.5 seconds for 1.5 seconds, resumes at 5 seconds, and skips forward at 6.2 seconds. Any touch stops the 10-second loop.
Install
npx expo install @shopify/react-native-skia expo-blur react-native-gesture-handler react-native-reanimated react-native-workletsWrap your application in GestureHandlerRootView. Configure Reanimated/worklets for your SDK. For web, initialize CanvasKit before importing this component (Expo's Skia web setup / WithSkiaWeb). Icons: Lucide (ISC); copied paths are included in icons.ts.
Usage
import { ArcScrubPlayer } from './arc-scrub-player';
<ArcScrubPlayer duration={638} title="Guided session" />;const player = useRef<ArcScrubPlayerRef>(null);
<ArcScrubPlayer
ref={player}
duration={638}
title="Find your quiet center"
subtitle="A guided pause for a softer day."
fontFamily="Geist_500Medium"
monoFontFamily="GeistMono_400Regular"
onSeek={seconds => audioEngine.seek(seconds)}
onPlayPause={playing => audioEngine.setPlaying(playing)}
theme={{ amber: '#FFD092', coral: '#E49B86', lilac: '#B6A4DA' }}
/>;
// Imperative controls: player.current?.seek(180), .play(), .pause().This component simulates time and voice energy; it does not play or analyze audio. Supply position and playing from your audio engine to synchronize external changes. Callbacks report user and imperative requests, not clock ticks. Seeking does not force paused playback to resume.
Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
| duration | number | 638 | Total seconds; minimum 1 |
| position | number | 173 internally | External time updates, clamped |
| onSeek | (seconds: number) => void | — | Seek request, once per completed gesture/action |
| playing | boolean | true internally | External play/pause updates |
| onPlayPause | (playing: boolean) => void | — | Playback request |
| title | string | Guided session | Headline |
| subtitle | string | A little room to breathe. | Supporting copy |
| theme | Partial<ArcScrubPlayerTheme> | warm palette | Background, glow, text, muted, track, progress, control, controlInk, glass, border, amber, coral, lilac, shadow, shadowFade, controlShadow |
| fontFamily | string | system | Interface font |
| monoFontFamily | string | system | Clock font |
| labels | Partial label map | English labels | play, pause, backward, forward, seek, skip, breatheIn, breatheOut, paused, remaining |
| formatTime | (seconds: number) => string | MM:SS | Clock and announcement formatting |
| onInteraction | () => void | — | Finger interaction notification |
| style | StyleProp<ViewStyle> | — | Root style override |
| testID | string | arc-scrub-player | Stable control prefix |
Ref: seek(seconds), play(), pause(), and scrubTo(seconds) (demonstrates the gesture pull).
Compatibility
Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0, and Skia 2.14.0. Uses SDK-compatible installed packages; no additional dependency installation was performed. Expo Go supports this stack. Supports iOS, Android, and web.
The root fills its host and measures it with onLayout. Artwork is constrained by both width and height, with a 440 pt width cap. Small hosts and large text can scroll. The embedding screen owns safe-area insets. The demo reserves the upper right for the catalog close button. Artwork is procedural, so there is no bitmap density constraint.
Accessibility
The arc is adjustable with increment/decrement actions, a current-time value, and a tap alternative to dragging. Buttons expose localized roles and labels; seek/play/pause changes are announced. Controls measure 52 or 64 pt. Reduced motion freezes grain and breathing, disables pull and rolling motion, and dims/restores colored light with a 200 ms fade.
Visual design
The edge-to-edge warm night field breathes with the session. The demo header and footer float over it. A 6 pt gradient arc uses an 18% track, four chapter dots, and a glowing handle that expands while dragging. A time tooltip follows the handle; the main elapsed clock uses large rolling tabular digits with a smaller remaining-time label. Glass skip controls contain centered numerals. The central play/pause symbol interpolates matching contours resampled from the original Lucide shapes.
No audio asset is included: connect the existing callbacks to an audio engine for sound. No generated artwork is required.




