# Camera

> A powerful camera component with advanced features like zoom, timer, torch, and video recording.

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

---

**Example:** A basic camera with default settings

```tsx
// components/demo/camera/camera-demo.tsx
import { Camera } from '@/components/ui/camera';
import React from 'react';
import { Alert } from 'react-native';

export function CameraDemo() {
  const handleCapture = ({ uri, type }: { uri: string; type: string }) => {
    Alert.alert('Picture Captured', `Saved to: ${uri}`);
  };

  const handleVideoCapture = ({ uri, type }: { uri: string; type: string }) => {
    Alert.alert('Video Recorded', `Saved to: ${uri}`);
  };

  return (
    <Camera
      onCapture={handleCapture}
      onVideoCapture={handleVideoCapture}
      style={{ height: 400 }}
    />
  );
}
```

## Installation

### CLI

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

### Manual

**1.** Install the following dependencies:

```bash
npx expo install expo-camera react-native-gesture-handler lucide-react-native
```

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

```tsx
// components/ui/camera.tsx
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Text } from '@/components/ui/text';
import { useColor } from '@/hooks/useColor';
import { BORDER_RADIUS, FONT_SIZE } from '@/theme/globals';
import {
  CameraMode,
  CameraRatio,
  CameraType,
  CameraView,
  useCameraPermissions,
} from 'expo-camera';
import {
  Camera as CameraIcon,
  Grid3X3,
  Settings,
  SwitchCamera,
  Timer,
  Video,
  Volume2,
  VolumeX,
  X,
  Zap,
  ZapOff,
} from 'lucide-react-native';
import React, {
  forwardRef,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import {
  ActivityIndicator,
  Alert,
  Dimensions,
  StyleSheet,
  TouchableOpacity,
  View,
  ViewStyle,
} from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  interpolate,
  runOnJS,
  useAnimatedProps,
  useAnimatedReaction,
  useAnimatedStyle,
  useSharedValue,
  withDelay,
  withSequence,
  withTiming,
} from 'react-native-reanimated';

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

const AnimatedCameraView = Animated.createAnimatedComponent(CameraView);

export type CaptureSuccess = {
  type: CameraMode;
  uri: string;
  cameraHeight: number;
};
export interface CameraProps {
  style?: ViewStyle;
  facing?: CameraType;
  enableTorch?: boolean;
  showControls?: boolean;
  timerOptions?: Array<number>;
  enableVideo?: boolean;
  maxVideoDuration?: number; // in seconds
  onClose?: () => void;
  onCapture?: ({ type, uri, cameraHeight }: CaptureSuccess) => void;
  onVideoCapture?: ({ type, uri, cameraHeight }: CaptureSuccess) => void;
}

export interface CameraRef {
  switchCamera: () => void;
  toggleTorch: () => void;
  takePicture: () => Promise<void>;
  startRecording: () => Promise<void>;
  stopRecording: () => Promise<void>;
}

export const Camera = forwardRef<CameraRef, CameraProps>(
  (
    {
      style,
      onCapture,
      onVideoCapture,
      onClose,
      enableTorch = true,
      showControls = true,
      enableVideo = true,
      maxVideoDuration = 60,
      timerOptions = [0, 3, 10],
      facing: initialFacing = 'back',
    },
    ref
  ) => {
    const cameraRef = useRef<CameraView>(null);
    const recordingInterval = useRef<ReturnType<typeof setTimeout> | null>(
      null
    );
    const timerInterval = useRef<ReturnType<typeof setTimeout> | null>(null);

    const fadeAnim = useSharedValue(0);
    const settingsAnim = useSharedValue(0);
    const zoomTextAnim = useSharedValue(0);
    const zoomControlsAnim = useSharedValue(0);
    const zoom = useSharedValue(0);
    const baseZoom = useSharedValue(0);

    const aspectRatios: Array<CameraRatio> = ['16:9', '4:3', '1:1'];

    const [permission, requestPermission] = useCameraPermissions();
    const [torch, setTorch] = useState(false);
    const [isCapturing, setIsCapturing] = useState(false);
    const [isRecording, setIsRecording] = useState(false);
    const [recordingTime, setRecordingTime] = useState(0);
    const [mode, setMode] = useState<CameraMode>('picture');
    const [facing, setFacing] = useState<CameraType>(initialFacing);
    const [showGrid, setShowGrid] = useState(false);
    const [timerSeconds, setTimerSeconds] = useState(0);
    const [selectedTimer, setSelectedTimer] = useState<number>(0);
    const [isTimerActive, setIsTimerActive] = useState(false);
    const [soundEnabled, setSoundEnabled] = useState(true);
    const [showSettings, setShowSettings] = useState(false);
    const [aspectRatioIndex, setAspectRatioIndex] = useState(1);
    const [zoomControls, setZoomControls] = useState(false);
    const [availableZoomFactors] = useState<number[]>([
      0, 0.25, 0.5, 0.75, 1.0,
    ]);
    const [currentZoomIndex, setCurrentZoomIndex] = useState(0);
    const [zoomFactorText, setZoomFactorText] = useState('1×');
    const [zoomProgress, setZoomProgress] = useState(0);

    const backgroundColor = useColor('background');
    const textColor = useColor('text');
    const primaryColor = useColor('primary');
    const cardColor = useColor('card');
    const destructiveColor = useColor('destructive');

    useAnimatedReaction(
      () => zoom.value,
      (currentValue) => {
        const text =
          currentValue === 0 ? '1×' : `${(1 + currentValue * 4).toFixed(1)}×`; // Adjusted to .toFixed(1) for smoother feedback
        runOnJS(setZoomFactorText)(text);
        runOnJS(setZoomProgress)(currentValue * 100);
      },
      []
    );

    const animatedContainerStyle = useAnimatedStyle(() => ({
      opacity: fadeAnim.value,
    }));
    const animatedSettingsStyle = useAnimatedStyle(() => ({
      opacity: settingsAnim.value,
      transform: [
        { translateY: interpolate(settingsAnim.value, [0, 1], [-100, 0]) },
      ],
    }));
    const animatedZoomTextStyle = useAnimatedStyle(() => ({
      opacity: zoomTextAnim.value,
    }));
    const animatedZoomControlsStyle = useAnimatedStyle(() => ({
      opacity: zoomControlsAnim.value,
    }));
    const animatedCameraProps = useAnimatedProps(() => ({ zoom: zoom.value }));

    const pinchGesture = Gesture.Pinch()
      .onStart(() => {
        'worklet';
        // Save the current zoom level when the pinch gesture begins
        baseZoom.value = zoom.value;
      })
      .onUpdate((event) => {
        'worklet';
        // Calculate new zoom based on the starting zoom and the current scale
        // The sensitivity factor (e.g., * 0.5) can be adjusted for feel
        const newZoom = baseZoom.value + (event.scale - 1) * 0.5;
        // Clamp the zoom value between 0 and 1
        zoom.value = Math.min(Math.max(newZoom, 0), 1);
      })
      .onEnd(() => {
        'worklet';
        // We no longer need to set baseZoom here.
        // Just animate the indicator.
        zoomTextAnim.value = withSequence(
          withTiming(1, { duration: 200 }),
          withDelay(1000, withTiming(0, { duration: 200 }))
        );
      });

    const doubleTapGesture = Gesture.Tap()
      .numberOfTaps(2)
      .onEnd(() => {
        'worklet';
        const newZoom = zoom.value > 0 ? 0 : 0.5;
        zoom.value = withTiming(newZoom);
        baseZoom.value = newZoom; // Keep this for double tap, as it's an instant change
        zoomTextAnim.value = withSequence(
          withTiming(1, { duration: 200 }),
          withDelay(1000, withTiming(0, { duration: 200 }))
        );
      });

    const composedGestures = Gesture.Simultaneous(
      pinchGesture,
      doubleTapGesture
    );

    useImperativeHandle(ref, () => ({
      switchCamera: toggleCameraFacing,
      toggleTorch,
      takePicture: handleCapture,
      startRecording: handleStartRecording,
      stopRecording: handleStopRecording,
    }));

    useEffect(() => {
      fadeAnim.value = withTiming(1, { duration: 300 });
    }, [fadeAnim]);

    useEffect(() => {
      zoomControlsAnim.value = withTiming(zoomControls ? 1 : 0, {
        duration: 300,
      });
    }, [zoomControls, zoomControlsAnim]);

    useEffect(() => {
      return () => {
        if (recordingInterval.current) clearInterval(recordingInterval.current);
        if (timerInterval.current) clearInterval(timerInterval.current);
      };
    }, []);

    const getCameraHeight = () => {
      const currentAspectRatio = aspectRatios[aspectRatioIndex];
      switch (currentAspectRatio) {
        case '16:9':
          return (screenWidth * 16) / 9;
        case '1:1':
          return screenWidth;
        case '4:3':
        default:
          return (screenWidth * 4) / 3;
      }
    };

    const startTimer = (seconds: number) => {
      setTimerSeconds(seconds);
      setIsTimerActive(true);
      timerInterval.current = setInterval(() => {
        setTimerSeconds((prev) => {
          if (prev <= 1) {
            setIsTimerActive(false);
            if (timerInterval.current) clearInterval(timerInterval.current);
            setTimeout(() => {
              if (mode === 'picture') handleActualCapture();
              else handleStartRecording();
            }, 100);
            return 0;
          }
          return prev - 1;
        });
      }, 1000);
    };

    const cancelTimer = () => {
      if (timerInterval.current) clearInterval(timerInterval.current);
      setIsTimerActive(false);
      setTimerSeconds(0);
    };

    const handleActualCapture = async () => {
      if (!cameraRef.current || isCapturing || isRecording) return;
      try {
        setIsCapturing(true);
        const picture = await cameraRef.current.takePictureAsync({
          quality: 1,
          base64: false,
          exif: true,
        });
        if (picture && onCapture)
          onCapture({
            type: 'picture',
            uri: picture.uri,
            cameraHeight: getCameraHeight(),
          });
      } catch (error) {
        console.error('Error taking picture:', error);
        Alert.alert('Error', 'Failed to take picture');
      } finally {
        setIsCapturing(false);
      }
    };

    const handleStartRecording = async () => {
      if (!cameraRef.current || isRecording || isCapturing) return;
      try {
        setIsRecording(true);
        setRecordingTime(0);
        recordingInterval.current = setInterval(() => {
          setRecordingTime((prev) => {
            if (prev >= maxVideoDuration) {
              handleStopRecording();
              return prev;
            }
            return prev + 1;
          });
        }, 1000);
        const video = await cameraRef.current.recordAsync({
          maxDuration: maxVideoDuration,
        });
        if (video && onVideoCapture)
          onVideoCapture({
            type: 'video',
            uri: video.uri,
            cameraHeight: getCameraHeight(),
          });
      } catch (error) {
        console.error('Error starting recording:', error);
        Alert.alert('Error', 'Failed to start recording');
        setIsRecording(false);
      }
    };

    const handleCapture = async () => {
      if (isCapturing || isRecording || isTimerActive) return;
      if (selectedTimer > 0) startTimer(selectedTimer);
      else if (mode === 'picture') handleActualCapture();
      else handleStartRecording();
    };

    const handleStopRecording = async () => {
      if (!cameraRef.current || !isRecording) return;
      try {
        await cameraRef.current.stopRecording();
        if (recordingInterval.current) clearInterval(recordingInterval.current);
      } catch (error) {
        console.error('Error stopping recording:', error);
      } finally {
        setIsRecording(false);
        setRecordingTime(0);
      }
    };

    const toggleCameraFacing = () =>
      setFacing((c) => (c === 'back' ? 'front' : 'back'));
    const toggleTorch = () => setTorch((c) => !c);
    const toggleMode = () => {
      if (!isRecording && !isCapturing)
        setMode((c) => (c === 'picture' ? 'video' : 'picture'));
    };

    const toggleSettings = () => {
      setShowSettings((prev) => {
        const newValue = !prev;
        settingsAnim.value = withTiming(newValue ? 1 : 0, { duration: 300 });
        return newValue;
      });
    };

    const handleZoomSliderChange = (value: number) => {
      const newZoom = value / 100;
      zoom.value = newZoom;
      baseZoom.value = newZoom;
    };

    const formatTime = (seconds: number) => {
      const mins = Math.floor(seconds / 60);
      const secs = seconds % 60;
      return `${mins.toString().padStart(2, '0')}:${secs
        .toString()
        .padStart(2, '0')}`;
    };

    const getTimerButtonText = () =>
      selectedTimer === 0 ? 'OFF' : `${selectedTimer}s`;

    const handleZoomButtonTap = () => {
      const nextIndex = (currentZoomIndex + 1) % availableZoomFactors.length;
      const nextZoom = availableZoomFactors[nextIndex];
      setCurrentZoomIndex(nextIndex);
      zoom.value = withTiming(nextZoom);
      baseZoom.value = nextZoom;
      zoomTextAnim.value = withSequence(
        withTiming(1, { duration: 200 }),
        withDelay(1000, withTiming(0, { duration: 200 }))
      );
    };

    if (!permission) {
      return (
        <View style={[styles.container, { backgroundColor }, style]}>
          <ActivityIndicator size='large' color={primaryColor} />
          <Text style={[styles.loadingText, { color: textColor }]}>
            Loading camera...
          </Text>
        </View>
      );
    }

    if (!permission.granted) {
      return (
        <View
          style={[
            styles.permissionContainer,
            { backgroundColor: cardColor },
            style,
          ]}
        >
          <CameraIcon
            size={36}
            color={textColor}
            style={styles.permissionIcon}
          />
          <Text variant='title' style={{ textAlign: 'center' }}>
            Camera Access Required
          </Text>
          <Text variant='body' style={{ textAlign: 'center' }}>
            We need access to your camera to take pictures and videos
          </Text>
          <View style={{ width: '100%' }}>
            <Button onPress={requestPermission} style={{ width: '100%' }}>
              Grant Permission
            </Button>
          </View>
        </View>
      );
    }

    return (
      <Animated.View
        style={[
          styles.container,
          { backgroundColor },
          style,
          animatedContainerStyle,
        ]}
      >
        <View style={[styles.cameraContainer, { height: getCameraHeight() }]}>
          <GestureDetector gesture={composedGestures}>
            <AnimatedCameraView
              ref={cameraRef}
              mode={mode}
              style={styles.camera}
              facing={facing}
              enableTorch={torch}
              animateShutter={true}
              mirror={mode === 'picture' && facing === 'front'}
              ratio={aspectRatios[aspectRatioIndex]}
              animatedProps={animatedCameraProps}
            >
              {/* Children of CameraView are rendered as an overlay */}
              {showGrid && (
                <View style={styles.gridOverlay}>
                  <View style={styles.gridLines}>
                    <View style={[styles.gridLine, styles.verticalLine1]} />
                    <View style={[styles.gridLine, styles.verticalLine2]} />
                    <View style={[styles.gridLine, styles.horizontalLine1]} />
                    <View style={[styles.gridLine, styles.horizontalLine2]} />
                  </View>
                </View>
              )}
              <Animated.View
                style={[styles.zoomIndicator, animatedZoomTextStyle]}
                pointerEvents='none'
              >
                <Text style={styles.zoomText}>{zoomFactorText}</Text>
              </Animated.View>
              {isTimerActive && (
                <TouchableOpacity
                  style={styles.timerOverlay}
                  onPress={cancelTimer}
                  activeOpacity={1}
                >
                  <Text style={styles.timerText}>{timerSeconds}</Text>
                  <View style={styles.cancelTimerButton}>
                    <X size={20} color='white' />
                  </View>
                  <Text style={styles.tapToCancelText}>Tap to cancel</Text>
                </TouchableOpacity>
              )}
              {isRecording && (
                <View style={styles.recordingIndicator}>
                  <View style={styles.recordingDot} />
                  <Text style={styles.recordingText}>
                    REC {formatTime(recordingTime)}
                  </Text>
                </View>
              )}
              {showControls && (
                <>
                  <View style={styles.topControls}>
                    <View style={styles.topLeft}>
                      {onClose && (
                        <TouchableOpacity
                          style={[
                            styles.controlButton,
                            { backgroundColor: cardColor },
                          ]}
                          onPress={onClose}
                          activeOpacity={0.7}
                          accessibilityRole='button'
                          accessibilityLabel='Close camera'
                        >
                          <X size={24} color={textColor} />
                        </TouchableOpacity>
                      )}
                    </View>
                    <View style={styles.topCenter}>
                      <Text style={[styles.modeText, { color: textColor }]}>
                        {mode.toUpperCase()}
                      </Text>
                    </View>
                    <View style={styles.topRight}>
                      <TouchableOpacity
                        style={[
                          styles.controlButton,
                          { backgroundColor: cardColor },
                        ]}
                        onPress={toggleSettings}
                        activeOpacity={0.7}
                        accessibilityRole='button'
                        accessibilityLabel='Camera settings'
                        accessibilityState={{ expanded: showSettings }}
                      >
                        <Settings size={24} color={textColor} />
                      </TouchableOpacity>
                    </View>
                  </View>
                  <Animated.View
                    style={[
                      styles.settingsPanel,
                      { backgroundColor: cardColor },
                      animatedSettingsStyle,
                    ]}
                    pointerEvents={showSettings ? 'auto' : 'none'}
                  >
                    <View style={styles.settingsRow}>
                      <TouchableOpacity
                        style={[
                          styles.settingButton,
                          showGrid && { backgroundColor: primaryColor },
                        ]}
                        onPress={() => setShowGrid(!showGrid)}
                        accessibilityRole='button'
                        accessibilityLabel='Toggle grid overlay'
                        accessibilityState={{ selected: showGrid }}
                      >
                        <Grid3X3
                          size={20}
                          color={showGrid ? cardColor : textColor}
                        />
                      </TouchableOpacity>
                      <TouchableOpacity
                        style={[
                          styles.settingButton,
                          {
                            backgroundColor: soundEnabled
                              ? primaryColor
                              : cardColor,
                          },
                        ]}
                        onPress={() => setSoundEnabled(!soundEnabled)}
                        accessibilityRole='button'
                        accessibilityLabel='Toggle sound'
                        accessibilityState={{ selected: soundEnabled }}
                      >
                        {soundEnabled ? (
                          <Volume2 size={20} color={cardColor} />
                        ) : (
                          <VolumeX size={20} color={textColor} />
                        )}
                      </TouchableOpacity>
                      <TouchableOpacity
                        style={[
                          styles.settingButton,
                          { backgroundColor: cardColor },
                        ]}
                        onPress={() => setAspectRatioIndex((p) => (p + 1) % 3)}
                      >
                        <Text
                          style={[styles.settingText, { color: textColor }]}
                        >
                          {aspectRatios[aspectRatioIndex]}
                        </Text>
                      </TouchableOpacity>
                      <TouchableOpacity
                        style={[
                          styles.settingButton,
                          {
                            backgroundColor:
                              selectedTimer > 0 ? primaryColor : cardColor,
                          },
                        ]}
                        onPress={() => {
                          const ci = timerOptions.indexOf(selectedTimer);
                          const ni = (ci + 1) % timerOptions.length;
                          setSelectedTimer(timerOptions[ni]);
                        }}
                      >
                        <Timer
                          size={16}
                          color={selectedTimer > 0 ? cardColor : textColor}
                        />
                        <Text
                          style={[
                            styles.timerSettingText,
                            {
                              color: selectedTimer > 0 ? cardColor : textColor,
                            },
                          ]}
                        >
                          {getTimerButtonText()}
                        </Text>
                      </TouchableOpacity>
                    </View>
                  </Animated.View>
                  <View style={styles.sideControls}>
                    {enableTorch && facing === 'back' && (
                      <TouchableOpacity
                        style={[
                          styles.controlButton,
                          {
                            backgroundColor: torch ? primaryColor : cardColor,
                          },
                        ]}
                        onPress={toggleTorch}
                        activeOpacity={0.7}
                        accessibilityRole='button'
                        accessibilityLabel={
                          torch ? 'Turn off flash' : 'Turn on flash'
                        }
                        accessibilityState={{ selected: torch }}
                      >
                        {torch ? (
                          <Zap size={24} color={cardColor} />
                        ) : (
                          <ZapOff size={24} color={textColor} />
                        )}
                      </TouchableOpacity>
                    )}
                    <TouchableOpacity
                      style={[
                        styles.controlButton,
                        { backgroundColor: cardColor },
                      ]}
                      onPress={toggleCameraFacing}
                      activeOpacity={0.7}
                      accessibilityRole='button'
                      accessibilityLabel='Switch camera'
                    >
                      <SwitchCamera size={24} color={textColor} />
                    </TouchableOpacity>
                    <TouchableOpacity
                      style={[
                        styles.controlButton,
                        {
                          backgroundColor: zoomControls
                            ? primaryColor
                            : cardColor,
                        },
                      ]}
                      onPress={handleZoomButtonTap}
                      activeOpacity={0.7}
                    >
                      <Text
                        style={{
                          fontWeight: '600',
                          color: zoomControls ? cardColor : textColor,
                        }}
                      >
                        {zoomFactorText}
                      </Text>
                    </TouchableOpacity>
                    {enableVideo && (
                      <TouchableOpacity
                        style={[
                          styles.controlButton,
                          { backgroundColor: cardColor },
                        ]}
                        onPress={toggleMode}
                        disabled={isRecording || isCapturing}
                        activeOpacity={0.7}
                        accessibilityRole='button'
                        accessibilityLabel={
                          mode === 'picture'
                            ? 'Switch to video mode'
                            : 'Switch to photo mode'
                        }
                      >
                        {mode === 'picture' ? (
                          <Video size={24} color={textColor} />
                        ) : (
                          <CameraIcon size={24} color={textColor} />
                        )}
                      </TouchableOpacity>
                    )}
                  </View>
                  <View style={styles.bottomControls}>
                    <TouchableOpacity
                      style={[
                        styles.captureButton,
                        {
                          backgroundColor:
                            mode === 'video' && isRecording
                              ? destructiveColor
                              : 'white',
                          borderColor:
                            mode === 'video' && isRecording
                              ? destructiveColor
                              : primaryColor,
                        },
                        (isCapturing || isTimerActive) &&
                          styles.capturingButton,
                      ]}
                      onPress={
                        mode === 'picture'
                          ? handleCapture
                          : isRecording
                            ? handleStopRecording
                            : handleCapture
                      }
                      disabled={isCapturing || isTimerActive}
                      activeOpacity={0.8}
                      accessibilityRole='button'
                      accessibilityLabel={
                        mode === 'video'
                          ? isRecording
                            ? 'Stop recording'
                            : 'Start recording'
                          : 'Take picture'
                      }
                    >
                      {isCapturing ? (
                        <ActivityIndicator size='small' color={primaryColor} />
                      ) : (
                        <View
                          style={[
                            styles.captureInner,
                            {
                              backgroundColor:
                                mode === 'video' && isRecording
                                  ? 'white'
                                  : primaryColor,
                              borderRadius:
                                mode === 'video' && isRecording ? 4 : 30,
                            },
                          ]}
                        />
                      )}
                    </TouchableOpacity>
                  </View>
                </>
              )}
            </AnimatedCameraView>
          </GestureDetector>
        </View>
      </Animated.View>
    );
  }
);

Camera.displayName = 'Camera';

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  cameraContainer: {
    width: screenWidth,
    borderRadius: BORDER_RADIUS,
    overflow: 'hidden',
  },
  camera: {
    flex: 1,
  },
  topControls: {
    position: 'absolute',
    top: 20,
    left: 20,
    right: 20,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    zIndex: 1,
  },
  topLeft: {
    flex: 1,
    alignItems: 'flex-start',
  },
  topCenter: {
    flex: 1,
    alignItems: 'center',
  },
  topRight: {
    flex: 1,
    alignItems: 'flex-end',
  },
  modeText: {
    fontSize: 16,
    fontWeight: 'bold',
    textShadowColor: 'rgba(0, 0, 0, 0.5)',
    textShadowOffset: { width: 1, height: 1 },
    textShadowRadius: 2,
  },
  settingsPanel: {
    position: 'absolute',
    top: 76,
    left: 20,
    right: 20,
    borderRadius: BORDER_RADIUS,
    padding: 16,
    zIndex: 2,
  },
  settingsRow: {
    flexDirection: 'row',
    justifyContent: 'space-around',
    alignItems: 'center',
  },
  settingButton: {
    width: 48,
    height: 48,
    borderRadius: 24,
    justifyContent: 'center',
    alignItems: 'center',
  },
  settingText: {
    fontSize: 12,
    fontWeight: 'bold',
  },
  timerSettingText: {
    fontSize: 10,
    fontWeight: 'bold',
    marginTop: 2,
  },
  sideControls: {
    position: 'absolute',
    right: 20,
    top: '50%',
    transform: [{ translateY: -120 }],
    gap: 16,
    zIndex: 1,
  },
  bottomControls: {
    position: 'absolute',
    bottom: 40,
    left: 20,
    right: 20,
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    zIndex: 1,
  },
  controlButton: {
    width: 48,
    height: 48,
    borderRadius: 24,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
  },
  captureButton: {
    width: 80,
    height: 80,
    borderRadius: 40,
    borderWidth: 4,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'white',
  },
  captureInner: {
    width: 32,
    height: 32,
    borderRadius: 30,
  },
  capturingButton: {
    transform: [{ scale: 0.9 }],
  },
  gridOverlay: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    zIndex: 1,
  },
  gridLines: {
    flex: 1,
    position: 'relative',
  },
  gridLine: {
    position: 'absolute',
    backgroundColor: 'rgba(255, 255, 255, 0.3)',
  },
  verticalLine1: {
    left: '33.33%',
    top: 0,
    bottom: 0,
    width: 1,
  },
  verticalLine2: {
    left: '66.66%',
    top: 0,
    bottom: 0,
    width: 1,
  },
  horizontalLine1: {
    top: '33.33%',
    left: 0,
    right: 0,
    height: 1,
  },
  horizontalLine2: {
    top: '66.66%',
    left: 0,
    right: 0,
    height: 1,
  },
  zoomIndicator: {
    position: 'absolute',
    top: '45%',
    alignSelf: 'center',
    backgroundColor: 'rgba(0, 0, 0, 0.7)',
    paddingHorizontal: 16,
    paddingVertical: 8,
    borderRadius: 20,
    zIndex: 2,
  },
  zoomText: {
    color: 'white',
    fontSize: 16,
    fontWeight: 'bold',
    textAlign: 'center',
  },
  timerOverlay: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    justifyContent: 'center',
    alignItems: 'center',
    zIndex: 3,
  },
  timerText: {
    fontSize: 72,
    fontWeight: 'bold',
    color: 'white',
    textAlign: 'center',
  },
  cancelTimerButton: {
    position: 'absolute',
    top: 60,
    right: 20,
    width: 48,
    height: 48,
    borderRadius: 24,
    backgroundColor: 'rgba(0, 0, 0, 0.7)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  tapToCancelText: {
    position: 'absolute',
    bottom: 100,
    color: 'white',
    fontSize: 16,
    textAlign: 'center',
  },
  recordingIndicator: {
    position: 'absolute',
    top: 20,
    left: 20,
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: 'rgba(255, 0, 0, 0.8)',
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 16,
    zIndex: 2,
  },
  recordingDot: {
    width: 8,
    height: 8,
    borderRadius: 4,
    backgroundColor: 'white',
    marginRight: 8,
  },
  recordingText: {
    color: 'white',
    fontSize: 14,
    fontWeight: 'bold',
  },
  permissionContainer: {
    flex: 1,
    gap: 16,
    padding: 32,
    borderRadius: BORDER_RADIUS,
    justifyContent: 'center',
    alignItems: 'center',
  },
  permissionIcon: {
    marginBottom: 16,
  },
  loadingText: {
    marginTop: 16,
    fontSize: FONT_SIZE,
  },
  zoomControls: {
    position: 'absolute',
    right: 20,
    top: '25%',
    padding: 12,
    borderRadius: 12,
    justifyContent: 'center',
    alignItems: 'center',
    zIndex: 100,
  },
  sliderContainer: {
    height: 200,
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingVertical: 10,
    transform: [{ rotate: '-90deg' }],
  },
  zoomSlider: {
    width: 160,
    borderRadius: 999,
  },
  zoomValue: {
    fontSize: 14,
    fontWeight: 'bold',
  },
  currentZoomText: {
    marginTop: 12,
    fontSize: 12,
    fontWeight: '600',
  },
});

export Camera;
```

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

