# Video

> A video player component with custom controls, gestures, and subtitle support.

**BNA UI** — a React Native / Expo component library.
These components render through `react-native`, not the DOM: there are no HTML
elements, no Tailwind classes and no Radix primitives. Source is copied into your
project and imported through `@/components/ui/*`, `@/components/charts/*`,
`@/hooks/*` and `@/theme/*`. Colours come from the `useColor` hook rather than
hardcoded hex; sizing tokens (`HEIGHT`, `FONT_SIZE`, `BORDER_RADIUS`, `CORNERS`)
come from `@/theme/globals`.

- Docs: https://ui.ahmedbna.com/docs/components/video
- Markdown: https://ui.ahmedbna.com/docs/components/video.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/video.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/video.json
- Install: `npx bna-ui add video`
- npm dependencies: `expo-video`, `lucide-react-native`, `react-native-gesture-handler`, `react-native-reanimated`, `react-native-worklets`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `globals`, `view`, `progress`, `text`
- Preview recording: https://demo.ahmedbna.com/0321-video-demo.MP4

---

**Example:** A basic video player with custom controls

```tsx
// components/demo/video/video-demo.tsx
import { Video } from '@/components/ui/video';

export function VideoDemo() {
  return (
    <Video
      source={{
        uri: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
      }}
      style={{
        width: '100%',
        height: 200,
        borderRadius: 8,
      }}
      autoPlay={false}
      loop={false}
      muted={false}
      showControls={true}
    />
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add video
```

### Manual

**1.** Install the following dependencies:

```bash
npx expo install expo-video lucide-react-native
```

**2.** Copy and paste the following code into your project.

