# Picker

> A customizable dropdown picker component with search, sections, and multiple selection 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/picker
- Markdown: https://ui.ahmedbna.com/docs/components/picker.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/picker.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/picker.json
- Install: `npx bna-ui add picker`
- npm dependencies: `expo-haptics`, `lucide-react-native`, `react-native-svg`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `useHaptics`, `globals`, `text`, `view`, `icon`, `scroll-view`
- Preview recording: https://demo.ahmedbna.com/0211-picker-demo.MP4

---

**Example:** A basic picker with simple options

```tsx
// components/demo/picker/picker-demo.tsx
import { Picker } from '@/components/ui/picker';
import React, { useState } from 'react';

export function PickerDemo() {
  const [value, setValue] = useState<string>('');

  const options = [
    { label: 'Apple', value: 'apple' },
    { label: 'Banana', value: 'banana' },
    { label: 'Orange', value: 'orange' },
    { label: 'Grape', value: 'grape' },
  ];

  return (
    <Picker
      options={options}
      value={value}
      onValueChange={setValue}
      placeholder='Select a fruit...'
    />
  );
}
```

## Installation

### CLI

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

### Manual

**1.** Install the following dependencies:

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

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

```tsx
// components/ui/picker.tsx
import { Icon } from '@/components/ui/icon';
import { ScrollView } from '@/components/ui/scroll-view';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useColor } from '@/hooks/useColor';
import { useHaptics } from '@/hooks/useHaptics';
import { BORDER_RADIUS, CORNERS, FONT_SIZE, HEIGHT } from '@/theme/globals';
import { ChevronDown, LucideProps } from 'lucide-react-native';
import React, { useMemo, useState } from 'react';
import {
  Modal,
  Pressable,
  TextInput,
  TextStyle,
  TouchableOpacity,
  ViewStyle,
} from 'react-native';

export interface PickerOption {
  label: string;
  value: string;
  description?: string;
  disabled?: boolean;
}

export interface PickerSection {
  title?: string;
  options: PickerOption[];
}

interface PickerProps {
  options?: PickerOption[];
  sections?: PickerSection[];
  value?: string;
  placeholder?: string;
  error?: string;
  variant?: 'outline' | 'filled' | 'group';
  onValueChange?: (value: string) => void;
  disabled?: boolean;
  style?: ViewStyle;
  multiple?: boolean;
  values?: string[];
  onValuesChange?: (values: string[]) => void;

  // Styling props
  label?: string;
  icon?: React.ComponentType<LucideProps>;
  rightComponent?: React.ReactNode | (() => React.ReactNode);
  inputStyle?: TextStyle;
  labelStyle?: TextStyle;
  errorStyle?: TextStyle;

  // Modal props
  modalTitle?: string;
  searchable?: boolean;
  searchPlaceholder?: string;
  haptic?: boolean;
}

export function Picker({
  options = [],
  sections = [],
  value,
  values = [],
  error,
  variant = 'filled',
  placeholder = 'Select an option...',
  onValueChange,
  onValuesChange,
  disabled = false,
  style,
  multiple = false,
  label,
  icon,
  rightComponent,
  inputStyle,
  labelStyle,
  errorStyle,
  modalTitle,
  searchable = false,
  searchPlaceholder = 'Search options...',
  haptic = true,
}: PickerProps) {
  const [isOpen, setIsOpen] = useState(false);
  const [searchQuery, setSearchQuery] = useState('');
  const feedback = useHaptics(haptic);

  // Move ALL theme color hooks to the top level
  const borderColor = useColor('border');
  const text = useColor('text');
  const muted = useColor('mutedForeground');
  const cardColor = useColor('card');
  const danger = useColor('red');
  const accent = useColor('accent');
  const primary = useColor('primary');
  const primaryForeground = useColor('primaryForeground');
  const input = useColor('input');
  const mutedBg = useColor('muted');
  const textMutedColor = useColor('textMuted');

  // Normalize data structure - convert options to sections format
  const normalizedSections: PickerSection[] =
    sections.length > 0 ? sections : [{ options }];

  // Filter sections based on search query — memoized so typing in an
  // unrelated part of the screen doesn't re-filter every option on every
  // render. Depends on `sections`/`options` directly rather than
  // `normalizedSections`, which is a fresh array every render.
  const filteredSections = useMemo(
    () =>
      searchable && searchQuery
        ? normalizedSections
            .map((section) => ({
              ...section,
              options: section.options.filter((option) =>
                option.label.toLowerCase().includes(searchQuery.toLowerCase())
              ),
            }))
            .filter((section) => section.options.length > 0)
        : normalizedSections,
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [searchable, searchQuery, sections, options]
  );

  // Get selected options for display
  const getSelectedOptions = () => {
    const allOptions = normalizedSections.flatMap((section) => section.options);

    if (multiple) {
      return allOptions.filter((option) => values.includes(option.value));
    } else {
      return allOptions.filter((option) => option.value === value);
    }
  };

  const selectedOptions = getSelectedOptions();

  const handleSelect = (optionValue: string) => {
    if (multiple) {
      // Multi-select rows behave like checkboxes, so they get toggle feedback
      // rather than the one-shot selection tick.
      const isSelected = values.includes(optionValue);
      feedback(isSelected ? 'toggle-off' : 'toggle-on');
      const newValues = isSelected
        ? values.filter((v) => v !== optionValue)
        : [...values, optionValue];
      onValuesChange?.(newValues);
    } else {
      feedback('selection');
      onValueChange?.(optionValue);
      setIsOpen(false);
    }
  };

  const handleOpen = () => {
    if (disabled) return;
    feedback('impact-light');
    setIsOpen(true);
  };

  const getDisplayText = () => {
    if (selectedOptions.length === 0) return placeholder;

    if (multiple) {
      if (selectedOptions.length === 1) {
        return selectedOptions[0].label;
      }
      return `${selectedOptions.length} selected`;
    }

    return selectedOptions[0]?.label || placeholder;
  };

  const triggerStyle: ViewStyle = {
    width: '100%',
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: variant === 'group' ? 0 : 16,
    borderWidth: variant === 'group' ? 0 : 1,
    borderColor: variant === 'outline' ? borderColor : cardColor,
    borderRadius: CORNERS,
    backgroundColor: variant === 'filled' ? cardColor : 'transparent',
    minHeight: variant === 'group' ? 'auto' : HEIGHT,
    opacity: disabled ? 0.5 : 1,
  };

  const renderOption = (
    option: PickerOption,
    sectionIndex: number,
    optionIndex: number
  ) => {
    const isSelected = multiple
      ? values.includes(option.value)
      : value === option.value;

    return (
      <TouchableOpacity
        key={`${sectionIndex}-${option.value}`}
        onPress={() => !option.disabled && handleSelect(option.value)}
        style={{
          paddingVertical: 16,
          paddingHorizontal: 20,
          borderRadius: CORNERS,
          backgroundColor: isSelected ? primary : 'transparent',
          marginVertical: 2,
          alignItems: 'center',
          opacity: option.disabled ? 0.3 : 1,
        }}
        disabled={option.disabled}
        accessibilityRole='menuitem'
        accessibilityState={{ selected: isSelected, disabled: option.disabled }}
      >
        <View
          style={{
            width: '100%',
            alignItems: 'center',
          }}
        >
          <Text
            style={{
              color: isSelected ? primaryForeground : text,
              fontWeight: isSelected ? '600' : '400',
              fontSize: FONT_SIZE,
              textAlign: 'center',
            }}
          >
            {option.label}
          </Text>
          {option.description && (
            <Text
              variant='caption'
              style={{
                marginTop: 4,
                fontSize: 12,
                color: isSelected ? primaryForeground : textMutedColor,
                textAlign: 'center',
              }}
            >
              {option.description}
            </Text>
          )}
        </View>
      </TouchableOpacity>
    );
  };

  return (
    <>
      <TouchableOpacity
        style={[triggerStyle, style]}
        onPress={handleOpen}
        disabled={disabled}
        activeOpacity={0.8}
      >
        {/* Icon & Label */}
        <View
          style={{
            width: label ? 128 : 'auto',
            flexDirection: 'row',
            alignItems: 'center',
            gap: 8,
          }}
          pointerEvents='none'
        >
          {icon && (
            <Icon name={icon} size={16} color={error ? danger : muted} />
          )}
          {label && (
            <Text
              variant='caption'
              numberOfLines={1}
              ellipsizeMode='tail'
              style={[
                {
                  color: error ? danger : muted,
                },
                labelStyle,
              ]}
              pointerEvents='none'
            >
              {label}
            </Text>
          )}
        </View>

        <View
          style={{
            flex: 1,
            flexDirection: 'row',
            alignItems: 'center',
            justifyContent: 'space-between',
          }}
        >
          <Text
            style={[
              {
                fontSize: FONT_SIZE,
                color:
                  selectedOptions.length > 0
                    ? text
                    : disabled
                      ? muted
                      : error
                        ? danger
                        : muted,
              },
              inputStyle,
            ]}
            numberOfLines={1}
            ellipsizeMode='tail'
          >
            {getDisplayText()}
          </Text>

          {rightComponent ? (
            typeof rightComponent === 'function' ? (
              rightComponent()
            ) : (
              rightComponent
            )
          ) : (
            <ChevronDown
              size={16}
              color={error ? danger : muted}
              style={{
                transform: [{ rotate: isOpen ? '180deg' : '0deg' }],
              }}
            />
          )}
        </View>
      </TouchableOpacity>

      {/* Error message */}
      {error && (
        <Text
          variant='caption'
          style={[
            {
              color: danger,
              marginTop: 4,
            },
            errorStyle,
          ]}
        >
          {error}
        </Text>
      )}

      <Modal
        visible={isOpen}
        transparent
        animationType='fade'
        onRequestClose={() => setIsOpen(false)}
      >
        <Pressable
          style={{
            flex: 1,
            backgroundColor: 'rgba(0, 0, 0, 0.5)',
            justifyContent: 'flex-end',
            alignItems: 'center',
          }}
          onPress={() => setIsOpen(false)}
        >
          <Pressable
            style={{
              backgroundColor: cardColor,
              borderTopStartRadius: BORDER_RADIUS,
              borderTopEndRadius: BORDER_RADIUS,
              maxHeight: '70%',
              width: '100%',
              paddingBottom: 32,
              overflow: 'hidden',
            }}
            onPress={(e) => e.stopPropagation()}
          >
            {/* Header */}
            {(modalTitle || multiple) && (
              <View
                style={{
                  padding: 16,
                  borderBottomWidth: 1,
                  borderBottomColor: borderColor,
                  flexDirection: 'row',
                  alignItems: 'center',
                  justifyContent: 'space-between',
                }}
              >
                <Text variant='title'>{modalTitle || 'Select Options'}</Text>

                {multiple && (
                  <TouchableOpacity onPress={() => setIsOpen(false)}>
                    <Text
                      style={{
                        color: primary,
                        fontWeight: '500',
                      }}
                    >
                      Done
                    </Text>
                  </TouchableOpacity>
                )}
              </View>
            )}

            {/* Search */}
            {searchable && (
              <View
                style={{
                  paddingHorizontal: 16,
                  paddingVertical: 8,
                  borderBottomWidth: 1,
                  borderBottomColor: borderColor,
                }}
              >
                <TextInput
                  style={{
                    height: 36,
                    paddingHorizontal: 12,
                    borderRadius: 8,
                    backgroundColor: input,
                    color: text,
                    fontSize: FONT_SIZE,
                  }}
                  placeholder={searchPlaceholder}
                  placeholderTextColor={muted}
                  value={searchQuery}
                  onChangeText={setSearchQuery}
                />
              </View>
            )}

            {/* Options - Updated to match date-picker styling */}
            <View style={{ height: 300 }}>
              <ScrollView
                showsVerticalScrollIndicator={false}
                contentContainerStyle={{
                  paddingVertical: 20,
                  paddingHorizontal: 16,
                }}
              >
                {filteredSections.map((section, sectionIndex) => (
                  <View key={sectionIndex}>
                    {section.title && (
                      <View
                        style={{
                          paddingHorizontal: 4,
                          paddingVertical: 12,
                          marginBottom: 8,
                        }}
                      >
                        <Text
                          variant='caption'
                          style={{
                            fontWeight: '600',
                            color: textMutedColor,
                            fontSize: 12,
                            textTransform: 'uppercase',
                            letterSpacing: 0.5,
                          }}
                        >
                          {section.title}
                        </Text>
                      </View>
                    )}
                    {section.options.map((option, optionIndex) =>
                      renderOption(option, sectionIndex, optionIndex)
                    )}
                  </View>
                ))}

                {filteredSections.every(
                  (section) => section.options.length === 0
                ) && (
                  <View
                    style={{
                      paddingHorizontal: 16,
                      paddingVertical: 24,
                      alignItems: 'center',
                    }}
                  >
                    <Text
                      variant='caption'
                      style={{
                        color: textMutedColor,
                      }}
                    >
                      {searchQuery
                        ? 'No results found'
                        : 'No options available'}
                    </Text>
                  </View>
                )}
              </ScrollView>
            </View>
          </Pressable>
        </Pressable>
      </Modal>
    </>
  );
}
```

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