**4.** Configure permissions in your app.json or expo.json:

```json
{
  "expo": {
    "plugins": [
      [
        "expo-camera",
        {
          "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera",
          "microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone",
          "recordAudioAndroid": true
        }
      ]
    ]
  }
}
```

## Usage

```tsx
import { Camera } from '@/components/ui/camera';
```

```tsx
<Camera
  onCapture={({ uri, type }) => {
    console.log('Captured:', uri, type);
  }}
  onVideoCapture={({ uri, type }) => {
    console.log('Video captured:', uri, type);
  }}
  onClose={() => {
    // Handle camera close
  }}
/>
```

## Examples

#### Default

**Example:** A basic camera with default settings

```tsx
// components/demo/camera/camera-demo.tsx
import { Camera } from '@/components/ui/camera';
import React from 'react';
import { Alert } from 'react-native';

export function CameraDemo() {
  const handleCapture = ({ uri, type }: { uri: string; type: string }) => {
    Alert.alert('Picture Captured', `Saved to: ${uri}`);
  };

  const handleVideoCapture = ({ uri, type }: { uri: string; type: string }) => {
    Alert.alert('Video Recorded', `Saved to: ${uri}`);
  };

  return (
    <Camera
      onCapture={handleCapture}
      onVideoCapture={handleVideoCapture}
      style={{ height: 400 }}
    />
  );
}
```

