# MediaPicker

> A versatile component for selecting images and videos from device gallery or camera with preview 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/media-picker
- Markdown: https://ui.ahmedbna.com/docs/components/media-picker.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/media-picker.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/media-picker.json
- Install: `npx bna-ui add media-picker`
- npm dependencies: `expo-haptics`, `expo-image`, `expo-image-picker`, `expo-media-library`, `lucide-react-native`, `react-native-reanimated`, `react-native-svg`, `react-native-worklets`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `globals`, `useHaptics`, `text`, `view`, `icon`, `spinner`, `button`
- Preview recording: https://demo.ahmedbna.com/0190-media-picker-demo.MP4

---

**Example:** A basic media picker with image and video selection

```tsx
// components/demo/media-picker/media-picker-demo.tsx
import { MediaPicker } from '@/components/ui/media-picker';
import React from 'react';

export function MediaPickerDemo() {
  return (
    <MediaPicker
      mediaType='all'
      onSelectionChange={(assets) => {
        console.log('Selected assets:', assets);
      }}
      onError={(error) => {
        console.error('Media picker error:', error);
      }}
    />
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add media-picker
```

### Manual

**1.** Install the following dependencies:

```bash
npx expo install expo-image expo-image-picker expo-media-library lucide-react-native
```

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

```tsx
// components/ui/media-picker.tsx
import { Button, ButtonSize, ButtonVariant } from '@/components/ui/button';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useColor } from '@/hooks/useColor';
import { CORNERS, FONT_SIZE } from '@/theme/globals';
import { Image as ExpoImage } from 'expo-image';
import * as ImagePicker from 'expo-image-picker';
import * as MediaLibrary from 'expo-media-library';
import { LucideProps, Video, X } from 'lucide-react-native';
import React, { forwardRef, useEffect, useRef, useState } from 'react';
import {
  Dimensions,
  FlatList,
  Linking,
  Modal,
  Pressable,
  View as RNView,
  StyleSheet,
  TouchableOpacity,
  ViewStyle,
} from 'react-native';

export type MediaType = 'image' | 'video' | 'all';
export type MediaQuality = 'low' | 'medium' | 'high';

export interface MediaAsset {
  id: string;
  uri: string;
  type: 'image' | 'video';
  width?: number;
  height?: number;
  duration?: number;
  filename?: string;
  fileSize?: number;
}

export interface MediaPickerProps {
  children?: React.ReactNode;
  style?: ViewStyle;
  size?: ButtonSize;
  variant?: ButtonVariant;
  icon?: React.ComponentType<LucideProps>;
  disabled?: boolean;
  mediaType?: MediaType;
  multiple?: boolean;
  maxSelection?: number;
  quality?: MediaQuality;
  buttonText?: string;
  placeholder?: string;
  gallery?: boolean;
  showPreview?: boolean;
  previewSize?: number;
  selectedAssets?: MediaAsset[];
  onSelectionChange?: (assets: MediaAsset[]) => void;
  onError?: (error: string) => void;
}

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

// Helper function to compare arrays of MediaAssets
const arraysEqual = (a: MediaAsset[], b: MediaAsset[]): boolean => {
  if (a.length !== b.length) return false;
  return a.every((item, index) => {
    const bItem = b[index];
    return (
      item.id === bItem.id && item.uri === bItem.uri && item.type === bItem.type
    );
  });
};

export const MediaPicker = forwardRef<RNView, MediaPickerProps>(
  (
    {
      children,
      mediaType = 'all',
      multiple = false,
      gallery = false,
      maxSelection = 10,
      quality = 'high',
      onSelectionChange,
      onError,
      buttonText,
      showPreview = true,
      previewSize = 80,
      style,
      variant,
      size,
      icon,
      disabled = false,
      selectedAssets = [],
    },
    ref
  ) => {
    const [assets, setAssets] = useState<MediaAsset[]>(selectedAssets);
    const [isGalleryVisible, setIsGalleryVisible] = useState(false);
    // SDK 56 replaced the eagerly-populated `Asset` object with a lazy handle
    // whose fields are async getters. `AssetInfo` is the resolved shape, so the
    // gallery resolves once on load and the render path stays synchronous.
    const [galleryAssets, setGalleryAssets] = useState<
      MediaLibrary.AssetInfo[]
    >([]);
    const [hasPermission, setHasPermission] = useState<boolean | null>(null);
    const [canAskAgain, setCanAskAgain] = useState(true);

    // Use ref to track previous selectedAssets to avoid unnecessary updates
    const prevSelectedAssetsRef = useRef<MediaAsset[]>(selectedAssets);

    // Theme colors
    const cardColor = useColor('card');
    const borderColor = useColor('border');
    const textColor = useColor('text');
    const mutedColor = useColor('mutedForeground');
    const primaryColor = useColor('primary');
    const secondary = useColor('secondary');

    // Update internal state when selectedAssets prop changes (with proper comparison)
    useEffect(() => {
      // Only update if the arrays are actually different
      if (!arraysEqual(prevSelectedAssetsRef.current, selectedAssets)) {
        setAssets(selectedAssets);
        prevSelectedAssetsRef.current = selectedAssets;
      }
    }, [selectedAssets]);

    // Requested lazily, from the picker button press, rather than eagerly on
    // mount — avoids surfacing the OS permission prompt before the user has
    // expressed any intent to pick media.
    const requestPermissions = async (): Promise<{
      granted: boolean;
      canAskAgain: boolean;
    }> => {
      try {
        const { status, canAskAgain: canAsk } =
          await MediaLibrary.requestPermissionsAsync();
        const granted = status === 'granted';
        setHasPermission(granted);
        setCanAskAgain(canAsk);

        if (!granted) {
          onError?.(
            canAsk
              ? 'Media library permission is required to access photos and videos'
              : 'Media library permission was denied. Enable it in Settings to continue.'
          );
        }

        return { granted, canAskAgain: canAsk };
      } catch (error) {
        onError?.('Failed to request permissions');
        setHasPermission(false);
        return { granted: false, canAskAgain: true };
      }
    };

    const loadGalleryAssets = async () => {
      if (!hasPermission) return;

      try {
        const query = new MediaLibrary.Query();

        if (mediaType === 'image') {
          query.eq(
            MediaLibrary.AssetField.MEDIA_TYPE,
            MediaLibrary.MediaType.IMAGE
          );
        } else if (mediaType === 'video') {
          query.eq(
            MediaLibrary.AssetField.MEDIA_TYPE,
            MediaLibrary.MediaType.VIDEO
          );
        } else {
          query.within(MediaLibrary.AssetField.MEDIA_TYPE, [
            MediaLibrary.MediaType.IMAGE,
            MediaLibrary.MediaType.VIDEO,
          ]);
        }

        const found = await query
          .orderBy({
            key: MediaLibrary.AssetField.CREATION_TIME,
            ascending: false,
          })
          .limit(100)
          .exe();

        setGalleryAssets(await Promise.all(found.map((a) => a.getInfo())));
      } catch (error) {
        onError?.('Failed to load gallery assets');
      }
    };

    const pickFromGallery = async () => {
      if (!hasPermission) {
        if (hasPermission === false && !canAskAgain) {
          Linking.openSettings();
          return;
        }

        const { granted, canAskAgain: canAsk } = await requestPermissions();
        if (!granted) {
          if (!canAsk) {
            Linking.openSettings();
          }
          return;
        }
      }

      if (gallery) {
        await loadGalleryAssets();
        setIsGalleryVisible(true);
        return;
      }

      try {
        const result = await ImagePicker.launchImageLibraryAsync({
          mediaTypes:
            mediaType === 'image'
              ? ['images']
              : mediaType === 'video'
                ? ['videos']
                : ['images', 'videos'],
          allowsMultipleSelection: multiple,
          quality: quality === 'high' ? 1 : quality === 'medium' ? 0.7 : 0.3,
          selectionLimit: multiple ? maxSelection : 1,
        });

        if (!result.canceled && result.assets) {
          const newAssets = result.assets.map((asset, index) => ({
            id: `gallery_${Date.now()}_${index}`,
            uri: asset.uri,
            type:
              asset.type === 'video' ? ('video' as const) : ('image' as const),
            width: asset.width,
            height: asset.height,
            duration: asset.duration || undefined,
            filename: asset.fileName || undefined,
            fileSize: asset.fileSize,
          }));

          handleAssetSelection(newAssets);
        }
      } catch (error) {
        onError?.('Failed to pick media from gallery');
      }
    };

    const handleAssetSelection = (newAssets: MediaAsset[]) => {
      let updatedAssets: MediaAsset[];

      if (multiple) {
        updatedAssets = [...assets, ...newAssets].slice(0, maxSelection);
      } else {
        updatedAssets = newAssets;
      }

      setAssets(updatedAssets);
      prevSelectedAssetsRef.current = updatedAssets; // Update ref to prevent loop
      onSelectionChange?.(updatedAssets);
    };

    const handleGalleryAssetSelect = async (
      galleryAsset: MediaLibrary.AssetInfo
    ) => {
      try {
        const newAsset: MediaAsset = {
          id: galleryAsset.id,
          uri: galleryAsset.uri,
          type:
            galleryAsset.mediaType === MediaLibrary.MediaType.VIDEO
              ? 'video'
              : 'image',
          width: galleryAsset.width,
          height: galleryAsset.height,
          duration: galleryAsset.duration || undefined,
          filename: galleryAsset.filename,
        };

        if (multiple) {
          const isAlreadySelected = assets.some(
            (asset) => asset.id === newAsset.id
          );
          if (isAlreadySelected) {
            const filteredAssets = assets.filter(
              (asset) => asset.id !== newAsset.id
            );
            setAssets(filteredAssets);
            prevSelectedAssetsRef.current = filteredAssets; // Update ref
            onSelectionChange?.(filteredAssets);
          } else if (assets.length < maxSelection) {
            const updatedAssets = [...assets, newAsset];
            setAssets(updatedAssets);
            prevSelectedAssetsRef.current = updatedAssets; // Update ref
            onSelectionChange?.(updatedAssets);
          }
        } else {
          const newAssets = [newAsset];
          setAssets(newAssets);
          prevSelectedAssetsRef.current = newAssets; // Update ref
          onSelectionChange?.(newAssets);
          setIsGalleryVisible(false);
        }
      } catch (error) {
        onError?.('Failed to select asset');
      }
    };

    const removeAsset = (assetId: string) => {
      const filteredAssets = assets.filter((asset) => asset.id !== assetId);
      setAssets(filteredAssets);
      prevSelectedAssetsRef.current = filteredAssets; // Update ref
      onSelectionChange?.(filteredAssets);
    };

    const renderPreviewItem = ({ item }: { item: MediaAsset }) => (
      <View style={[styles.previewItem, { borderColor }]}>
        <ExpoImage
          source={{ uri: item.uri }}
          style={[
            styles.previewImage,
            { width: previewSize, height: previewSize },
          ]}
          contentFit='cover'
        />
        {item.type === 'video' && (
          <View style={styles.videoIndicator}>
            <Video size={16} color='white' />
          </View>
        )}
        <TouchableOpacity
          style={[styles.removeButton, { backgroundColor: primaryColor }]}
          onPress={() => removeAsset(item.id)}
        >
          <X size={12} color={secondary} />
        </TouchableOpacity>
      </View>
    );

    const renderGalleryItem = ({ item }: { item: MediaLibrary.AssetInfo }) => {
      const isSelected = assets.some((asset) => asset.id === item.id);
      const itemWidth = screenWidth / 3 - 4;

      return (
        <Pressable
          style={[
            styles.galleryItem,
            { width: itemWidth, height: itemWidth },
            isSelected && { borderColor: primaryColor, borderWidth: 3 },
          ]}
          onPress={() => handleGalleryAssetSelect(item)}
        >
          <ExpoImage
            source={{ uri: item.uri }}
            style={styles.galleryImage}
            contentFit='cover'
          />
          {item.mediaType === MediaLibrary.MediaType.VIDEO && (
            <View style={styles.videoIndicator}>
              <Video size={20} color='white' />
            </View>
          )}
          {multiple && isSelected && (
            <View
              style={[
                styles.selectedIndicator,
                { backgroundColor: primaryColor },
              ]}
            >
              <Text
                style={{
                  color: secondary,
                  fontSize: 12,
                  fontWeight: 'bold',
                }}
              >
                {assets.findIndex((asset) => asset.id === item.id) + 1}
              </Text>
            </View>
          )}
        </Pressable>
      );
    };

    return (
      <View ref={ref} style={style}>
        {children ? (
          children
        ) : (
          <Button
            onPress={pickFromGallery}
            disabled={disabled}
            variant={variant}
            size={size}
            icon={icon}
          >
            {buttonText ||
              `Select ${
                mediaType === 'all'
                  ? 'Media'
                  : mediaType === 'image'
                    ? 'Images'
                    : 'Videos'
              }`}
          </Button>
        )}

        {showPreview && assets.length > 0 && (
          <FlatList
            data={assets}
            renderItem={renderPreviewItem}
            keyExtractor={(item) => item.id}
            horizontal
            showsHorizontalScrollIndicator={false}
            style={styles.previewContainer}
            contentContainerStyle={styles.previewContent}
          />
        )}

        {gallery && (
          <Modal
            visible={isGalleryVisible}
            animationType='slide'
            presentationStyle='pageSheet'
          >
            <View
              style={[styles.modalContainer, { backgroundColor: cardColor }]}
            >
              <View
                style={[styles.modalHeader, { borderBottomColor: borderColor }]}
              >
                <Text variant='title'>
                  {buttonText ||
                    `Select ${
                      mediaType === 'all'
                        ? 'Media'
                        : mediaType === 'image'
                          ? 'Images'
                          : 'Videos'
                    }`}
                </Text>
                <View style={styles.modalActions}>
                  {multiple && (
                    <Text
                      style={[styles.selectionCount, { color: mutedColor }]}
                    >
                      {assets.length}/{maxSelection}
                    </Text>
                  )}

                  <Button
                    size='sm'
                    variant='success'
                    onPress={() => setIsGalleryVisible(false)}
                  >
                    Done
                  </Button>
                </View>
              </View>

              <FlatList
                data={galleryAssets}
                renderItem={renderGalleryItem}
                keyExtractor={(item) => item.id}
                numColumns={3}
                contentContainerStyle={styles.galleryContent}
              />
            </View>
          </Modal>
        )}
      </View>
    );
  }
);

const styles = StyleSheet.create({
  compactButton: {
    width: 60,
    height: 60,
    borderRadius: CORNERS,
    borderWidth: 1,
    borderStyle: 'dashed',
    alignItems: 'center',
    justifyContent: 'center',
  },

  disabled: {
    opacity: 0.5,
  },

  previewContainer: {
    marginTop: 12,
  },

  previewContent: {
    paddingHorizontal: 4,
  },

  previewItem: {
    marginHorizontal: 4,
    borderRadius: 8,
    borderWidth: 1,
    overflow: 'hidden',
    position: 'relative',
  },

  previewImage: {
    borderRadius: 8,
  },

  videoIndicator: {
    position: 'absolute',
    top: 8,
    left: 8,
    backgroundColor: 'rgba(0, 0, 0, 0.6)',
    borderRadius: 12,
    padding: 4,
  },

  removeButton: {
    position: 'absolute',
    top: 6,
    right: 6,
    width: 20,
    height: 20,
    borderRadius: 10,
    alignItems: 'center',
    justifyContent: 'center',
  },

  modalContainer: {
    flex: 1,
  },

  modalHeader: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 16,
    borderBottomWidth: StyleSheet.hairlineWidth,
  },

  modalActions: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 16,
  },

  selectionCount: {
    fontSize: FONT_SIZE,
    fontWeight: '500',
  },

  closeButton: {
    padding: 4,
  },

  galleryContent: {
    padding: 2,
  },

  galleryItem: {
    margin: 1,
    borderRadius: 4,
    overflow: 'hidden',
    position: 'relative',
  },

  galleryImage: {
    width: '100%',
    height: '100%',
  },

  selectedIndicator: {
    position: 'absolute',
    top: 8,
    right: 8,
    width: 24,
    height: 24,
    borderRadius: 12,
    alignItems: 'center',
    justifyContent: 'center',
  },
});

MediaPicker.displayName = 'MediaPicker';
```

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

