# File Picker

> A customizable file picker component with validation, preview, and multiple file support.

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

- Docs: https://ui.ahmedbna.com/docs/components/file-picker
- Markdown: https://ui.ahmedbna.com/docs/components/file-picker.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/file-picker.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/file-picker.json
- Install: `npx bna-ui add file-picker`
- npm dependencies: `expo-document-picker`, `expo-haptics`, `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/0138-file-picker-demo.MP4

---

**Example:** A basic file picker with validation and preview

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

export function FilePickerDemo() {
  return (
    <FilePicker
      onFilesSelected={(files) => console.log('Selected files:', files)}
      onError={(error) => console.error('Error:', error)}
      fileType='all'
      multiple={true}
      maxFiles={5}
      placeholder='Select your files'
      showFileInfo={true}
    />
  );
}
```

## Installation

### CLI

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

### Manual

**1.** Install the following dependencies:

```bash
npx expo install expo-document-picker lucide-react-native
```

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

```tsx
// components/ui/file-picker.tsx
import { Button, 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 * as DocumentPicker from 'expo-document-picker';
import { File, Image, X } from 'lucide-react-native';
import React, { forwardRef, useCallback, useMemo, useState } from 'react';
import {
  ScrollView,
  StyleSheet,
  TouchableOpacity,
  ViewStyle,
} from 'react-native';

export type FileType = 'image' | 'document' | 'all';

export interface SelectedFile {
  uri: string;
  name: string;
  type?: string;
  size?: number;
  mimeType?: string;
}

export interface FilePickerProps {
  // Core functionality
  onFilesSelected: (files: SelectedFile[]) => void;
  onError?: (error: string) => void;

  // Configuration
  fileType?: FileType;
  multiple?: boolean;
  maxFiles?: number;
  maxSizeBytes?: number;
  allowedExtensions?: string[];

  // UI customization
  placeholder?: string;
  disabled?: boolean;
  style?: ViewStyle;
  showPreview?: boolean;
  showFileInfo?: boolean;

  // Accessibility
  accessibilityLabel?: string;
  accessibilityHint?: string;

  variant?: ButtonVariant;
}

interface FilePickerMethods {
  clearFiles: () => void;
  openPicker: () => void;
}

export const FilePicker = forwardRef<FilePickerMethods, FilePickerProps>(
  (
    {
      onFilesSelected,
      onError,
      fileType = 'all',
      multiple = false,
      maxFiles = 10,
      maxSizeBytes = 10 * 1024 * 1024, // 10MB default
      allowedExtensions,
      placeholder = 'Select files',
      disabled = false,
      style = {},
      showPreview = true,
      showFileInfo = true,
      accessibilityLabel,
      accessibilityHint,
      variant = 'outline',
    },
    ref
  ) => {
    const [selectedFiles, setSelectedFiles] = useState<SelectedFile[]>([]);

    // Theme colors
    const backgroundColor = useColor('card');
    const borderColor = useColor('border');
    const textColor = useColor('text');
    const mutedTextColor = useColor('textMuted');
    const primaryColor = useColor('primary');

    // Expose methods via ref
    React.useImperativeHandle(ref, () => ({
      clearFiles: () => {
        setSelectedFiles([]);
        onFilesSelected([]);
      },
      openPicker: () => {
        handleDocumentPick();
      },
    }));

    const validateFile = useCallback(
      (file: SelectedFile): string | null => {
        // Size validation
        if (file.size && file.size > maxSizeBytes) {
          return `File size exceeds ${(maxSizeBytes / (1024 * 1024)).toFixed(
            1
          )}MB limit`;
        }

        // Extension validation
        if (allowedExtensions && allowedExtensions.length > 0) {
          const extension = file.name.split('.').pop()?.toLowerCase();
          if (!extension || !allowedExtensions.includes(extension)) {
            return `File type not allowed. Allowed types: ${allowedExtensions.join(
              ', '
            )}`;
          }
        }

        return null;
      },
      [maxSizeBytes, allowedExtensions]
    );

    const addFiles = useCallback(
      (newFiles: SelectedFile[]) => {
        const validFiles: SelectedFile[] = [];
        const errors: string[] = [];

        for (const file of newFiles) {
          const error = validateFile(file);
          if (error) {
            errors.push(`${file.name}: ${error}`);
          } else {
            validFiles.push(file);
          }
        }

        if (errors.length > 0) {
          onError?.(errors.join('\n'));
        }

        if (validFiles.length > 0) {
          const updatedFiles = multiple
            ? [...selectedFiles, ...validFiles].slice(0, maxFiles)
            : validFiles.slice(0, 1);

          setSelectedFiles(updatedFiles);
          onFilesSelected(updatedFiles);

          if (multiple && selectedFiles.length + validFiles.length > maxFiles) {
            onError?.(`Only first ${maxFiles} files were selected`);
          }
        }
      },
      [
        selectedFiles,
        multiple,
        maxFiles,
        validateFile,
        onFilesSelected,
        onError,
      ]
    );

    const removeFile = useCallback(
      (index: number) => {
        const updatedFiles = selectedFiles.filter((_, i) => i !== index);
        setSelectedFiles(updatedFiles);
        onFilesSelected(updatedFiles);
      },
      [selectedFiles, onFilesSelected]
    );

    const handleDocumentPick = useCallback(async () => {
      try {
        const result = await DocumentPicker.getDocumentAsync({
          type: fileType === 'image' ? 'image/*' : '*/*',
          multiple,
          copyToCacheDirectory: true,
        });

        if (!result.canceled) {
          const files: SelectedFile[] = result.assets.map((asset) => ({
            uri: asset.uri,
            name: asset.name,
            size: asset.size,
            mimeType: asset.mimeType || undefined,
          }));
          addFiles(files);
        }
      } catch (error) {
        onError?.(`Failed to pick document: ${error}`);
      }
    }, [fileType, multiple, addFiles, onError]);

    const handlePickerPress = useCallback(() => {
      if (disabled) return;

      handleDocumentPick();
    }, [disabled, fileType, handleDocumentPick]);

    const formatFileSize = (bytes: number): string => {
      if (bytes < 1024) return `${bytes} B`;
      if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
      return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
    };

    const getFileIcon = (fileName: string) => {
      const extension = fileName.split('.').pop()?.toLowerCase();
      if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension || '')) {
        return <Image size={20} color={primaryColor} />;
      }
      return <File size={20} color={primaryColor} />;
    };

    return (
      <View style={[styles.container]}>
        {/* File Picker Button */}
        <Button
          variant={variant}
          onPress={handlePickerPress}
          disabled={disabled}
          style={[styles.pickerButton, style]}
          accessibilityLabel={accessibilityLabel || `Select ${fileType} files`}
          accessibilityHint={accessibilityHint || 'Opens file picker'}
        >
          <View style={styles.buttonContent}>
            {fileType === 'image' ? (
              <Image
                size={20}
                color={disabled ? mutedTextColor : primaryColor}
              />
            ) : (
              <File
                size={20}
                color={disabled ? mutedTextColor : primaryColor}
              />
            )}
            <Text
              style={[
                styles.buttonText,
                { color: disabled ? mutedTextColor : textColor },
              ]}
            >
              {selectedFiles.length > 0
                ? `${selectedFiles.length} file${
                    selectedFiles.length > 1 ? 's' : ''
                  } selected`
                : placeholder}
            </Text>
          </View>
        </Button>

        {/* Selected Files Preview */}
        {showPreview && selectedFiles.length > 0 && (
          <ScrollView
            style={styles.filesContainer}
            showsVerticalScrollIndicator={false}
          >
            {selectedFiles.map((file, index) => (
              <View
                key={`${file.uri}-${index}`}
                style={[styles.fileItem, { backgroundColor, borderColor }]}
              >
                <View style={styles.fileInfo}>
                  {getFileIcon(file.name)}
                  <View style={styles.fileDetails}>
                    <Text
                      style={[styles.fileName, { color: textColor }]}
                      numberOfLines={1}
                    >
                      {file.name}
                    </Text>
                    {showFileInfo && file.size && (
                      <Text
                        style={[styles.fileSize, { color: mutedTextColor }]}
                      >
                        {formatFileSize(file.size)}
                      </Text>
                    )}
                  </View>
                </View>
                <TouchableOpacity
                  onPress={() => removeFile(index)}
                  style={styles.removeButton}
                  accessibilityLabel={`Remove ${file.name}`}
                >
                  <X size={16} color={mutedTextColor} />
                </TouchableOpacity>
              </View>
            ))}
          </ScrollView>
        )}
      </View>
    );
  }
);

FilePicker.displayName = 'FilePicker';

const styles = StyleSheet.create({
  container: {
    width: '100%',
  },
  pickerButton: {
    justifyContent: 'flex-start',
    paddingHorizontal: 16,
    minHeight: 48,
  },
  buttonContent: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 12,
  },
  buttonText: {
    fontSize: FONT_SIZE,
    fontWeight: '400',
  },
  filesContainer: {
    marginTop: 12,
    maxHeight: 300,
  },
  fileItem: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    padding: 12,
    borderRadius: CORNERS,
    borderWidth: 1,
    marginBottom: 8,
  },
  fileInfo: {
    flexDirection: 'row',
    alignItems: 'center',
    flex: 1,
    gap: 12,
  },
  fileDetails: {
    flex: 1,
  },
  fileName: {
    fontSize: FONT_SIZE,
    fontWeight: '500',
  },
  fileSize: {
    fontSize: 14,
    marginTop: 2,
  },
  removeButton: {
    padding: 4,
  },
});

// Export utility functions for external use
export const createFileFromUri = async (
  uri: string,
  name?: string
): Promise<SelectedFile> => {
  return {
    uri,
    name: name || uri.split('/').pop() || 'file',
  };
};

export const validateFiles = (
  files: SelectedFile[],
  options: {
    maxSize?: number;
    allowedExtensions?: string[];
    maxFiles?: number;
  }
): { valid: SelectedFile[]; errors: string[] } => {
  const valid: SelectedFile[] = [];
  const errors: string[] = [];

  for (const file of files) {
    if (options.maxSize && file.size && file.size > options.maxSize) {
      errors.push(`${file.name}: File too large`);
      continue;
    }

    if (options.allowedExtensions) {
      const ext = file.name.split('.').pop()?.toLowerCase();
      if (!ext || !options.allowedExtensions.includes(ext)) {
        errors.push(`${file.name}: File type not allowed`);
        continue;
      }
    }

    valid.push(file);
  }

  if (options.maxFiles && valid.length > options.maxFiles) {
    valid.splice(options.maxFiles);
    errors.push(`Only first ${options.maxFiles} files selected`);
  }

  return { valid, errors };
};

export interface UseFilePickerOptions {
  maxFiles?: number;
  maxSizeBytes?: number;
  allowedExtensions?: string[];
  onError?: (error: string) => void;
}

export interface UseFilePickerReturn {
  files: SelectedFile[];
  addFiles: (newFiles: SelectedFile[]) => void;
  removeFile: (index: number) => void;
  clearFiles: () => void;
  totalSize: number;
  isValid: boolean;
  errors: string[];
}

export function useFilePicker(
  options: UseFilePickerOptions = {}
): UseFilePickerReturn {
  const {
    maxFiles = 10,
    maxSizeBytes = 10 * 1024 * 1024, // 10MB default
    allowedExtensions,
    onError,
  } = options;

  const [files, setFiles] = useState<SelectedFile[]>([]);
  const [errors, setErrors] = useState<string[]>([]);

  const validateFile = useCallback(
    (file: SelectedFile): string | null => {
      // Check file size
      if (file.size && file.size > maxSizeBytes) {
        return `File size exceeds ${(maxSizeBytes / (1024 * 1024)).toFixed(
          1
        )}MB limit`;
      }

      // Check file extension
      if (allowedExtensions && allowedExtensions.length > 0) {
        const extension = file.name.split('.').pop()?.toLowerCase();
        if (!extension || !allowedExtensions.includes(extension)) {
          return `File type not allowed. Allowed types: ${allowedExtensions.join(
            ', '
          )}`;
        }
      }

      return null;
    },
    [maxSizeBytes, allowedExtensions]
  );

  const addFiles = useCallback(
    (newFiles: SelectedFile[]) => {
      const validFiles: SelectedFile[] = [];
      const validationErrors: string[] = [];

      // Validate each file
      for (const file of newFiles) {
        const error = validateFile(file);
        if (error) {
          validationErrors.push(`${file.name}: ${error}`);
        } else {
          validFiles.push(file);
        }
      }

      // Handle validation errors
      if (validationErrors.length > 0) {
        setErrors(validationErrors);
        onError?.(validationErrors.join('\n'));
      } else {
        setErrors([]);
      }

      // Add valid files
      if (validFiles.length > 0) {
        setFiles((prev) => {
          const combined = [...prev, ...validFiles];

          // Check if exceeds max files limit
          if (combined.length > maxFiles) {
            const truncated = combined.slice(0, maxFiles);
            const truncationError = `Only first ${maxFiles} files were selected`;
            setErrors((prev) => [...prev, truncationError]);
            onError?.(truncationError);
            return truncated;
          }

          return combined;
        });
      }
    },
    [validateFile, maxFiles, onError]
  );

  const removeFile = useCallback((index: number) => {
    setFiles((prev) => prev.filter((_, i) => i !== index));
    // Clear errors when files are removed
    setErrors([]);
  }, []);

  const clearFiles = useCallback(() => {
    setFiles([]);
    setErrors([]);
  }, []);

  // Calculate total size of all files
  const totalSize = useMemo(() => {
    return files.reduce((sum, file) => sum + (file.size || 0), 0);
  }, [files]);

  // Check if current state is valid
  const isValid = useMemo(() => {
    return errors.length === 0 && files.length > 0 && files.length <= maxFiles;
  }, [errors.length, files.length, maxFiles]);

  return {
    files,
    addFiles,
    removeFile,
    clearFiles,
    totalSize,
    isValid,
    errors,
  };
}
```

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