#### With Custom Controls

**Example:** Camera with custom control settings

```tsx
// components/demo/camera/camera-custom-controls.tsx
import { Camera } from '@/components/ui/camera';
import React from 'react';
import { Alert } from 'react-native';

export function CameraCustomControls() {
  const handleCapture = ({ uri, type }: { uri: string; type: string }) => {
    Alert.alert('Capture Complete', `${type} saved successfully`);
  };

  const handleVideoCapture = ({ uri, type }: { uri: string; type: string }) => {
    Alert.alert('Recording Complete', `Video saved successfully`);
  };

  return (
    <Camera
      facing='front'
      enableTorch={false}
      timerOptions={[0, 5, 15]}
      maxVideoDuration={30}
      onCapture={handleCapture}
      onVideoCapture={handleVideoCapture}
      style={{ height: 400, borderRadius: 12 }}
    />
  );
}
```

#### Picture Only Mode

**Example:** Camera configured for picture-only mode

```tsx
// components/demo/camera/camera-picture-only.tsx
import { Camera } from '@/components/ui/camera';
import React from 'react';
import { Alert } from 'react-native';

export function CameraPictureOnly() {
  const handleCapture = ({ uri, type }: { uri: string; type: string }) => {
    Alert.alert('Photo Captured', 'Picture saved to gallery');
  };

  return (
    <Camera
      enableVideo={false}
      onCapture={handleCapture}
      style={{ height: 400 }}
    />
  );
}
```