## Usage

```tsx
import { MediaPicker } from '@/components/ui/media-picker';
```

```tsx
<MediaPicker
  mediaType='all'
  multiple={true}
  maxSelection={5}
  onSelectionChange={(assets) => console.log(assets)}
/>
```

## Examples

#### Default

**Example:** A basic media picker with image and video selection

```tsx
// components/demo/media-picker/media-picker-demo.tsx
import { MediaPicker } from '@/components/ui/media-picker';
import React from 'react';

export function MediaPickerDemo() {
  return (
    <MediaPicker
      mediaType='all'
      onSelectionChange={(assets) => {
        console.log('Selected assets:', assets);
      }}
      onError={(error) => {
        console.error('Media picker error:', error);
      }}
    />
  );
}
```

#### Image Only

**Example:** Media picker configured for images only

```tsx
// components/demo/media-picker/media-picker-images.tsx
import { MediaPicker } from '@/components/ui/media-picker';
import { Image } from 'lucide-react-native';
import React from 'react';

export function MediaPickerImages() {
  return (
    <MediaPicker
      mediaType='image'
      buttonText='Select Images'
      icon={Image}
      variant='outline'
      onSelectionChange={(assets) => {
        console.log('Selected images:', assets);
      }}
    />
  );
}
```

#### Video Only