## Usage

```tsx
import { FilePicker } from '@/components/ui/file-picker';
```

```tsx
<FilePicker
  onFilesSelected={(files) => console.log('Selected files:', files)}
  onError={(error) => console.error('Error:', error)}
  fileType='all'
  multiple={true}
  maxFiles={5}
  placeholder='Select your files'
/>
```

## Examples

#### Default

**Example:** A basic file picker with validation and preview

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

export function FilePickerDemo() {
  return (
    <FilePicker
      onFilesSelected={(files) => console.log('Selected files:', files)}
      onError={(error) => console.error('Error:', error)}
      fileType='all'
      multiple={true}
      maxFiles={5}
      placeholder='Select your files'
      showFileInfo={true}
    />
  );
}
```

#### Image Only

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

```tsx
// components/demo/file-picker/file-picker-images.tsx
import { FilePicker, SelectedFile } from '@/components/ui/file-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function FilePickerImages() {
  const [selectedFiles, setSelectedFiles] = useState<SelectedFile[]>([]);

  return (
    <View style={{ gap: 12 }}>
      <FilePicker
        onFilesSelected={setSelectedFiles}
        onError={(error) => console.error('Error:', error)}
        fileType='image'
        multiple={true}
        maxFiles={3}
        maxSizeBytes={5 * 1024 * 1024} // 5MB
        allowedExtensions={['jpg', 'jpeg', 'png', 'gif', 'webp']}
        placeholder='Select images (max 3)'
        showFileInfo={true}
      />
      {selectedFiles.length > 0 && (
        <Text style={{ fontSize: 14, opacity: 0.7 }}>
          {selectedFiles.length} image{selectedFiles.length > 1 ? 's' : ''}{' '}
          selected
        </Text>
      )}
    </View>
  );
}
```

#### Single File

**Example:** File picker for selecting a single file

```tsx
// components/demo/file-picker/file-picker-single.tsx
import { FilePicker, SelectedFile } from '@/components/ui/file-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function FilePickerSingle() {
  const [selectedFile, setSelectedFile] = useState<SelectedFile[]>([]);

  return (
    <View style={{ gap: 12 }}>
      <FilePicker
        onFilesSelected={setSelectedFile}
        onError={(error) => console.error('Error:', error)}
        fileType='document'
        multiple={false}
        maxFiles={1}
        maxSizeBytes={2 * 1024 * 1024} // 2MB
        placeholder='Select a document'
        showFileInfo={true}
      />

      {selectedFile.length > 0 && (
        <View style={{ padding: 12, borderRadius: 8 }}>
          <Text style={{ fontWeight: '500' }}>Selected File:</Text>
          <Text style={{ fontSize: 14 }}>{selectedFile[0].name}</Text>
          {selectedFile[0].size && (
            <Text style={{ fontSize: 12, opacity: 0.7 }}>
              {(selectedFile[0].size / 1024).toFixed(1)} KB
            </Text>
          )}
        </View>
      )}
    </View>
  );
}
```

#### With Validation

**Example:** File picker with size limits and extension validation

```tsx
// components/demo/file-picker/file-picker-validation.tsx
import { FilePicker, SelectedFile } from '@/components/ui/file-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function FilePickerValidation() {
  const [error, setError] = useState('');
  const [files, setFiles] = useState<SelectedFile[]>([]);

  return (
    <View style={{ gap: 12 }}>
      <FilePicker
        onFilesSelected={(files) => {
          setFiles(files);
          setError('');
        }}
        onError={setError}
        fileType='all'
        multiple={true}
        maxFiles={2}
        maxSizeBytes={1 * 1024 * 1024} // 1MB limit
        allowedExtensions={['pdf', 'doc', 'docx', 'txt']}
        placeholder='Select (PDF, DOC, DOCX, TXT only)'
        showFileInfo={true}
      />

      {error && (
        <View
          style={{
            padding: 12,
            backgroundColor: '#ffeaea',
            borderRadius: 8,
            borderWidth: 1,
            borderColor: '#ffcaca',
          }}
        >
          <Text style={{ color: '#d32f2f', fontSize: 14 }}>{error}</Text>
        </View>
      )}

      {files.length > 0 && !error && (
        <View
          style={{
            padding: 12,
            backgroundColor: '#e8f5e8',
            borderRadius: 8,
            borderWidth: 1,
            borderColor: '#c8e6c9',
          }}
        >
          <Text style={{ color: '#2e7d32', fontSize: 14, fontWeight: '500' }}>
            ✓ Files validated successfully
          </Text>
          <Text style={{ color: '#2e7d32', fontSize: 12, marginTop: 4 }}>
            {files.length} file{files.length > 1 ? 's' : ''} ready for upload
          </Text>
        </View>
      )}
    </View>
  );
}
```

#### Custom Styling

**Example:** File picker with custom styling and colors

```tsx
// components/demo/file-picker/file-picker-styled.tsx
import { FilePicker } from '@/components/ui/file-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React from 'react';