#### Video Recording

**Example:** Camera with video recording capabilities

```tsx
// components/demo/camera/camera-video.tsx
import { Camera } from '@/components/ui/camera';
import React from 'react';
import { Alert } from 'react-native';

export function CameraVideo() {
  const handleCapture = ({ uri, type }: { uri: string; type: string }) => {
    Alert.alert(
      'Picture Taken',
      `Saved: ${uri.substring(uri.lastIndexOf('/') + 1)}`
    );
  };

  const handleVideoCapture = ({ uri, type }: { uri: string; type: string }) => {
    Alert.alert('Video Recorded', `Duration: ${uri ? 'Success' : 'Failed'}`);
  };

  return (
    <Camera
      maxVideoDuration={120}
      onCapture={handleCapture}
      onVideoCapture={handleVideoCapture}
      style={{ height: 400 }}
    />
  );
}
```

#### Timer Features

**Example:** Camera with timer functionality

```tsx
// components/demo/camera/camera-timer.tsx
import { Camera } from '@/components/ui/camera';
import React from 'react';
import { Alert } from 'react-native';

export function CameraTimer() {
  const handleCapture = ({ uri, type }: { uri: string; type: string }) => {
    Alert.alert('Timer Capture', 'Photo captured after countdown!');
  };

  const handleVideoCapture = ({ uri, type }: { uri: string; type: string }) => {
    Alert.alert('Timer Recording', 'Video started after countdown!');
  };

  return (
    <Camera
      timerOptions={[0, 3, 5, 10, 15]}
      onCapture={handleCapture}
      onVideoCapture={handleVideoCapture}
      style={{ height: 400 }}
    />
  );
}
```