```tsx
// components/ui/video.tsx
import { useColor } from '@/hooks/useColor';
import { BORDER_RADIUS } from '@/theme/globals';
import { useEvent } from 'expo';
import { useVideoPlayer, VideoSource, VideoView } from 'expo-video';
import { Pause, Play, Volume2, VolumeX } from 'lucide-react-native';
import React, {
  forwardRef,
  useCallback,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import {
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
  ViewStyle,
} from 'react-native';
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
} from 'react-native-gesture-handler';
import Animated, {
  runOnJS,
  useAnimatedStyle,
  useDerivedValue,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';

interface VideoProps {
  source: VideoSource;
  style?: ViewStyle;
  seekBy?: number; // seconds to seek by on double tap
  autoPlay?: boolean;
  loop?: boolean;
  muted?: boolean;
  nativeControls?: boolean;
  showControls?: boolean;
  /**
   * Kept as a boolean on this component's own API — SDK 55 replaced
   * `VideoView`'s `allowsFullscreen` prop with `fullscreenOptions.enable`,
   * and that translation happens internally so consumers don't have to care.
   */
  allowsFullscreen?: boolean;
  allowsPictureInPicture?: boolean;
  contentFit?: 'contain' | 'cover' | 'fill';
  onLoad?: () => void;
  onError?: (error: any) => void;
  onPlaybackStatusUpdate?: (status: any) => void;
  onFullscreenUpdate?: (isFullscreen: boolean) => void;
  subtitles?: Array<{
    start: number;
    end: number;
    text: string;
  }>;
}

interface VideoRef {
  play: () => void;
  pause: () => void;
  seekTo: (seconds: number) => void;
  setVolume: (volume: number) => void;
  getCurrentTime: () => number;
  getDuration: () => number;
  isPlaying: () => boolean;
  isMuted: () => boolean;
}

// Helper function to format time
const formatTime = (seconds: number): string => {
  if (isNaN(seconds) || seconds < 0) return '0:00';
  const mins = Math.floor(seconds / 60);
  const secs = Math.floor(seconds % 60);
  return `${mins}:${secs.toString().padStart(2, '0')}`;
};

// --- Custom Reanimated Progress Bar ---
const PROGRESS_HEIGHT = 8;
const THUMB_SIZE = 16;

interface ReanimatedProgressProps {
  duration: number;
  currentTime: number;
  onSeek: (progress: number) => void;
  onSeekStart?: () => void;
  onSeekEnd?: () => void;
}

const ReanimatedProgress = ({
  duration,
  currentTime,
  onSeek,
  onSeekStart,
  onSeekEnd,
}: ReanimatedProgressProps) => {
  const [barWidth, setBarWidth] = useState(0);
  const isScrubbing = useSharedValue(false);
  const translateX = useSharedValue(0);
  const scale = useSharedValue(1);

  useDerivedValue(() => {
    if (!isScrubbing.value && duration > 0 && barWidth > 0) {
      const progress = currentTime / duration;
      translateX.value = withTiming(progress * barWidth, { duration: 100 });
    }
  });

  const panGesture = Gesture.Pan()
    .minDistance(1)
    .onBegin(() => {
      isScrubbing.value = true;
      scale.value = withTiming(1.2);
      if (onSeekStart) runOnJS(onSeekStart)();
    })
    .onChange((event) => {
      translateX.value = Math.max(
        0,
        Math.min(barWidth, translateX.value + event.changeX)
      );
    })
    .onEnd(() => {
      const finalProgress = translateX.value / barWidth;
      runOnJS(onSeek)(finalProgress);
      isScrubbing.value = false;
      scale.value = withTiming(1);
      if (onSeekEnd) runOnJS(onSeekEnd)();
    });

  const tapGesture = Gesture.Tap()
    .onBegin(() => {
      if (onSeekStart) runOnJS(onSeekStart)();
    })
    .onEnd((event) => {
      const newTranslateX = Math.max(0, Math.min(barWidth, event.x));
      translateX.value = newTranslateX;
      const finalProgress = newTranslateX / barWidth;
      runOnJS(onSeek)(finalProgress);
      if (onSeekEnd) runOnJS(onSeekEnd)();
    });

  const composedGesture = Gesture.Race(panGesture, tapGesture);

  const animatedProgressStyle = useAnimatedStyle(() => ({
    width: translateX.value,
  }));

  const animatedThumbStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: translateX.value }, { scale: scale.value }],
  }));

  return (
    <GestureDetector gesture={composedGesture}>
      <Animated.View
        style={progressStyles.container}
        onLayout={(e) => setBarWidth(e.nativeEvent.layout.width)}
      >
        <View style={progressStyles.track} />
        <Animated.View
          style={[progressStyles.progress, animatedProgressStyle]}
        />
        <Animated.View
          style={[progressStyles.thumbContainer, animatedThumbStyle]}
        >
          <View style={progressStyles.thumb} />
        </Animated.View>
      </Animated.View>
    </GestureDetector>
  );
};

// --- Main Video Component ---
export const Video = forwardRef<VideoRef, VideoProps>(
  (
    {
      source,
      style,
      autoPlay = false,
      loop = false,
      muted = false,
      nativeControls = false,
      allowsFullscreen = true,
      allowsPictureInPicture = true,
      contentFit = 'cover',
      onLoad,
      onError,
      seekBy = 2,
      onPlaybackStatusUpdate,
      onFullscreenUpdate,
      subtitles = [],
      ...props
    },
    ref
  ) => {
    const textColor = useColor('text');
    const cardColor = useColor('card');
    const mutedColor = useColor('mutedForeground');

    const [currentTime, setCurrentTime] = useState(0);
    const [duration, setDuration] = useState(0);
    const [isMuted, setIsMuted] = useState(muted);
    const [currentSubtitle, setCurrentSubtitle] = useState<string>('');
    const [isVideoEnded, setIsVideoEnded] = useState(false);
    const [showPlayIcon, setShowPlayIcon] = useState(false);
    const [showCustomControls, setShowCustomControls] = useState(false);
    const [isSeeking, setIsSeeking] = useState(false);

    const nativeRef = useRef<VideoView>(null);

    const hideControlsTimeout = useRef<ReturnType<typeof setTimeout> | null>(
      null
    );
    const hidePlayIconTimeout = useRef<ReturnType<typeof setTimeout> | null>(
      null
    );

    const controlsOpacity = useSharedValue(0);
    const playIconOpacity = useSharedValue(0);

    const player = useVideoPlayer(source, (player) => {
      try {
        if (autoPlay && player.play) player.play();
        player.loop = loop;
        player.muted = muted;
        onLoad?.();
      } catch (error) {
        console.error('Video player initialization error:', error);
        onError?.(error);
      }
    });

    const { isPlaying } = useEvent(player, 'playingChange', {
      isPlaying: player?.playing || false,
    });

    useImperativeHandle(ref, () => ({
      play: () => player.play(),
      pause: () => player.pause(),
      seekTo: (seconds: number) => {
        player.currentTime = seconds;
      },
      setVolume: (volume: number) => {
        player.volume = volume;
      },
      getCurrentTime: () => player.currentTime,
      getDuration: () => player.duration,
      isPlaying: () => player.playing,
      isMuted: () => player.muted,
    }));

    // --- !! EFFECT UPDATED TO RESPECT isSeeking STATE !! ---
    useEffect(() => {
      const interval = setInterval(() => {
        // Only update time from player if the user is not actively seeking
        if (player && !isSeeking) {
          const time = player.currentTime || 0;
          const dur = player.duration || 0;
          setCurrentTime(time);
          if (dur > 0) setDuration(dur);

          if (dur > 0 && time >= dur - 0.25 && !loop) setIsVideoEnded(true);
          else setIsVideoEnded(false);

          const activeSubtitle = subtitles.find(
            (s) => time >= s.start && time <= s.end
          );
          setCurrentSubtitle(activeSubtitle?.text || '');

          onPlaybackStatusUpdate?.({
            currentTime: time,
            duration: dur,
            isPlaying: player.playing,
          });
        }
      }, 250);

      return () => clearInterval(interval);
    }, [player, subtitles, onPlaybackStatusUpdate, loop, isSeeking]);

    const controlsAnimatedStyle = useAnimatedStyle(() => ({
      opacity: controlsOpacity.value,
    }));

    const playIconAnimatedStyle = useAnimatedStyle(() => ({
      opacity: playIconOpacity.value,
    }));

    const showControls = useCallback(() => {
      setShowCustomControls(true);
      controlsOpacity.value = withTiming(1, { duration: 200 });

      if (hideControlsTimeout.current)
        clearTimeout(hideControlsTimeout.current);
      if (isPlaying) {
        // Only hide controls if video is playing
        hideControlsTimeout.current = setTimeout(hideControls, 3000);
      }
    }, [controlsOpacity, isPlaying]);

    const hideControls = useCallback(() => {
      controlsOpacity.value = withTiming(0, { duration: 200 }, (isFinished) => {
        if (isFinished) runOnJS(setShowCustomControls)(false);
      });
    }, [controlsOpacity]);

    const showPlayIconAnimation = useCallback(() => {
      setShowPlayIcon(true);
      playIconOpacity.value = withTiming(1, { duration: 200 });

      if (hidePlayIconTimeout.current)
        clearTimeout(hidePlayIconTimeout.current);
      hidePlayIconTimeout.current = setTimeout(() => {
        playIconOpacity.value = withTiming(
          0,
          { duration: 200 },
          (isFinished) => {
            if (isFinished) runOnJS(setShowPlayIcon)(false);
          }
        );
      }, 1000);
    }, [playIconOpacity]);

    const handleSingleTap = useCallback(() => {
      if (!player) return;
      if (isVideoEnded) {
        player.currentTime = 0;
        player.play();
        setIsVideoEnded(false);
      } else {
        player.playing ? player.pause() : player.play();
      }
      showPlayIconAnimation();
      showControls();
    }, [player, isVideoEnded, showControls, showPlayIconAnimation]);

    const handleLeftDoubleTap = useCallback(() => {
      if (player) {
        player.seekBy(-seekBy);
        showControls();
      }
    }, [player, showControls, seekBy]);

    const handleRightDoubleTap = useCallback(() => {
      if (player) {
        player.seekBy(seekBy);
        showControls();
      }
    }, [player, showControls, seekBy]);

    const toggleMute = useCallback(() => {
      const newMuted = !isMuted;
      setIsMuted(newMuted);
      player.muted = newMuted;
    }, [isMuted, player]);

    const handleProgressChange = useCallback(
      (progress: number) => {
        if (!player || !duration || duration <= 0) return;
        const newTime = progress * duration;
        // This is the "optimistic update" - we set the local state immediately
        setCurrentTime(newTime);
        player.currentTime = newTime;
        if (isVideoEnded) setIsVideoEnded(false);
        // Reset the hide controls timer
        if (hideControlsTimeout.current)
          clearTimeout(hideControlsTimeout.current);
        hideControlsTimeout.current = setTimeout(hideControls, 3000);
      },
      [player, duration, isVideoEnded, hideControls]
    );

    const handleSeekStart = useCallback(() => {
      setIsSeeking(true);
      if (hideControlsTimeout.current)
        clearTimeout(hideControlsTimeout.current);
    }, []);

    const handleSeekEnd = useCallback(() => {
      setIsSeeking(false);
    }, []);

    useEffect(() => {
      return () => {
        if (hideControlsTimeout.current)
          clearTimeout(hideControlsTimeout.current);
        if (hidePlayIconTimeout.current)
          clearTimeout(hidePlayIconTimeout.current);
      };
    }, []);

    return (
      <GestureHandlerRootView
        style={[styles.container, { backgroundColor: cardColor }, style]}
      >
        <VideoView
          ref={nativeRef}
          player={player}
          style={styles.video}
          fullscreenOptions={{ enable: allowsFullscreen }}
          allowsPictureInPicture={allowsPictureInPicture}
          nativeControls={nativeControls}
          contentFit={contentFit}
          onFullscreenEnter={() => onFullscreenUpdate?.(true)}
          onFullscreenExit={() => onFullscreenUpdate?.(false)}
          {...props}
        />
        <View style={styles.gestureOverlay}>
          <TouchableOpacity
            style={styles.gestureArea}
            onPress={handleLeftDoubleTap}
            activeOpacity={0}
            accessibilityRole='button'
            accessibilityLabel={`Rewind ${seekBy} seconds`}
          />
          <TouchableOpacity
            style={styles.gestureAreaCenter}
            onPress={handleSingleTap}
            activeOpacity={0}
            accessibilityRole='button'
            accessibilityLabel={isPlaying ? 'Pause' : 'Play'}
          />
          <TouchableOpacity
            style={styles.gestureArea}
            onPress={handleRightDoubleTap}
            activeOpacity={0}
            accessibilityRole='button'
            accessibilityLabel={`Forward ${seekBy} seconds`}
          />
        </View>

        {showCustomControls && (
          <Animated.View
            style={[styles.controlsContainer, controlsAnimatedStyle]}
            pointerEvents='box-none'
          >
            <View style={styles.topControls}>
              <TouchableOpacity
                onPress={toggleMute}
                style={styles.controlButton}
                activeOpacity={0.7}
                accessibilityRole='button'
                accessibilityLabel={isMuted ? 'Unmute' : 'Mute'}
              >
                {isMuted ? (
                  <VolumeX size={24} color={textColor} />
                ) : (
                  <Volume2 size={24} color={textColor} />
                )}
              </TouchableOpacity>
            </View>

            <View style={styles.bottomControls}>
              <View style={styles.timeContainer}>
                <Text style={[styles.timeText, { color: mutedColor }]}>
                  {formatTime(currentTime)}
                </Text>
                <Text style={[styles.timeText, { color: mutedColor }]}>
                  {formatTime(duration)}
                </Text>
              </View>

              <ReanimatedProgress
                duration={duration}
                currentTime={currentTime}
                onSeek={handleProgressChange}
                onSeekStart={handleSeekStart}
                onSeekEnd={handleSeekEnd}
              />
            </View>
          </Animated.View>
        )}
      </GestureHandlerRootView>
    );
  }
);

Video.displayName = 'Video';

const styles = StyleSheet.create({
  container: {
    width: '100%',
    height: '100%',
    borderRadius: BORDER_RADIUS,
    overflow: 'hidden',
  },
  video: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    width: '100%',
    height: '100%',
  },
  gestureOverlay: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    flexDirection: 'row',
  },
  gestureArea: { flex: 1, backgroundColor: 'transparent' },
  gestureAreaCenter: { flex: 2, backgroundColor: 'transparent' },
  centerPlayIcon: {
    position: 'absolute',
    top: '50%',
    left: '50%',
    transform: [{ translateX: -40 }, { translateY: -40 }],
    zIndex: 100,
  },
  centerPlayIconBackground: {
    width: 80,
    height: 80,
    borderRadius: 40,
    backgroundColor: 'rgba(0, 0, 0, 0.7)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  subtitleContainer: {
    position: 'absolute',
    bottom: 80,
    left: 20,
    right: 20,
    alignItems: 'center',
  },
  subtitleText: {
    fontSize: 16,
    textAlign: 'center',
    backgroundColor: 'rgba(0, 0, 0, 0.7)',
    paddingHorizontal: 12,
    paddingVertical: 8,
    borderRadius: 6,
  },
  controlsContainer: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: 'rgba(0, 0, 0, 0.3)',
    justifyContent: 'space-between',
  },
  topControls: {
    flexDirection: 'row',
    justifyContent: 'flex-end',
    padding: 16,
  },
  bottomControls: { padding: 16, gap: 6, paddingBottom: 6 },
  timeContainer: { flexDirection: 'row', justifyContent: 'space-between' },
  timeText: { fontSize: 12 },
  controlButton: {
    width: 44,
    height: 44,
    borderRadius: 22,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
  },
});
const progressStyles = StyleSheet.create({
  container: { height: THUMB_SIZE * 2, justifyContent: 'center' },
  track: {
    height: PROGRESS_HEIGHT,
    backgroundColor: 'rgba(255, 255, 255, 0.3)',
    borderRadius: PROGRESS_HEIGHT / 2,
  },
  progress: {
    height: PROGRESS_HEIGHT,
    backgroundColor: '#FFFFFF',
    borderRadius: PROGRESS_HEIGHT / 2,
    position: 'absolute',
  },
  thumbContainer: {
    position: 'absolute',
    top: (THUMB_SIZE * 2 - THUMB_SIZE) / 2,
    left: -THUMB_SIZE / 2,
  },
  thumb: {
    width: THUMB_SIZE,
    height: THUMB_SIZE,
    borderRadius: THUMB_SIZE / 2,
    backgroundColor: '#FFFFFF',
  },
});

export type { VideoProps, VideoRef, VideoSource };
```