export function FilePickerStyled() {
  return (
    <View style={{ gap: 16 }}>
      {/* Primary Style */}
      <View>
        <Text style={{ marginBottom: 8, fontWeight: '500' }}>
          Primary Style
        </Text>

        <FilePicker
          variant='ghost'
          onFilesSelected={(files) => console.log('Primary files:', files)}
          onError={(error) => console.error('Error:', error)}
          fileType='all'
          multiple={true}
          maxFiles={3}
          placeholder='Upload files'
          style={{
            borderWidth: 2,
            borderColor: '#007AFF',
            borderRadius: 12,
            // backgroundColor: '#f0f8ff',
          }}
        />
      </View>

      {/* Minimal Style */}
      <View>
        <Text style={{ marginBottom: 8, fontWeight: '500' }}>
          Minimal Style
        </Text>
        <FilePicker
          variant='ghost'
          onFilesSelected={(files) => console.log('Minimal files:', files)}
          onError={(error) => console.error('Error:', error)}
          fileType='image'
          multiple={false}
          maxFiles={1}
          placeholder='Choose image'
          style={{
            borderWidth: 1,
            borderStyle: 'dashed',
            borderColor: '#ccc',
            borderRadius: 8,
            backgroundColor: 'transparent',
          }}
        />
      </View>

      {/* Success Style */}
      <View>
        <Text style={{ marginBottom: 8, fontWeight: '500' }}>
          Success Style
        </Text>
        <FilePicker
          variant='ghost'
          onFilesSelected={(files) => console.log('Success files:', files)}
          onError={(error) => console.error('Error:', error)}
          fileType='document'
          multiple={true}
          maxFiles={5}
          placeholder='Select documents'
          style={{
            borderWidth: 2,
            borderColor: '#34C759',
            borderRadius: 16,
            // backgroundColor: '#f0fff4',
          }}
        />
      </View>
    </View>
  );
}
```

#### Controlled

**Example:** Controlled file picker using the useFilePicker hook

```tsx
// components/demo/file-picker/file-picker-controlled.tsx
import { Button } from '@/components/ui/button';
import { useFilePicker } from '@/components/ui/file-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React from 'react';