#### Zoom Controls

**Example:** Camera with zoom controls and gestures

```tsx
// components/demo/camera/camera-zoom.tsx
import { Camera } from '@/components/ui/camera';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React from 'react';
import { Alert } from 'react-native';

export function CameraZoom() {
  const handleCapture = ({ uri, type }: { uri: string; type: string }) => {
    Alert.alert('Zoomed Capture', 'Photo taken with current zoom level');
  };

  return (
    <View style={{ gap: 8 }}>
      <Text variant='body' style={{ textAlign: 'center', opacity: 0.7 }}>
        Pinch to zoom • Double tap for quick zoom • Tap zoom button to cycle
        levels
      </Text>
      <Camera onCapture={handleCapture} style={{ height: 400 }} />
    </View>
  );
}
```

#### Settings Panel

**Example:** Camera with advanced settings panel

```tsx
// components/demo/camera/camera-settings.tsx
import { Camera } from '@/components/ui/camera';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React from 'react';
import { Alert } from 'react-native';

export function CameraSettings() {
  const handleCapture = ({ uri, type }: { uri: string; type: string }) => {
    Alert.alert('Settings Demo', 'Captured with current settings applied');
  };

  const handleVideoCapture = ({ uri, type }: { uri: string; type: string }) => {
    Alert.alert('Settings Demo', 'Recorded with current settings applied');
  };

  return (
    <View style={{ gap: 8 }}>
      <Text variant='body' style={{ textAlign: 'center', opacity: 0.7 }}>
        Tap the settings icon to access grid, sound, aspect ratio, and timer
        controls
      </Text>
      <Camera
        onCapture={handleCapture}
        onVideoCapture={handleVideoCapture}
        style={{ height: 400 }}
      />
    </View>
  );
}
```