## Usage

```tsx
import { Picker } from '@/components/ui/picker';
```

```tsx
<Picker
  options={[
    { label: 'Option 1', value: '1' },
    { label: 'Option 2', value: '2' },
    { label: 'Option 3', value: '3' },
  ]}
  value={selectedValue}
  onValueChange={setSelectedValue}
  placeholder='Select an option...'
/>
```

## Examples

#### Default

**Example:** A basic picker with simple options

```tsx
// components/demo/picker/picker-demo.tsx
import { Picker } from '@/components/ui/picker';
import React, { useState } from 'react';

export function PickerDemo() {
  const [value, setValue] = useState<string>('');

  const options = [
    { label: 'Apple', value: 'apple' },
    { label: 'Banana', value: 'banana' },
    { label: 'Orange', value: 'orange' },
    { label: 'Grape', value: 'grape' },
  ];

  return (
    <Picker
      options={options}
      value={value}
      onValueChange={setValue}
      placeholder='Select a fruit...'
    />
  );
}
```

#### With Sections

**Example:** Picker with grouped options in sections

```tsx
// components/demo/picker/picker-sections.tsx
import { Picker } from '@/components/ui/picker';
import React, { useState } from 'react';

export function PickerSections() {
  const [value, setValue] = useState<string>('');

  const sections = [
    {
      title: 'Fruits',
      options: [
        { label: 'Apple', value: 'apple' },
        { label: 'Banana', value: 'banana' },
        { label: 'Orange', value: 'orange' },
      ],
    },
    {
      title: 'Vegetables',
      options: [
        { label: 'Carrot', value: 'carrot' },
        { label: 'Broccoli', value: 'broccoli' },
        { label: 'Spinach', value: 'spinach' },
      ],
    },
  ];

  return (
    <Picker
      sections={sections}
      value={value}
      onValueChange={setValue}
      placeholder='Select an item...'
      modalTitle='Choose Food'
    />
  );
}
```