**Example:** Media picker configured for videos only

```tsx
// components/demo/media-picker/media-picker-videos.tsx
import { MediaPicker } from '@/components/ui/media-picker';
import { Video } from 'lucide-react-native';
import React from 'react';

export function MediaPickerVideos() {
  return (
    <MediaPicker
      mediaType='video'
      buttonText='Select Videos'
      icon={Video}
      variant='secondary'
      onSelectionChange={(assets) => {
        console.log('Selected videos:', assets);
      }}
    />
  );
}
```

#### Multiple Selection

**Example:** Media picker with multiple selection enabled

```tsx
// components/demo/media-picker/media-picker-multiple.tsx
import { MediaPicker } from '@/components/ui/media-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { Plus } from 'lucide-react-native';
import React, { useState } from 'react';

export function MediaPickerMultiple() {
  const [selectedCount, setSelectedCount] = useState(0);

  return (
    <View style={{ gap: 12 }}>
      <MediaPicker
        mediaType='all'
        multiple={true}
        maxSelection={5}
        buttonText={`Select Media (${selectedCount}/5)`}
        icon={Plus}
        onSelectionChange={(assets) => {
          setSelectedCount(assets.length);
          console.log('Selected assets:', assets);
        }}
      />

      {selectedCount > 0 && (
        <Text variant='caption'>
          {selectedCount} item{selectedCount !== 1 ? 's' : ''} selected
        </Text>
      )}
    </View>
  );
}
```