## API Reference

### Camera

The main camera component with comprehensive controls and features.

| Prop               | Type                                    | Default      | Description                                         |
| ------------------ | --------------------------------------- | ------------ | --------------------------------------------------- |
| `style`            | `ViewStyle`                             | -            | Additional styles to apply to the camera container. |
| `facing`           | `'front' \| 'back'`                     | `'back'`     | The initial camera facing direction.                |
| `enableTorch`      | `boolean`                               | `true`       | Whether to show the torch/flash control.            |
| `showControls`     | `boolean`                               | `true`       | Whether to show the camera controls overlay.        |
| `timerOptions`     | `number[]`                              | `[0, 3, 10]` | Available timer options in seconds.                 |
| `enableVideo`      | `boolean`                               | `true`       | Whether to enable video recording mode.             |
| `maxVideoDuration` | `number`                                | `60`         | Maximum video duration in seconds.                  |
| `onClose`          | `() => void`                            | -            | Callback when the close button is pressed.          |
| `onCapture`        | `({ type, uri, cameraHeight }) => void` | -            | Callback when a picture is captured.                |
| `onVideoCapture`   | `({ type, uri, cameraHeight }) => void` | -            | Callback when a video is captured.                  |

### CameraRef

The camera component exposes these methods via ref:

| Method           | Type                  | Description                           |
| ---------------- | --------------------- | ------------------------------------- |
| `switchCamera`   | `() => void`          | Switch between front and back camera. |
| `toggleTorch`    | `() => void`          | Toggle the torch/flash on and off.    |
| `takePicture`    | `() => Promise<void>` | Programmatically take a picture.      |
| `startRecording` | `() => Promise<void>` | Start video recording.                |
| `stopRecording`  | `() => Promise<void>` | Stop video recording.                 |