#### Multiple Selection

**Example:** Picker allowing multiple selections

```tsx
// components/demo/picker/picker-multiple.tsx
import { Picker } from '@/components/ui/picker';
import React, { useState } from 'react';

export function PickerMultiple() {
  const [values, setValues] = useState<string[]>([]);

  const options = [
    { label: 'JavaScript', value: 'js' },
    { label: 'TypeScript', value: 'ts' },
    { label: 'Python', value: 'py' },
    { label: 'Java', value: 'java' },
    { label: 'C++', value: 'cpp' },
    { label: 'Rust', value: 'rust' },
  ];

  return (
    <Picker
      options={options}
      values={values}
      onValuesChange={setValues}
      placeholder='Select languages...'
      multiple
      modalTitle='Programming Languages'
    />
  );
}
```

#### Searchable

**Example:** Picker with search functionality

```tsx
// components/demo/picker/picker-searchable.tsx
import { Picker } from '@/components/ui/picker';
import React, { useState } from 'react';

export function PickerSearchable() {
  const [value, setValue] = useState<string>('');

  const options = [
    { label: 'United States', value: 'us' },
    { label: 'Canada', value: 'ca' },
    { label: 'United Kingdom', value: 'uk' },
    { label: 'Germany', value: 'de' },
    { label: 'France', value: 'fr' },
    { label: 'Japan', value: 'jp' },
    { label: 'Australia', value: 'au' },
    { label: 'Brazil', value: 'br' },
    { label: 'India', value: 'in' },
    { label: 'China', value: 'cn' },
  ];

  return (
    <Picker
      options={options}
      value={value}
      onValueChange={setValue}
      placeholder='Select a country...'
      searchable
      searchPlaceholder='Search countries...'
      modalTitle='Countries'
    />
  );
}
```