#### Custom Gallery

**Example:** Media picker with custom gallery modal

```tsx
// components/demo/media-picker/media-picker-gallery.tsx
import { MediaAsset, MediaPicker } from '@/components/ui/media-picker';
import { Folder } from 'lucide-react-native';
import React, { useState } from 'react';

export function MediaPickerGallery() {
  const [selected, setSelected] = useState<MediaAsset[]>([]);

  return (
    <MediaPicker
      mediaType='all'
      gallery={true}
      multiple={true}
      maxSelection={8}
      buttonText='Open Gallery'
      icon={Folder}
      variant='outline'
      selectedAssets={selected}
      onSelectionChange={setSelected}
    />
  );
}
```

#### With Preview

**Example:** Media picker showing selected media previews

```tsx
// components/demo/media-picker/media-picker-preview.tsx
import { MediaAsset, MediaPicker } from '@/components/ui/media-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { ImageIcon } from 'lucide-react-native';
import React, { useState } from 'react';

export function MediaPickerPreview() {
  const [assets, setAssets] = useState<MediaAsset[]>([]);

  return (
    <View style={{ gap: 16 }}>
      <MediaPicker
        mediaType='all'
        multiple={true}
        maxSelection={6}
        showPreview={true}
        previewSize={100}
        buttonText='Add Media'
        icon={ImageIcon}
        selectedAssets={assets}
        onSelectionChange={(newAssets) => {
          setAssets(newAssets);
          console.log('Assets with preview:', newAssets);
        }}
      />

      {assets.length > 0 && (
        <View>
          <Text variant='caption'>
            {assets.length} item{assets.length !== 1 ? 's' : ''} selected
          </Text>
          <Text variant='caption'>
            Types: {assets.map((a) => a.type).join(', ')}
          </Text>
        </View>
      )}
    </View>
  );
}
```