export function FilePickerControlled() {
  const {
    files,
    addFiles,
    removeFile,
    clearFiles,
    totalSize,
    isValid,
    errors,
  } = useFilePicker({
    maxFiles: 3,
    maxSizeBytes: 2 * 1024 * 1024, // 2MB
    allowedExtensions: ['pdf', 'jpg', 'png', 'doc'],
    onError: (error) => console.error('Validation error:', error),
  });

  const formatSize = (bytes: number) => {
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
    return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
  };

  // Simulate adding files (in real app, this would come from file picker)
  const simulateAddFiles = () => {
    const mockFiles = [
      { uri: 'file://test1.pdf', name: 'test1.pdf', size: 150000 },
      { uri: 'file://test2.jpg', name: 'test2.jpg', size: 250000 },
    ];
    addFiles(mockFiles);
  };

  return (
    <View style={{ gap: 16 }}>
      <View style={{ flexDirection: 'row', gap: 8 }}>
        <Button
          onPress={simulateAddFiles}
          style={{ flex: 1 }}
          variant='outline'
        >
          <Text>Add Mock Files</Text>
        </Button>
        <Button
          onPress={clearFiles}
          style={{ flex: 1 }}
          variant='outline'
          disabled={files.length === 0}
        >
          <Text>Clear All</Text>
        </Button>
      </View>

      {/* Status Info */}
      <View
        style={{
          padding: 12,
          borderRadius: 8,
        }}
      >
        <Text style={{ fontWeight: '500' }}>Status</Text>
        <Text style={{ fontSize: 14 }}>Files: {files.length}/3</Text>
        <Text style={{ fontSize: 14 }}>
          Total Size: {formatSize(totalSize)}
        </Text>
        <Text style={{ fontSize: 14 }}>Valid: {isValid ? '✓' : '✗'}</Text>
      </View>

      {/* Errors */}
      {errors.length > 0 && (
        <View
          style={{
            padding: 12,
            backgroundColor: '#ffeaea',
            borderRadius: 8,
          }}
        >
          <Text style={{ color: '#d32f2f', fontWeight: '500' }}>Errors:</Text>
          {errors.map((error, index) => (
            <Text key={index} style={{ color: '#d32f2f', fontSize: 14 }}>
              • {error}
            </Text>
          ))}
        </View>
      )}

      {/* Files List */}
      {files.length > 0 && (
        <View>
          <Text style={{ fontWeight: '500', marginBottom: 8 }}>
            Selected Files:
          </Text>
          {files.map((file, index) => (
            <View
              key={index}
              style={{
                flexDirection: 'row',
                justifyContent: 'space-between',
                alignItems: 'center',
                padding: 8,
                borderRadius: 6,
                marginBottom: 4,
                borderWidth: 1,
                borderColor: '#e0e0e0',
              }}
            >
              <View style={{ flex: 1 }}>
                <Text style={{ fontSize: 14, fontWeight: '500' }}>
                  {file.name}
                </Text>
                {file.size && (
                  <Text style={{ fontSize: 12, opacity: 0.7 }}>
                    {formatSize(file.size)}
                  </Text>
                )}
              </View>
              <Button
                onPress={() => removeFile(index)}
                variant='ghost'
                style={{ padding: 4 }}
              >
                <Text style={{ color: '#d32f2f' }}>Remove</Text>
              </Button>
            </View>
          ))}
        </View>
      )}
    </View>
  );
}
```

#### With File Info

**Example:** File picker displaying detailed file information

```tsx
// components/demo/file-picker/file-picker-info.tsx
import { FilePicker, SelectedFile } from '@/components/ui/file-picker';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useColor } from '@/hooks/useColor';
import React, { useState } from 'react';

