# Camera Preview

> A comprehensive camera component with capture, preview, and media management capabilities.

**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/camera-preview
- Markdown: https://ui.ahmedbna.com/docs/components/camera-preview.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/camera-preview.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/camera-preview.json
- Install: `npx bna-ui add camera-preview`
- npm dependencies: `expo-camera`, `expo-haptics`, `expo-image`, `expo-media-library`, `expo-video`, `lucide-react-native`, `react-native-gesture-handler`, `react-native-reanimated`, `react-native-safe-area-context`, `react-native-svg`, `react-native-worklets`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `useHaptics`, `globals`, `text`, `view`, `icon`, `spinner`, `button`, `image`, `progress`, `video`, `camera`
- Preview recording: https://demo.ahmedbna.com/0082-camera-preview-demo.mov

---

**Example:** A basic camera preview with capture and save functionality

```tsx
// components/demo/camera-preview/camera-preview-demo.tsx
import { CameraPreview } from '@/components/ui/camera-preview';

export function CameraPreviewDemo() {
  return <CameraPreview />;
}
```

## Installation

### CLI

```bash
npx bna-ui add camera-preview
```

### Manual

**1.** Install the following dependencies:

```bash
npx expo install expo-camera expo-media-library lucide-react-native
```

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

```tsx
// components/ui/camera-preview.tsx
import { Button } from '@/components/ui/button';
import { Camera, CaptureSuccess } from '@/components/ui/camera';
import { Image } from '@/components/ui/image';
import { Text } from '@/components/ui/text';
import { Video } from '@/components/ui/video';
import { View } from '@/components/ui/view';
import { useColor } from '@/hooks/useColor';
import * as MediaLibrary from 'expo-media-library';
import { Download, Upload, X } from 'lucide-react-native';
import { useState } from 'react';
import { Alert, Dimensions, StyleSheet, TouchableOpacity } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';

const { width: screenWidth } = Dimensions.get('window');

export function CameraPreview() {
  const [showCamera, setShowCamera] = useState(false);
  const [cameraHeight, setCameraHeight] = useState((screenWidth * 4) / 3);
  const [capturedMedia, setCapturedMedia] = useState<{
    uri: string;
    type: 'picture' | 'video';
  } | null>(null);
  const [showPreview, setShowPreview] = useState(false);
  const [mediaLibraryPermission, requestMediaLibraryPermission] =
    MediaLibrary.usePermissions();

  const backgroundColor = useColor('background');
  const cardColor = useColor('card');
  const textColor = useColor('text');

  const handleCapture = (results: CaptureSuccess) => {
    setCameraHeight(results.cameraHeight);
    setCapturedMedia({ type: results.type, uri: results.uri });
    setShowCamera(false);
    setShowPreview(true);
  };

  const handleVideoCapture = (results: CaptureSuccess) => {
    setCameraHeight(results.cameraHeight);
    setCapturedMedia({ type: results.type, uri: results.uri });
    setShowCamera(false);
    setShowPreview(true);
  };

  const handleOpenCamera = () => {
    setCapturedMedia(null);
    setShowPreview(false);
    setShowCamera(true);
  };

  const handleCloseCamera = () => {
    setShowCamera(false);
  };

  const handleRetakeMedia = () => {
    setCapturedMedia(null);
    setShowPreview(false);
    setShowCamera(true);
  };

  const handleSaveToAlbum = async () => {
    if (!capturedMedia) return;

    try {
      // Request permission if not granted
      if (mediaLibraryPermission?.status !== 'granted') {
        const permission = await requestMediaLibraryPermission();
        if (!permission.granted) {
          Alert.alert(
            'Permission Required',
            'Please grant permission to save media to your picture library.'
          );
          return;
        }
      }

      // Save to media library. SDK 56 removed `saveToLibraryAsync` — it still
      // type-checks from the root entrypoint but throws at runtime.
      await MediaLibrary.Asset.create(capturedMedia.uri);

      Alert.alert(
        'Success!',
        `${
          capturedMedia.type === 'picture' ? 'Photo' : 'Video'
        } saved to your picture library.`,
        [
          {
            text: 'OK',
            onPress: () => {
              setCapturedMedia(null);
              setShowPreview(false);
            },
          },
        ]
      );
    } catch (error) {
      console.error('Error saving to album:', error);
      Alert.alert('Error', 'Failed to save media to your picture library.');
    }
  };

  const handleUploadAction = () => {
    if (!capturedMedia) return;

    // This is where you would implement your upload logic
    // For example: upload to a server, save to database, etc.

    const mediaDetails = {
      uri: capturedMedia.uri,
      type: capturedMedia.type,
      timestamp: new Date().toISOString(),
      // Add any other metadata you need
    };

    console.log('Media details for upload/processing:', mediaDetails);

    // Example: Call your upload function
    // uploadToServer(mediaDetails);
    // saveToDatabase(mediaDetails);

    Alert.alert(
      'Upload Action',
      `${
        capturedMedia.type === 'picture' ? 'Photo' : 'Video'
      } ready for processing.\n\nCheck console for media details.`,
      [
        {
          text: 'Continue',
          onPress: () => {
            // You might want to keep the preview open or close it
            // depending on your use case
          },
        },
        {
          text: 'Done',
          onPress: () => {
            // setCapturedMedia(null);
            // setShowPreview(false);
          },
        },
      ]
    );
  };

  // Preview Mode
  if (showPreview && capturedMedia) {
    return (
      <SafeAreaView style={[styles.container, { backgroundColor }]}>
        <View style={[styles.previewContainer, { height: cameraHeight }]}>
          {capturedMedia.type === 'picture' && capturedMedia.uri ? (
            <Image source={{ uri: capturedMedia.uri }} />
          ) : (
            <Video
              source={{ uri: capturedMedia.uri }}
              // nativeControls
              allowsFullscreen
              allowsPictureInPicture
            />
          )}

          {/* Top Floating Buttons */}
          <View style={styles.topFloatingButtons}>
            <View
              style={{
                flex: 1,
                flexDirection: 'row',
                alignItems: 'center',
                justifyContent: 'space-between',
              }}
            >
              <TouchableOpacity
                style={[
                  styles.floatingButton,
                  { backgroundColor: cardColor, opacity: 0.9 },
                ]}
                onPress={handleRetakeMedia}
                activeOpacity={0.8}
                accessibilityRole='button'
                accessibilityLabel='Retake'
              >
                <X size={24} color={textColor} />
              </TouchableOpacity>

              <View
                style={{
                  flexDirection: 'row',
                  alignItems: 'center',
                  justifyContent: 'space-between',
                  gap: 16,
                }}
              >
                <TouchableOpacity
                  style={[
                    styles.floatingButton,
                    { backgroundColor: cardColor, opacity: 0.9 },
                  ]}
                  onPress={handleSaveToAlbum}
                  activeOpacity={0.8}
                  accessibilityRole='button'
                  accessibilityLabel='Save to album'
                >
                  <Download size={24} color={textColor} />
                </TouchableOpacity>

                <TouchableOpacity
                  style={[
                    styles.floatingButton,
                    { backgroundColor: cardColor, opacity: 0.9 },
                  ]}
                  onPress={handleUploadAction}
                  activeOpacity={0.8}
                  accessibilityRole='button'
                  accessibilityLabel='Upload'
                >
                  <Upload size={24} color={textColor} />
                </TouchableOpacity>
              </View>
            </View>
          </View>
        </View>
      </SafeAreaView>
    );
  }

  // Camera Mode
  if (showCamera) {
    return (
      <Camera
        onCapture={handleCapture}
        onVideoCapture={handleVideoCapture}
        onClose={handleCloseCamera}
        facing='back'
        enableTorch={true}
        showControls={true}
        enableVideo={true}
        style={{ flex: 1 }}
      />
    );
  }

  // Main Screen
  return (
    <SafeAreaView style={[styles.container, { backgroundColor }]}>
      <View style={styles.content}>
        <Text variant='heading' style={styles.title}>
          Camera Component
        </Text>

        <Text variant='body' style={styles.description}>
          Tap the button below to open the camera and capture photos or videos.
          After capturing, you can preview, save, or process your media.
        </Text>

        <View style={styles.buttonContainer}>
          <Button
            variant='default'
            size='lg'
            onPress={handleOpenCamera}
            style={styles.button}
          >
            Open Camera
          </Button>
        </View>
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  content: {
    flex: 1,
    padding: 20,
    justifyContent: 'center',
    alignItems: 'center',
  },
  title: {
    marginBottom: 16,
    textAlign: 'center',
  },
  description: {
    textAlign: 'center',
    marginBottom: 32,
    paddingHorizontal: 20,
  },
  lastCaptureContainer: {
    padding: 16,
    borderRadius: 12,
    marginBottom: 32,
    alignItems: 'center',
    maxWidth: '100%',
  },
  lastCaptureTitle: {
    marginBottom: 12,
  },
  thumbnailImage: {
    width: 120,
    height: 120,
    borderRadius: 8,
  },
  videoThumbnailContainer: {
    position: 'relative',
    width: 120,
    height: 120,
    borderRadius: 8,
    overflow: 'hidden',
  },
  playIconOverlay: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(0, 0, 0, 0.3)',
  },
  playIcon: {
    fontSize: 24,
  },
  viewButton: {
    paddingHorizontal: 16,
    paddingVertical: 8,
    borderRadius: 8,
    marginTop: 12,
  },
  viewButtonText: {
    color: 'white',
    fontWeight: '600',
  },
  buttonContainer: {
    width: '100%',
    gap: 16,
    alignItems: 'center',
  },
  button: {
    minWidth: 200,
  },
  previewContainer: {
    width: screenWidth,
    borderRadius: 12,
    overflow: 'hidden',
    position: 'relative',
    marginHorizontal: 0,
  },
  previewMedia: {
    width: '100%',
    height: '100%',
  },
  topFloatingButtons: {
    position: 'absolute',
    bottom: 40,
    left: 20,
    right: 20,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  floatingButton: {
    width: 56,
    height: 56,
    borderRadius: 28,
    justifyContent: 'center',
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 4,
    elevation: 5,
  },
  bottomActionContainer: {
    padding: 20,
    alignItems: 'center',
  },
  uploadButton: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 32,
    paddingVertical: 16,
    borderRadius: 12,
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.15,
    shadowRadius: 4,
    elevation: 3,
  },
  uploadIcon: {
    marginRight: 12,
  },
  uploadButtonText: {
    color: 'white',
    fontSize: 18,
    fontWeight: '600',
  },
  mediaInfo: {
    alignItems: 'center',
    paddingHorizontal: 20,
    paddingBottom: 20,
  },
  mediaInfoText: {
    fontSize: 16,
    fontWeight: '600',
    marginBottom: 4,
  },
  mediaInfoSubtext: {
    fontSize: 14,
    textAlign: 'center',
  },
});
```

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