**3.** Update the import paths to match your project setup.

## Usage

```tsx
import { Video } from '@/components/ui/video';
```

```tsx
<Video
  source={{ uri: 'https://example.com/video.mp4' }}
  autoPlay={true}
  showControls={true}
/>
```

## Examples

#### Default

**Example:** A basic video player with custom controls

```tsx
// components/demo/video/video-demo.tsx
import { Video } from '@/components/ui/video';

export function VideoDemo() {
  return (
    <Video
      source={{
        uri: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4',
      }}
      style={{
        width: '100%',
        height: 200,
        borderRadius: 8,
      }}
      autoPlay={false}
      loop={false}
      muted={false}
      showControls={true}
    />
  );
}
```

#### Native Controls

**Example:** Video player using native system controls

```tsx
// components/demo/video/video-native-controls.tsx
import { Video } from '@/components/ui/video';
import React from 'react';

export function VideoNativeControls() {
  return (
    <Video
      source={{
        uri: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
      }}
      style={{
        width: '100%',
        height: 200,
        borderRadius: 8,
      }}
      nativeControls={true}
      autoPlay={false}
      loop={false}
    />
  );
}
```

#### Custom Controls

**Example:** Video player with custom control interface

```tsx
// components/demo/video/video-custom-controls.tsx
import { Video } from '@/components/ui/video';
import React from 'react';

export function VideoCustomControls() {
  return (
    <Video
      source={{
        uri: 'https://ui.ahmedbna.com/',
      }}
      style={{
        width: '100%',
        height: 250,
        borderRadius: 12,
      }}
      nativeControls={false}
      showControls={true}
      autoPlay={false}
      loop={true}
      seekBy={5}
      onPlaybackStatusUpdate={(status) => {
        console.log('Playback status:', status);
      }}
      onLoad={() => {
        console.log('Video loaded successfully');
      }}
    />
  );
}
```