export function FilePickerInfo() {
  const card = useColor('card');
  const [files, setFiles] = useState<SelectedFile[]>([]);
  const [uploadProgress, setUploadProgress] = useState<any>({});

  const handleFilesSelected = (selectedFiles: SelectedFile[]) => {
    setFiles(selectedFiles);
    // Simulate upload progress
    selectedFiles.forEach((file, index) => {
      let progress = 0;
      const interval = setInterval(() => {
        progress += Math.random() * 20;
        if (progress >= 100) {
          progress = 100;
          clearInterval(interval);
        }
        setUploadProgress((prev: any) => ({
          ...prev,
          [index]: Math.min(progress, 100),
        }));
      }, 200);
    });
  };

  const formatFileSize = (bytes: number) => {
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
    return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
  };

  const getFileTypeIcon = (fileName: string) => {
    const ext = fileName.split('.').pop()?.toLowerCase() || '';

    const typeMap: Record<string, string> = {
      pdf: '📄',
      doc: '📝',
      docx: '📝',
      txt: '📄',
      jpg: '🖼️',
      jpeg: '🖼️',
      png: '🖼️',
      gif: '🖼️',
      zip: '📦',
      rar: '📦',
      mp4: '🎥',
      mp3: '🎵',
    };

    return typeMap[ext] || '📎';
  };

  return (
    <View style={{ gap: 16 }}>
      <FilePicker
        onFilesSelected={handleFilesSelected}
        onError={(error) => console.error('Error:', error)}
        fileType='all'
        multiple={true}
        maxFiles={4}
        maxSizeBytes={5 * 1024 * 1024} // 5MB
        placeholder='Select files for detailed preview'
        showFileInfo={true}
      />

      {files.length > 0 && (
        <View>
          <Text style={{ fontSize: 16, fontWeight: '600', marginBottom: 12 }}>
            File Details
          </Text>

          {files.map((file, index) => (
            <View
              key={index}
              style={{
                padding: 16,
                backgroundColor: card,
                borderRadius: 12,
                marginBottom: 12,

                shadowColor: '#000',
                shadowOffset: { width: 0, height: 2 },
                shadowOpacity: 0.1,
                shadowRadius: 4,
                elevation: 2,
              }}
            >
              <View
                style={{
                  flexDirection: 'row',
                  alignItems: 'flex-start',
                  gap: 12,
                }}
              >
                <Text style={{ fontSize: 24 }}>
                  {getFileTypeIcon(file.name)}
                </Text>

                <View style={{ flex: 1 }}>
                  <Text
                    style={{ fontSize: 16, fontWeight: '500', marginBottom: 4 }}
                  >
                    {file.name}
                  </Text>

                  <View style={{ gap: 2 }}>
                    {file.size && (
                      <Text variant='caption' style={{ fontSize: 14 }}>
                        Size: {formatFileSize(file.size)}
                      </Text>
                    )}

                    {file.mimeType && (
                      <Text variant='caption' style={{ fontSize: 14 }}>
                        Type: {file.mimeType}
                      </Text>
                    )}

                    <Text variant='caption' style={{ fontSize: 14 }}>
                      Status:{' '}
                      {uploadProgress[index] >= 100
                        ? 'Uploaded'
                        : 'Uploading...'}
                    </Text>
                  </View>

                  {/* Progress Bar */}
                  {uploadProgress[index] !== undefined && (
                    <View style={{ marginTop: 8 }}>
                      <View
                        style={{
                          height: 4,
                          backgroundColor: '#e0e0e0',
                          borderRadius: 2,
                          overflow: 'hidden',
                        }}
                      >
                        <View
                          style={{
                            height: '100%',
                            width: `${uploadProgress[index] || 0}%`,
                            backgroundColor:
                              uploadProgress[index] >= 100
                                ? '#4CAF50'
                                : '#2196F3',
                            borderRadius: 2,
                          }}
                        />
                      </View>
                      <Text
                        variant='caption'
                        style={{ fontSize: 12, marginTop: 4 }}
                      >
                        {Math.round(uploadProgress[index] || 0)}%
                      </Text>
                    </View>
                  )}
                </View>
              </View>
            </View>
          ))}

          {/* Summary */}
          <View
            style={{
              padding: 12,
              backgroundColor: '#f0f8ff',
              borderRadius: 8,
              borderWidth: 1,
              borderColor: '#e3f2fd',
            }}
          >
            <Text style={{ fontSize: 14, fontWeight: '500', color: '#1976d2' }}>
              Summary
            </Text>
            <Text style={{ fontSize: 14, color: '#1976d2' }}>
              {files.length} file{files.length > 1 ? 's' : ''} • Total size:{' '}
              {formatFileSize(
                files.reduce((sum, file) => sum + (file.size || 0), 0)
              )}
            </Text>
          </View>
        </View>
      )}
    </View>
  );
}
```

## API Reference

### FilePicker

The main file picker component.

| Prop                  | Type                              | Default          | Description                                |
| --------------------- | --------------------------------- | ---------------- | ------------------------------------------ |
| `onFilesSelected`     | `(files: SelectedFile[]) => void` | -                | Callback when files are selected.          |
| `onError?`            | `(error: string) => void`         | -                | Callback when an error occurs.             |
| `fileType?`           | `'image' \| 'document' \| 'all'`  | `'all'`          | Type of files to allow.                    |
| `multiple?`           | `boolean`                         | `false`          | Whether to allow multiple file selection.  |
| `maxFiles?`           | `number`                          | `10`             | Maximum number of files to select.         |
| `maxSizeBytes?`       | `number`                          | `10MB`           | Maximum file size in bytes.                |
| `allowedExtensions?`  | `string[]`                        | -                | Array of allowed file extensions.          |
| `placeholder?`        | `string`                          | `'Select files'` | Placeholder text for the picker button.    |
| `disabled?`           | `boolean`                         | `false`          | Whether the picker is disabled.            |
| `style?`              | `ViewStyle`                       | -                | Additional styles for the container.       |
| `variant?`            | `ButtonVariant`                   | `'outline'`      | Visual variant of the picker button.       |
| `showPreview?`        | `boolean`                         | `true`           | Whether to show the selected-files list.   |
| `showFileInfo?`       | `boolean`                         | `true`           | Whether to show file size information.     |
| `accessibilityLabel?` | `string`                          | -                | Accessibility label for the picker button. |
| `accessibilityHint?`  | `string`                          | -                | Accessibility hint for the picker button.  |

### SelectedFile

The file object structure returned by the component.

| Property    | Type     | Description             |
| ----------- | -------- | ----------------------- |
| `uri`       | `string` | The file URI.           |
| `name`      | `string` | The file name.          |
| `type?`     | `string` | The file MIME type.     |
| `size?`     | `number` | The file size in bytes. |
| `mimeType?` | `string` | The file MIME type.     |

### useFilePicker Hook

A hook for managing file picker state programmatically.

```tsx
const { files, addFiles, removeFile, clearFiles, totalSize, isValid, errors } =
  useFilePicker({
    maxFiles: 5,
    maxSizeBytes: 5 * 1024 * 1024, // 5MB
    allowedExtensions: ['pdf', 'doc', 'docx'],
    onError: (error) => console.error(error),
  });