**4.** Add camera and media library permissions to your app.json:

```json
{
  "expo": {
    "plugins": [
      [
        "expo-camera",
        {
          "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera",
          "microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone",
          "recordAudioAndroid": true
        }
      ],
      [
        "expo-media-library",
        {
          "photosPermission": "Allow $(PRODUCT_NAME) to access your photos.",
          "savePhotosPermission": "Allow $(PRODUCT_NAME) to save photos.",
          "isAccessMediaLocationEnabled": true
        }
      ]
    ]
  }
}
```

## Usage

```tsx
import { CameraPreview } from '@/components/ui/camera-preview';
```

```tsx
<CameraPreview />
```

## Examples

#### Default

**Example:** A basic camera preview with capture and save functionality

```tsx
// components/demo/camera-preview/camera-preview-demo.tsx
import { CameraPreview } from '@/components/ui/camera-preview';

export function CameraPreviewDemo() {
  return <CameraPreview />;
}
```

## Features

### Camera Capabilities

- **Photo Capture**: High-quality photo capture with multiple resolution options
- **Video Recording**: Full video recording with audio support
- **Flash Control**: Built-in torch/flash toggle functionality
- **Camera Switching**: Front and back camera switching
- **Focus Control**: Tap-to-focus functionality

### Media Management