#### With Subtitles

**Example:** Video player with subtitle support

```tsx
// components/demo/video/video-subtitles.tsx
import { Video } from '@/components/ui/video';
import React from 'react';

export function VideoSubtitles() {
  const subtitles = [
    { start: 0, end: 3, text: 'Welcome to our video demo' },
    { start: 3, end: 6, text: 'This video shows subtitle support' },
    { start: 6, end: 9, text: 'Subtitles appear at the bottom' },
    { start: 9, end: 12, text: 'They automatically sync with playback' },
    { start: 12, end: 15, text: 'Perfect for accessibility!' },
  ];

  return (
    <Video
      source={{
        uri: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/SubaruOutbackOnStreetAndDirt.mp4',
      }}
      style={{
        width: '100%',
        height: 200,
        borderRadius: 8,
      }}
      subtitles={subtitles}
      autoPlay={true}
      loop={true}
      showControls={true}
    />
  );
}
```

#### Autoplay & Loop

**Example:** Video that automatically plays and loops

```tsx
// components/demo/video/video-autoplay-loop.tsx
import { Video } from '@/components/ui/video';
import React from 'react';

export function VideoAutoplayLoop() {
  return (
    <Video
      source={{
        uri: 'https://ui.ahmedbna.com/',
      }}
      style={{
        width: '100%',
        height: 180,
        borderRadius: 8,
      }}
      autoPlay={true}
      loop={true}
      muted={true}
      showControls={true}
      contentFit='cover'
    />
  );
}
```