#### Quality Settings

**Example:** Media picker with different quality settings

```tsx
// components/demo/media-picker/media-picker-quality.tsx
import { MediaPicker } from '@/components/ui/media-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { Settings } from 'lucide-react-native';
import React from 'react';

export function MediaPickerQuality() {
  return (
    <View style={{ gap: 16 }}>
      <View>
        <Text variant='title' style={{ marginBottom: 8 }}>
          High Quality
        </Text>
        <MediaPicker
          mediaType='image'
          quality='high'
          buttonText='High Quality Images'
          icon={Settings}
          variant='outline'
          size='sm'
          onSelectionChange={(assets) => {
            console.log('High quality assets:', assets);
          }}
        />
      </View>

      <View>
        <Text variant='title' style={{ marginBottom: 8 }}>
          Medium Quality
        </Text>
        <MediaPicker
          mediaType='image'
          quality='medium'
          buttonText='Medium Quality Images'
          icon={Settings}
          variant='secondary'
          size='sm'
          onSelectionChange={(assets) => {
            console.log('Medium quality assets:', assets);
          }}
        />
      </View>

      <View>
        <Text variant='title' style={{ marginBottom: 8 }}>
          Low Quality
        </Text>
        <MediaPicker
          mediaType='image'
          quality='low'
          buttonText='Low Quality Images'
          icon={Settings}
          variant='ghost'
          size='sm'
          onSelectionChange={(assets) => {
            console.log('Low quality assets:', assets);
          }}
        />
      </View>
    </View>
  );
}
```