#### With Icons and Labels

**Example:** Picker with custom styling, icons, and labels

```tsx
// components/demo/picker/picker-styled.tsx
import { Picker } from '@/components/ui/picker';
import { MapPin, Settings, User } from 'lucide-react-native';
import React, { useState } from 'react';

export function PickerStyled() {
  const [location, setLocation] = useState<string>('');
  const [user, setUser] = useState<string>('');
  const [setting, setSetting] = useState<string>('');

  const locations = [
    { label: 'New York', value: 'ny' },
    { label: 'Los Angeles', value: 'la' },
    { label: 'Chicago', value: 'chi' },
  ];

  const users = [
    { label: 'John Doe', value: 'john' },
    { label: 'Jane Smith', value: 'jane' },
    { label: 'Bob Johnson', value: 'bob' },
  ];

  const settings = [
    { label: 'Notifications', value: 'notifications' },
    { label: 'Privacy', value: 'privacy' },
    { label: 'Account', value: 'account' },
  ];

  return (
    <>
      <Picker
        options={locations}
        value={location}
        onValueChange={setLocation}
        placeholder='Select location...'
        icon={MapPin}
        label='Location'
        variant='outline'
      />

      <Picker
        options={users}
        value={user}
        onValueChange={setUser}
        placeholder='Select user...'
        icon={User}
        label='User'
        variant='filled'
        style={{ marginTop: 16 }}
      />

      <Picker
        options={settings}
        value={setting}
        onValueChange={setSetting}
        placeholder='Select setting...'
        icon={Settings}
        label='Settings'
        variant='group'
        style={{ marginTop: 16 }}
      />
    </>
  );
}
```