#### Different Sources

**Example:** Video players with different source types

```tsx
// components/demo/video/video-sources.tsx
import { Text } from '@/components/ui/text';
import { Video } from '@/components/ui/video';
import { View } from '@/components/ui/view';
import React from 'react';

export function VideoSources() {
  const videoSources = [
    {
      title: 'MP4 Source',
      uri: 'https://ui.ahmedbna.com/',
    },
    {
      title: 'Alternative MP4',
      uri: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4',
    },
  ];

  return (
    <View style={{ gap: 16 }}>
      {videoSources.map((source, index) => (
        <View key={index} style={{ gap: 8 }}>
          <Text variant='body' style={{ fontWeight: '600' }}>
            {source.title}
          </Text>
          <Video
            source={{ uri: source.uri }}
            style={{
              width: '100%',
              height: 160,
              borderRadius: 8,
            }}
            autoPlay={false}
            showControls={true}
          />
        </View>
      ))}
    </View>
  );
}
```

#### Gesture Controls

**Example:** Video player with tap-to-play and seek gestures

```tsx
// components/demo/video/video-gestures.tsx
import { Text } from '@/components/ui/text';
import { Video } from '@/components/ui/video';
import { View } from '@/components/ui/view';
import React from 'react';

export function VideoGestures() {
  return (
    <View style={{ gap: 12 }}>
      <Text variant='body' style={{ fontSize: 14, opacity: 0.8 }}>
        Tap center to play/pause • Tap left to seek back • Tap right to seek
        forward
      </Text>
      <Video
        source={{
          uri: 'https://ui.ahmedbna.com/',
        }}
        style={{
          width: '100%',
          height: 220,
          borderRadius: 12,
        }}
        seekBy={10}
        autoPlay={false}
        showControls={true}
        onPlaybackStatusUpdate={(status) => {
          // Handle playback updates
        }}
      />
      <Text variant='caption' style={{ textAlign: 'center', opacity: 0.6 }}>
        Try the gesture controls! This video seeks by 10 seconds.
      </Text>
    </View>
  );
}
```