### CaptureSuccess

The callback data structure for successful captures:

| Property       | Type                   | Description                                  |
| -------------- | ---------------------- | -------------------------------------------- |
| `type`         | `'picture' \| 'video'` | The type of media captured.                  |
| `uri`          | `string`               | The local URI of the captured media.         |
| `cameraHeight` | `number`               | The height of the camera view when captured. |

## Features

### Camera Controls

- **Capture Button**: Large, prominent button for taking pictures or starting/stopping video recording
- **Mode Toggle**: Switch between picture and video modes
- **Camera Flip**: Switch between front and back cameras
- **Torch/Flash**: Toggle flashlight for back camera
- **Zoom**: Pinch-to-zoom gestures and tap-to-zoom controls

### Advanced Features

- **Timer**: Set delays of 0, 3, or 10 seconds before capture
- **Grid Lines**: Rule of thirds overlay for better composition
- **Aspect Ratios**: Support for 16:9, 4:3, and 1:1 ratios
- **Sound Control**: Enable/disable camera sounds
- **Settings Panel**: Collapsible panel with advanced options

### Gestures

- **Pinch to Zoom**: Smooth zoom in/out with gesture controls
- **Double Tap**: Quick zoom toggle between 1x and 2.5x
- **Tap Controls**: Tap zoom button to cycle through zoom levels