## API Reference

### MediaPicker

The main component for selecting media from device gallery or camera.

| Prop                | Type                             | Default  | Description                                                   |
| ------------------- | -------------------------------- | -------- | ------------------------------------------------------------- |
| `children`          | `ReactNode`                      | -        | Custom trigger element. If not provided, uses default button. |
| `style`             | `ViewStyle`                      | -        | Additional styles to apply to the container.                  |
| `size`              | `ButtonSize`                     | -        | Size of the default button trigger.                           |
| `variant`           | `ButtonVariant`                  | -        | Variant of the default button trigger.                        |
| `icon`              | `ComponentType<LucideProps>`     | -        | Icon for the default button trigger.                          |
| `disabled`          | `boolean`                        | `false`  | Whether the media picker is disabled.                         |
| `mediaType`         | `'image' \| 'video' \| 'all'`    | `'all'`  | Type of media to allow selection.                             |
| `multiple`          | `boolean`                        | `false`  | Whether to allow multiple selection.                          |
| `maxSelection`      | `number`                         | `10`     | Maximum number of items that can be selected.                 |
| `quality`           | `'low' \| 'medium' \| 'high'`    | `'high'` | Quality of selected media.                                    |
| `buttonText`        | `string`                         | -        | Text for the default button trigger.                          |
| `placeholder`       | `string`                         | -        | Placeholder text (currently unused).                          |
| `gallery`           | `boolean`                        | `false`  | Whether to show custom gallery modal.                         |
| `showPreview`       | `boolean`                        | `true`   | Whether to show preview of selected media.                    |
| `previewSize`       | `number`                         | `80`     | Size of preview thumbnails in pixels.                         |
| `selectedAssets`    | `MediaAsset[]`                   | `[]`     | Controlled selected assets.                                   |
| `onSelectionChange` | `(assets: MediaAsset[]) => void` | -        | Callback when selection changes.                              |
| `onError`           | `(error: string) => void`        | -        | Callback when an error occurs.                                |

### MediaAsset

The interface for media assets returned by the picker.

| Prop       | Type                 | Description                      |
| ---------- | -------------------- | -------------------------------- |
| `id`       | `string`             | Unique identifier for the asset. |
| `uri`      | `string`             | Local URI of the selected media. |
| `type`     | `'image' \| 'video'` | Type of the media asset.         |
| `width`    | `number?`            | Width of the media in pixels.    |
| `height`   | `number?`            | Height of the media in pixels.   |
| `duration` | `number?`            | Duration in seconds for videos.  |
| `filename` | `string?`            | Original filename of the media.  |
| `fileSize` | `number?`            | File size in bytes.              |

## Permissions

The MediaPicker component requires the following permissions:

- **iOS**: `NSPhotoLibraryUsageDescription` in Info.plist
- **Android**: `READ_EXTERNAL_STORAGE` permission

Permission is requested when the picker button is first pressed, not on
mount. If the user has permanently denied access, pressing the button again
opens the device Settings app instead of re-prompting.

Add the following plugins to your `app.json` so Expo generates the required
native permission entries:

```json
{
  "expo": {
    "plugins": ["expo-image-picker", "expo-media-library"]
  }
}
```

## Features

- **Multiple Media Types**: Support for images, videos, or both
- **Gallery Integration**: Custom gallery modal or system picker
- **Preview Support**: Show thumbnails of selected media
- **Quality Control**: Adjustable media quality settings
- **Batch Selection**: Select multiple items with configurable limits
- **Error Handling**: Comprehensive error handling and callbacks
- **Accessibility**: Built-in accessibility features
- **Theme Integration**: Respects your app's theme colors

## Accessibility

The MediaPicker component is built with accessibility in mind:

- Proper labeling for screen readers
- Keyboard navigation support
- High contrast support for buttons and indicators
- Semantic structure for better navigation
- Error announcements for screen readers

## Notes

- The component uses `expo-image-picker` and `expo-media-library` for media selection
- Permissions are automatically requested when needed
- Selected assets are stored in memory during the session
- Preview thumbnails are generated automatically
- Video duration and file size information is included when available