#### Content Fit Options

**Example:** Videos with different content fitting options

```tsx
// components/demo/video/video-content-fit.tsx
import { Text } from '@/components/ui/text';
import { Video } from '@/components/ui/video';
import { View } from '@/components/ui/view';
import React from 'react';

export function VideoContentFit() {
  const contentFitOptions: Array<{
    mode: 'contain' | 'cover' | 'fill';
    description: string;
  }> = [
    { mode: 'contain', description: 'Fit entirely within bounds' },
    { mode: 'cover', description: 'Fill bounds, may crop' },
    { mode: 'fill', description: 'Stretch to fill bounds' },
  ];

  return (
    <View style={{ gap: 16 }}>
      {contentFitOptions.map((option, index) => (
        <View key={index} style={{ gap: 8 }}>
          <View>
            <Text variant='body' style={{ fontWeight: '600' }}>
              {option.mode.charAt(0).toUpperCase() + option.mode.slice(1)}
            </Text>
            <Text variant='caption' style={{ opacity: 0.7 }}>
              {option.description}
            </Text>
          </View>
          <Video
            source={{
              uri: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4',
            }}
            style={{
              width: '100%',
              height: 120,
              borderRadius: 8,
              backgroundColor: '#f0f0f0',
            }}
            contentFit={option.mode}
            autoPlay={false}
            showControls={true}
            muted={true}
          />
        </View>
      ))}
    </View>
  );
}
```