#### Variants

**Example:** Different picker variants: outline, filled, and group

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

export function PickerVariants() {
  const [outlineValue, setOutlineValue] = useState<string>('');
  const [filledValue, setFilledValue] = useState<string>('');
  const [groupValue, setGroupValue] = useState<string>('');

  const options = [
    { label: 'Small', value: 'sm' },
    { label: 'Medium', value: 'md' },
    { label: 'Large', value: 'lg' },
    { label: 'Extra Large', value: 'xl' },
  ];

  return (
    <View style={{ gap: 20 }}>
      <View>
        <Text variant='caption' style={{ marginBottom: 8 }}>
          Outline Variant
        </Text>
        <Picker
          options={options}
          value={outlineValue}
          onValueChange={setOutlineValue}
          placeholder='Select size...'
          variant='outline'
        />
      </View>

      <View>
        <Text variant='caption' style={{ marginBottom: 8 }}>
          Filled Variant
        </Text>
        <Picker
          options={options}
          value={filledValue}
          onValueChange={setFilledValue}
          placeholder='Select size...'
          variant='filled'
        />
      </View>

      <View>
        <Text variant='caption' style={{ marginBottom: 8 }}>
          Group Variant
        </Text>
        <Picker
          options={options}
          value={groupValue}
          onValueChange={setGroupValue}
          placeholder='Select size...'
          variant='group'
        />
      </View>
    </View>
  );
}
```

#### Form Integration

**Example:** Picker integrated with form validation and error handling

```tsx
// components/demo/picker/picker-form.tsx
import { Button } from '@/components/ui/button';
import { Picker } from '@/components/ui/picker';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function PickerForm() {
  const [category, setCategory] = useState<string>('');
  const [priority, setPriority] = useState<string>('');
  const [errors, setErrors] = useState<{
    category?: string;
    priority?: string;
  }>({});

  const categories = [
    { label: 'Bug Report', value: 'bug' },
    { label: 'Feature Request', value: 'feature' },
    { label: 'General Inquiry', value: 'general' },
  ];

  const priorities = [
    { label: 'Low', value: 'low' },
    { label: 'Medium', value: 'medium' },
    { label: 'High', value: 'high' },
    { label: 'Critical', value: 'critical' },
  ];

  const handleSubmit = () => {
    const newErrors: { category?: string; priority?: string } = {};

    if (!category) {
      newErrors.category = 'Please select a category';
    }

    if (!priority) {
      newErrors.priority = 'Please select a priority';
    }

    setErrors(newErrors);

    if (Object.keys(newErrors).length === 0) {
      // Form is valid
      console.log('Form submitted:', { category, priority });
    }
  };

  return (
    <View style={{ gap: 16 }}>
      <Picker
        options={categories}
        value={category}
        onValueChange={(value) => {
          setCategory(value);
          if (errors.category) {
            setErrors((prev) => ({ ...prev, category: undefined }));
          }
        }}
        placeholder='Select category...'
        label='Category'
        error={errors.category}
        variant='outline'
      />

      <Picker
        options={priorities}
        value={priority}
        onValueChange={(value) => {
          setPriority(value);
          if (errors.priority) {
            setErrors((prev) => ({ ...prev, priority: undefined }));
          }
        }}
        placeholder='Select priority...'
        label='Priority'
        error={errors.priority}
        variant='outline'
      />

      <Button onPress={handleSubmit}>Submit Ticket</Button>
    </View>
  );
}
```

#### Advanced Features

**Example:** Picker with descriptions, disabled options, and custom modal title

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

export function PickerAdvanced() {
  const [plan, setPlan] = useState<string>('');

  const sections = [
    {
      title: 'Individual Plans',
      options: [
        {
          label: 'Basic',
          value: 'basic',
          description: '$9/month - Perfect for individuals',
        },
        {
          label: 'Pro',
          value: 'pro',
          description: '$19/month - Advanced features included',
        },
      ],
    },
    {
      title: 'Team Plans',
      options: [
        {
          label: 'Team',
          value: 'team',
          description: '$39/month - Collaboration tools',
        },
        {
          label: 'Enterprise',
          value: 'enterprise',
          description: '$99/month - Full enterprise features',
        },
        {
          label: 'Custom',
          value: 'custom',
          description: 'Contact us for pricing',
          disabled: true,
        },
      ],
    },
  ];

  return (
    <View style={{ gap: 16 }}>
      <Picker
        sections={sections}
        value={plan}
        onValueChange={setPlan}
        placeholder='Select a plan...'
        modalTitle='Subscription Plans'
        searchable
        searchPlaceholder='Search plans...'
        variant='outline'
      />

      {plan && (
        <View
          style={{
            padding: 12,
            backgroundColor: '#f0f9ff',
            borderRadius: 8,
            borderWidth: 1,
            borderColor: '#0284c7',
          }}
        >
          <Text style={{ color: '#0284c7', fontWeight: '500' }}>
            Selected:{' '}
            {
              sections.flatMap((s) => s.options).find((o) => o.value === plan)
                ?.label
            }
          </Text>
        </View>
      )}
    </View>
  );
}
```