```

#### Options

| Property             | Type                      | Default | Description                       |
| -------------------- | ------------------------- | ------- | --------------------------------- |
| `maxFiles?`          | `number`                  | `10`    | Maximum number of files.          |
| `maxSizeBytes?`      | `number`                  | `10MB`  | Maximum file size in bytes.       |
| `allowedExtensions?` | `string[]`                | -       | Array of allowed file extensions. |
| `onError?`           | `(error: string) => void` | -       | Callback when an error occurs.    |

#### Return Value

| Property     | Type                              | Description                         |
| ------------ | --------------------------------- | ----------------------------------- |
| `files`      | `SelectedFile[]`                  | Array of selected files.            |
| `addFiles`   | `(files: SelectedFile[]) => void` | Function to add files.              |
| `removeFile` | `(index: number) => void`         | Function to remove a file by index. |
| `clearFiles` | `() => void`                      | Function to clear all files.        |
| `totalSize`  | `number`                          | Total size of all files in bytes.   |
| `isValid`    | `boolean`                         | Whether the current state is valid. |
| `errors`     | `string[]`                        | Array of validation errors.         |

### Utility Functions

#### createFileFromUri

```tsx
const file = await createFileFromUri(uri, 'custom-name.pdf');
```

#### validateFiles

```tsx
const { valid, errors } = validateFiles(files, {
  maxSize: 5 * 1024 * 1024,
  allowedExtensions: ['pdf', 'doc'],
  maxFiles: 3,
});
```

## File Types

The component supports three file type modes:

- `'all'` - All file types (default)
- `'image'` - Images only (jpg, jpeg, png, gif, webp, etc.)
- `'document'` - All file types with document picker

## Validation

The FilePicker includes built-in validation for:

- **File size** - Configurable maximum size per file
- **File extensions** - Whitelist of allowed extensions
- **File count** - Maximum number of files
- **MIME types** - Automatic validation based on file type

## Accessibility

The FilePicker component is built with accessibility in mind:

- Proper accessibility labels and hints
- Screen reader support for file information
- Keyboard navigation support
- Clear error messaging
- Semantic button structure

## Best Practices

1. **Set appropriate file size limits** to prevent memory issues
2. **Use specific file type restrictions** when possible
3. **Provide clear error messages** to guide users
4. **Show file previews** when relevant
5. **Handle loading states** for better UX
6. **Validate files** on both client and server side
