Skip to content
Native Animation
Video Scrub Preview animation preview

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

npx nativeanimation add video-scrub-preview
GlassSkia

Video Scrub Preview

A simulated video scrubber with panorama-based previews and configurable image, labels, accent color, 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 simulated video scrubber with panorama-based previews and configurable image, labels, accent color, and fonts. It uses a virtual clock and does not include a video player.

Why this one

A timeline usually shows a position without showing what is there. This one previews the same temporal crop in the large frame, ten thumbnails, and the floating loupe. A traveling crop turns one local panorama into a coherent simulated clip. Lowering the finger progressively reduces the gain, making an exact frame easier to find without playhead jumps.

How it works

  1. One UI-thread time value drives the moving image crop, playhead, and preview position. Playback advances from the frame clock, with no React updates per frame.
  2. The panorama is enlarged to 140% of each 16:9 viewport height. Time determines horizontal crop position. Ten strip cells show fixed temporal crops of the same image.
  3. An exclusive Pan/Tap gesture handles both drag and tap on the 64-point timeline hit area.
  4. Horizontal deltas are integrated with vertical gain. The 60- and 120-point thresholds blend across 40-point bands, from 1 to 0.5 to 0.25. Each sample re-anchors the horizontal origin.
  5. The loupe enters from 0.92 scale, follows with a spring, and stays within the timeline edges. Release uses decay 0.99, with velocity capped at 12 seconds per second.
  6. Time labels and accessibility values update only at second boundaries. Callbacks run on JS through scheduleOnRN.

Install

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

Wrap the application in GestureHandlerRootView. Follow the installed Skia version's CanvasKit setup for web and load Skia before importing this component (the catalog uses withSkia). The panorama is caller-supplied; the component never imports demo assets or theme files.

Usage

import { VideoScrubPreview } from './video-scrub-preview';

<View style={{ flex: 1 }}>
  <VideoScrubPreview source={require('./assets/panorama.jpg')} />
</View>
const player = useRef<VideoScrubPreviewRef>(null);

<VideoScrubPreview
  ref={player}
  source={require('./assets/panorama.jpg')}
  duration={60}
  initialTime={10}
  detailColor="#A3E635"
  springDamping={30}
  colorScheme="auto"
  onTimeChange={savePosition}
  onTrim={openTrimEditor}
/>;
// Event handlers can call player.current?.seek(20) or setPlaying(true).

Props

PropTypeDefaultPurpose
labelsPartial<VideoScrubPreviewLabels>American English defaultsplaying: Playing; preview: Preview; trim: Trim; play: Play video; pause: Pause video; trimAccessibility: Trim clip; videoBadge: 16:9 · ORIGINAL. Omitted keys retain their defaults.
sourceDataSourceParamRequiredLocal panoramic image
durationnumber42Clip length in seconds, minimum 1
initialTimenumber14Initial time, clamped to clip bounds
fontFamilystringSystemInterface font
monoFontFamilystringSystemTimecode font
accentColorstring#F4F4F8Playhead and handles
detailColorstring#FACC15Precision indicator
colorScheme'dark' | 'light' | 'auto''dark'Material palette
springDampingnumber28Spring damping. Values below the damping needed for ζ ≥ 0.8 are raised to that minimum.
onTimeChange(seconds: number) => void—Whole-second changes and scrub completion
onPlayingChange(playing: boolean) => void—Playback state changes
onTrim() => void—Trim action; disabled when absent
accessibilityLabelstringVideo timelineAdjustable timeline label
styleStyleProp<ViewStyle>—Root container styling
testIDstring—Root test identifier

Ref methods: seek(seconds), setPlaying(boolean), beginScrub(), scrubBy(deltaX, depth), endScrub(). Programmatic scrub methods let a host build guided demonstrations without adding autoplay behavior to the component. Playback pauses when dragging begins and stays paused after release. Tapping seeks while preserving playback state. The speed readout is a fixed 1× indicator, not a speed selector. The demo's Trim button marks/clears a marker; the component forwards this action to the host.

Compatibility

Targets Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, worklets 0.13.0, Gesture Handler 3.3, and Skia 2.14.0. These modules are included in the matching Expo Go release. SDK 58 Android blur uses BlurTargetView and dimezisBlurViewSdk31Plus; older Android versions use the tint fallback.

Accessibility

The timeline is adjustable, reports “14 seconds of 42,” and exposes one-second increment/decrement actions. Tapping the strip provides an alternative to dragging. Play and trim controls have button roles and labels, and hit areas are at least 44 points. With reduced motion, seeking and loupe position are immediate; entrance scaling and inertia are removed, and autoplay is disabled. Playback remains available when requested by the user.

Demo storyboard

At 300 ms the loupe lifts; 350–1100 ms sweeps right; 1100–1900 ms lowers the virtual finger to 150 points while the playhead slows. At 1950 ms the loupe closes, then playback starts at 2200 ms. The sequence repeats every five seconds. Layout is measured through onLayout: preview at 0.08H with 0.94W, strip at 0.56H, transport at 0.74H, editorial caption below 0.86H. The open upper-right corner is reserved for the catalog viewer's close button.

Icons: Lucide (ISC). Demo brand: Orrin, from the internal brand kit. The simulated footage is a bundled local panorama, not a video decoder.

Connecting real video (host adapter)

Install expo-video in the host app with npx expo install expo-video; it is not a dependency of this piece. The Expo SDK 58 video API provides useVideoPlayer and VideoView. This one-way example forwards simulated timeline requests to a separate player:

import { View } from 'react-native';
import { useVideoPlayer, VideoView } from 'expo-video';
import { VideoScrubPreview } from './video-scrub-preview';

export function VideoPreview() {
  const player = useVideoPlayer(require('./assets/clip.mp4'));
  return (
    <View style={{ flex: 1 }}>
      <VideoView player={player} style={{ height: 180 }} nativeControls={false} />
      <VideoScrubPreview
        source={require('./assets/panorama.jpg')}
        duration={42}
        initialTime={0}
        onTimeChange={(seconds) => { player.currentTime = seconds; }}
        onPlayingChange={(playing) => {
          if (playing) player.play();
          else player.pause();
        }}
      />
    </View>
  );
}

Use the actual clip length instead of 42. This adapter demonstrates seek/play/pause wiring, not a synchronized production player: the component still uses its own clock and image crops, and time callbacks are coarse (second boundaries and scrub completion). For a fully connected version, replace the virtual clock with player time, subscribe to timeUpdate (set timeUpdateEventInterval, for example 0.25 seconds), playingChange, sourceLoad, statusChange, and playToEnd, and handle buffering, errors, duration, and completion in the host. Keep player observations separate from seek requests to avoid feedback loops; throttle drag seeks. Genuine video thumbnails must also be supplied or decoded by the host.