## API Reference

### Video

The main video player component with customizable controls and features.

| Prop                     | Type                              | Default   | Description                                       |
| ------------------------ | --------------------------------- | --------- | ------------------------------------------------- |
| `source`                 | `VideoSource`                     | -         | The video source (required).                      |
| `style`                  | `ViewStyle`                       | -         | Additional styles for the video container.        |
| `seekBy`                 | `number`                          | `2`       | Seconds to seek by on double tap.                 |
| `autoPlay`               | `boolean`                         | `false`   | Whether to auto-play the video.                   |
| `loop`                   | `boolean`                         | `false`   | Whether to loop the video.                        |
| `muted`                  | `boolean`                         | `false`   | Whether to start muted.                           |
| `nativeControls`         | `boolean`                         | `false`   | Use native video controls instead of custom ones. |
| `showControls`           | `boolean`                         | `true`    | Whether to show custom controls.                  |
| `allowsFullscreen`       | `boolean`                         | `true`    | Allow fullscreen mode.                            |
| `allowsPictureInPicture` | `boolean`                         | `true`    | Allow picture-in-picture mode.                    |
| `contentFit`             | `'contain' \| 'cover' \| 'fill'`  | `'cover'` | How the video should fit within its container.    |
| `onLoad`                 | `() => void`                      | -         | Callback when video is loaded.                    |
| `onError`                | `(error: any) => void`            | -         | Callback when an error occurs.                    |
| `onPlaybackStatusUpdate` | `(status: any) => void`           | -         | Callback for playback status updates.             |
| `onFullscreenUpdate`     | `(isFullscreen: boolean) => void` | -         | Callback when fullscreen state changes.           |
| `subtitles`              | `Array<Subtitle>`                 | `[]`      | Array of subtitle objects.                        |

### VideoRef

Reference methods available on the Video component.

| Method           | Type                        | Description                              |
| ---------------- | --------------------------- | ---------------------------------------- |
| `play`           | `() => void`                | Start playing the video.                 |
| `pause`          | `() => void`                | Pause the video.                         |
| `seekTo`         | `(seconds: number) => void` | Seek to a specific time in seconds.      |
| `setVolume`      | `(volume: number) => void`  | Set the volume (0-1).                    |
| `getCurrentTime` | `() => number`              | Get the current playback time.           |
| `getDuration`    | `() => number`              | Get the total duration of the video.     |
| `isPlaying`      | `() => boolean`             | Check if the video is currently playing. |
| `isMuted`        | `() => boolean`             | Check if the video is muted.             |

### Subtitle

Subtitle object structure for the subtitles prop.

| Property | Type     | Description                             |
| -------- | -------- | --------------------------------------- |
| `start`  | `number` | Start time in seconds for the subtitle. |
| `end`    | `number` | End time in seconds for the subtitle.   |
| `text`   | `string` | The subtitle text to display.           |

## Gestures

The Video component supports several gesture interactions:

- **Single tap (center)**: Play/pause toggle
- **Single tap (left side)**: Seek backward by `seekBy` seconds
- **Single tap (right side)**: Seek forward by `seekBy` seconds
- **Progress bar tap**: Seek to specific position

## Features

### Custom Controls

- Play/pause button with visual feedback
- Progress bar with seek functionality
- Time display (current/total)
- Mute/unmute toggle
- Auto-hide controls after 3 seconds

### Subtitle Support

- Display subtitles based on current playback time
- Customizable subtitle styling
- Automatic subtitle timing

### Error Handling

- Graceful error handling with fallbacks
- Console logging for debugging
- Error callbacks for custom handling

### Accessibility

The Video component is built with accessibility in mind:

- Touch targets meet minimum size requirements
- Clear visual feedback for interactions
- Subtitle support for hearing accessibility
- Proper semantic structure for screen readers

## Performance Considerations

- Efficient playback status updates (100ms intervals)
- Optimized gesture handling
- Memory cleanup on component unmount
- Smooth animations with native driver when possible