### Video Recording

- **Recording Timer**: Shows elapsed recording time
- **Duration Limit**: Configurable maximum recording duration
- **Visual Feedback**: Recording indicator with red dot animation

## Permissions

The Camera component requires camera and microphone permissions. The component will:

1. Check for existing permissions
2. Show a permission request screen if not granted
3. Provide a clear call-to-action to grant permissions
4. Handle permission denial gracefully

## Accessibility

The Camera component includes accessibility features:

- **Screen Reader Support**: All controls have appropriate labels
- **High Contrast**: Clear visual distinction between active/inactive states
- **Touch Targets**: All interactive elements meet minimum size requirements
- **Keyboard Navigation**: Focus management for external keyboard users
- **Reduced Motion**: Respects system animation preferences

## Performance

- **Optimized Rendering**: Minimal re-renders using React.memo and useCallback
- **Gesture Handling**: Efficient native gesture recognition
- **Memory Management**: Proper cleanup of timers and resources
- **Battery Optimization**: Automatic torch disable when switching cameras

## Error Handling

The component handles various error scenarios:

- **Permission Denied**: Shows clear permission request screen
- **Camera Unavailable**: Graceful fallback with error messages
- **Recording Failures**: User-friendly error alerts
- **Memory Issues**: Automatic cleanup and error recovery

## Customization

The Camera component can be customized through:

- **Theme Integration**: Uses theme colors and design tokens
- **Custom Styles**: Style prop for container customization
- **Control Visibility**: Toggle individual control elements
- **Timer Options**: Configure available timer durations
- **Aspect Ratios**: Support for multiple aspect ratios