## API Reference

### Picker

The main picker component that displays options in a modal.

| Prop                | Type                               | Default                 | Description                                                                              |
| ------------------- | ---------------------------------- | ----------------------- | ---------------------------------------------------------------------------------------- |
| `haptic`            | `boolean`                          | `true`                  | Whether to trigger haptic feedback when the picker opens and when an option is selected. |
| `options`           | `PickerOption[]`                   | `[]`                    | Array of options to display.                                                             |
| `sections`          | `PickerSection[]`                  | `[]`                    | Array of sections containing grouped options.                                            |
| `value`             | `string`                           | -                       | Currently selected value (single selection).                                             |
| `values`            | `string[]`                         | `[]`                    | Currently selected values (multiple selection).                                          |
| `placeholder`       | `string`                           | `"Select an option..."` | Placeholder text when no option is selected.                                             |
| `error`             | `string`                           | -                       | Error message to display.                                                                |
| `variant`           | `"outline" \| "filled" \| "group"` | `"filled"`              | Visual variant of the picker.                                                            |
| `onValueChange`     | `(value: string) => void`          | -                       | Callback when single selection changes.                                                  |
| `onValuesChange`    | `(values: string[]) => void`       | -                       | Callback when multiple selection changes.                                                |
| `disabled`          | `boolean`                          | `false`                 | Whether the picker is disabled.                                                          |
| `multiple`          | `boolean`                          | `false`                 | Enable multiple selection mode.                                                          |
| `label`             | `string`                           | -                       | Label text to display.                                                                   |
| `icon`              | `React.ComponentType<LucideProps>` | -                       | Icon component to display.                                                               |
| `rightComponent`    | `ReactNode \| (() => ReactNode)`   | -                       | Custom component to display on the right side.                                           |
| `modalTitle`        | `string`                           | -                       | Title for the modal header.                                                              |
| `searchable`        | `boolean`                          | `false`                 | Enable search functionality.                                                             |
| `searchPlaceholder` | `string`                           | `"Search options..."`   | Placeholder for search input.                                                            |
| `style`             | `ViewStyle`                        | -                       | Additional styles for the picker container.                                              |
| `inputStyle`        | `TextStyle`                        | -                       | Additional styles for the input text.                                                    |
| `labelStyle`        | `TextStyle`                        | -                       | Additional styles for the label text.                                                    |
| `errorStyle`        | `TextStyle`                        | -                       | Additional styles for the error text.                                                    |

### PickerOption

Interface for individual picker options.

| Prop          | Type      | Description                     |
| ------------- | --------- | ------------------------------- |
| `label`       | `string`  | Display text for the option.    |
| `value`       | `string`  | Unique value for the option.    |
| `description` | `string`  | Optional description text.      |
| `disabled`    | `boolean` | Whether the option is disabled. |

### PickerSection

Interface for grouping options into sections.

| Prop      | Type             | Description                  |
| --------- | ---------------- | ---------------------------- |
| `title`   | `string`         | Optional section title.      |
| `options` | `PickerOption[]` | Array of options in section. |

## Accessibility

- Option rows expose `accessibilityRole="menuitem"` and
  `accessibilityState={{ selected, disabled }}` so screen readers announce
  selection state
- Uses `Modal` for the options sheet, which traps interaction within it while open
- The trigger meets the 44×44 minimum touch target on all variants