- **Preview Mode**: Full-screen preview of captured media
- **Save to Gallery**: Direct save to device photo library
- **Custom Upload**: Configurable upload handling
- **Media Processing**: Ready for custom processing workflows

### User Experience

- **Responsive Design**: Adapts to different screen sizes
- **Theme Support**: Automatic light/dark theme support
- **Permission Handling**: Graceful permission request flows
- **Error Handling**: Comprehensive error handling and user feedback

## API Reference

### CameraPreview

`CameraPreview` takes **no props**. It's a self-contained, full-screen camera
screen that hardcodes its own configuration internally (back camera, torch
and video capture enabled) and manages capture, preview, and save-to-gallery
state on its own. It composes the registry's own `Camera` and `Video`
components — there is no `CameraPreviewProps` type, no `onCapture`/`onError`/
`onPermission` events, and no `MediaDetails` type to import.

If you need a configurable camera with props and callbacks, use
[`Camera`](/docs/components/camera) directly and build your own preview flow
around it.

## Permissions

The Camera Preview component requires the following permissions:

### iOS

- **Camera**: Required for photo and video capture
- **Microphone**: Required for video recording with audio
- **Photo Library**: Required for saving media to gallery

### Android

- **CAMERA**: Required for photo and video capture
- **RECORD\_AUDIO**: Required for video recording with audio
- **WRITE\_EXTERNAL\_STORAGE**: Required for saving media
- **READ\_EXTERNAL\_STORAGE**: Required for accessing saved media

## Best Practices

### Performance

- Use appropriate quality settings for your use case
- Implement proper cleanup when component unmounts
- Handle memory management for large media files
- Use compression for uploaded media when appropriate

### User Experience

- Always request permissions gracefully
- Provide clear feedback during capture and processing
- Implement proper loading states
- Handle edge cases like low storage space

### Security

- Validate uploaded media on your backend
- Implement proper file type checking
- Consider implementing media scanning for inappropriate content
- Use secure upload endpoints with proper authentication

## Accessibility

The Camera Preview component is built with accessibility in mind:

- Screen reader support for all interactive elements
- High contrast mode support
- Voice-over announcements for capture events
- Keyboard navigation support where applicable
- Proper focus management throughout the interface

## Troubleshooting

### Common Issues

**Camera not working on iOS simulator**

- The iOS simulator doesn't support camera functionality
- Test on a physical device for full functionality

**Permission denied errors**

- Ensure permissions are properly configured in app.json
- Check that users have granted necessary permissions
- Implement graceful fallbacks for denied permissions

**Media not saving to gallery**

- Verify Media Library permissions are granted
- Check available storage space
- Ensure proper error handling is implemented

**Video recording issues**

- Verify microphone permissions for audio recording
- Check maximum duration settings
- Monitor memory usage during long recordings
