# Combobox

> A searchable dropdown component that combines an input with a list of options.

**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/combobox
- Markdown: https://ui.ahmedbna.com/docs/components/combobox.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/combobox.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/combobox.json
- Install: `npx bna-ui add combobox`
- npm dependencies: `expo-haptics`, `lucide-react-native`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `useHaptics`, `globals`
- Preview recording: https://demo.ahmedbna.com/0123-combobox-demo.MP4

---

**Example:** A basic combobox with search functionality

```tsx
// components/demo/combobox/combobox-demo.tsx
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
  OptionType,
} from '@/components/ui/combobox';
import React, { useState } from 'react';

const frameworks: OptionType[] = [
  { value: 'react', label: 'React' },
  { value: 'vue', label: 'Vue' },
  { value: 'angular', label: 'Angular' },
  { value: 'svelte', label: 'Svelte' },
  { value: 'next', label: 'Next.js' },
  { value: 'nuxt', label: 'Nuxt.js' },
];

export function ComboboxDemo() {
  const [value, setValue] = useState<OptionType | null>(null);

  return (
    <Combobox value={value} onValueChange={setValue}>
      <ComboboxTrigger>
        <ComboboxValue placeholder='Select framework...' />
      </ComboboxTrigger>
      <ComboboxContent>
        <ComboboxInput placeholder='Search frameworks...' />
        <ComboboxList>
          <ComboboxEmpty>No framework found.</ComboboxEmpty>
          {frameworks.map((framework) => (
            <ComboboxItem key={framework.value} value={framework.value}>
              {framework.label}
            </ComboboxItem>
          ))}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add combobox
```

### 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/combobox.tsx
import { useColor } from '@/hooks/useColor';
import { useHaptics } from '@/hooks/useHaptics';
import { BORDER_RADIUS, CORNERS, FONT_SIZE, HEIGHT } from '@/theme/globals';
import { ChevronDown } from 'lucide-react-native';
import React, {
  Children,
  cloneElement,
  createContext,
  isValidElement,
  ReactNode,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import {
  Dimensions,
  Modal,
  Pressable,
  ScrollView,
  StyleSheet,
  Text,
  TextInput,
  TextStyle,
  TouchableOpacity,
  View,
  ViewStyle,
} from 'react-native';

// --- 1. DEFINE A SHARED OPTION TYPE ---
export interface OptionType {
  value: string;
  label: string;
}

// Helper to extract a simple string label from children
const getLabelFromChildren = (children: ReactNode): string => {
  let label = '';
  React.Children.forEach(children, (child) => {
    if (typeof child === 'string' || typeof child === 'number') {
      label += child;
    }
  });
  return label;
};

interface ComboboxContextType {
  isOpen: boolean;
  setIsOpen: (open: boolean) => void;
  value: OptionType | null;
  setValue: (option: OptionType) => void;
  searchQuery: string;
  setSearchQuery: (query: string) => void;
  triggerLayout: { x: number; y: number; width: number; height: number };
  setTriggerLayout: (layout: any) => void;
  disabled: boolean;
  multiple: boolean;
  values: OptionType[];
  setValues: (options: OptionType[]) => void;
  filteredItemsCount: number;
  setFilteredItemsCount: (count: number) => void;
  haptic: boolean;
}

const ComboboxContext = createContext<ComboboxContextType | undefined>(
  undefined
);

const useCombobox = () => {
  const context = useContext(ComboboxContext);
  if (!context) {
    throw new Error('Combobox components must be used within a Combobox');
  }
  return context;
};

interface ComboboxProps {
  children: ReactNode;
  value?: OptionType | null;
  onValueChange?: (option: OptionType | null) => void;
  disabled?: boolean;
  multiple?: boolean;
  values?: OptionType[];
  onValuesChange?: (options: OptionType[]) => void;
  haptic?: boolean;
}

export function Combobox({
  children,
  value = null,
  onValueChange,
  disabled = false,
  multiple = false,
  values = [],
  onValuesChange,
  haptic = true,
}: ComboboxProps) {
  const [isOpen, setIsOpen] = useState(false);
  const [searchQuery, setSearchQuery] = useState('');
  const [filteredItemsCount, setFilteredItemsCount] = useState(0);
  const [triggerLayout, setTriggerLayout] = useState({
    x: 0,
    y: 0,
    width: 0,
    height: 0,
  });

  const setValue = (newOption: OptionType) => {
    if (multiple) {
      const isAlreadySelected = values.some((v) => v.value === newOption.value);
      const newValues = isAlreadySelected
        ? values.filter((v) => v.value !== newOption.value)
        : [...values, newOption];
      onValuesChange?.(newValues);
    } else {
      onValueChange?.(newOption);
    }
  };

  const setValues = (newOptions: OptionType[]) => {
    onValuesChange?.(newOptions);
  };

  return (
    <ComboboxContext.Provider
      value={{
        isOpen,
        setIsOpen,
        value,
        setValue,
        searchQuery,
        setSearchQuery,
        triggerLayout,
        setTriggerLayout,
        disabled,
        multiple,
        values,
        setValues,
        filteredItemsCount,
        setFilteredItemsCount,
        haptic,
      }}
    >
      {children}
    </ComboboxContext.Provider>
  );
}

interface ComboboxTriggerProps {
  children: ReactNode;
  style?: ViewStyle;
  error?: boolean;
}

export function ComboboxTrigger({
  children,
  style,
  error = false,
}: ComboboxTriggerProps) {
  const { setIsOpen, setTriggerLayout, disabled, isOpen, haptic } =
    useCombobox();
  const triggerRef = useRef<React.ComponentRef<typeof TouchableOpacity>>(null);
  const cardColor = useColor('card');
  const destructiveColor = useColor('destructive');
  const mutedColor = useColor('textMuted');
  const feedback = useHaptics(haptic);

  const measureTrigger = () => {
    if (triggerRef.current) {
      triggerRef.current.measure((_x, _y, width, height, pageX, pageY) => {
        setTriggerLayout({ x: pageX, y: pageY, width, height });
      });
    }
  };

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

  return (
    <TouchableOpacity
      ref={triggerRef}
      style={[
        styles.trigger,
        {
          backgroundColor: cardColor,
          borderColor: error ? destructiveColor : cardColor,
          opacity: disabled ? 0.6 : 1,
        },
        style,
      ]}
      onPress={handlePress}
      disabled={disabled}
      activeOpacity={0.7}
      accessibilityRole='combobox'
      accessibilityState={{ expanded: isOpen, disabled }}
    >
      <View style={styles.triggerContent}>{children}</View>
      <ChevronDown
        size={20}
        color={mutedColor}
        strokeWidth={2}
        style={[
          styles.chevron,
          { transform: [{ rotate: isOpen ? '180deg' : '0deg' }] },
        ]}
      />
    </TouchableOpacity>
  );
}

interface ComboboxValueProps {
  placeholder?: string;
  style?: TextStyle;
}

export function ComboboxValue({
  placeholder = 'Select...',
  style,
}: ComboboxValueProps) {
  const { value, values, multiple } = useCombobox();
  const textColor = useColor('text');
  const mutedColor = useColor('textMuted');

  const hasValue = multiple ? values.length > 0 : !!value;

  const displayText = multiple
    ? values.length === 0
      ? placeholder
      : values.length === 1
        ? values[0].label
        : `${values.length} selected`
    : value?.label || placeholder;

  return (
    <Text
      style={[
        styles.valueText,
        {
          color: hasValue ? textColor : mutedColor,
        },
        style,
      ]}
      numberOfLines={1}
    >
      {displayText}
    </Text>
  );
}

interface ComboboxContentProps {
  children: ReactNode;
  maxHeight?: number;
}

export function ComboboxContent({
  children,
  maxHeight = 400,
}: ComboboxContentProps) {
  const { isOpen, setIsOpen, setSearchQuery, triggerLayout } = useCombobox();
  const cardColor = useColor('card');
  const borderColor = useColor('border');

  const handleClose = () => {
    setIsOpen(false);
    setSearchQuery('');
  };

  const screenHeight = Dimensions.get('window').height;
  const availableHeight =
    screenHeight - triggerLayout.y - triggerLayout.height - 100;
  const dropdownHeight = Math.min(maxHeight, availableHeight);

  if (!isOpen) {
    return null;
  }

  return (
    <Modal
      visible={isOpen}
      transparent
      animationType='fade'
      onRequestClose={handleClose}
    >
      <Pressable style={styles.overlay} onPress={handleClose}>
        <View
          style={[
            styles.dropdown,
            {
              backgroundColor: cardColor,
              borderColor: borderColor,
              top: triggerLayout.y + triggerLayout.height + 6,
              left: triggerLayout.x,
              width: triggerLayout.width,
              maxHeight: dropdownHeight,
            },
          ]}
        >
          {children}
        </View>
      </Pressable>
    </Modal>
  );
}

interface ComboboxInputProps {
  placeholder?: string;
  style?: ViewStyle;
  autoFocus?: boolean;
}

export function ComboboxInput({
  placeholder = 'Search...',
  style,
  autoFocus = true,
}: ComboboxInputProps) {
  const { searchQuery, setSearchQuery } = useCombobox();
  const textColor = useColor('text');
  const mutedColor = useColor('textMuted');
  const borderColor = useColor('border');

  return (
    <View
      style={[
        styles.searchContainer,
        { borderBottomColor: borderColor },
        style,
      ]}
    >
      <TextInput
        style={[styles.searchInput, { color: textColor }]}
        placeholder={placeholder}
        placeholderTextColor={mutedColor}
        value={searchQuery}
        onChangeText={setSearchQuery}
        autoFocus={autoFocus}
      />
    </View>
  );
}

interface ComboboxListProps {
  children: ReactNode;
  style?: ViewStyle;
}

const countFilteredItems = (nodes: React.ReactNode[]): number => {
  return nodes.reduce<number>((count, node) => {
    if (isValidElement(node)) {
      if (node.type === ComboboxItem) {
        return count + 1;
      }
      if (node.type === ComboboxGroup) {
        const groupChildren = Children.toArray((node.props as any).children);
        return count + countFilteredItems(groupChildren);
      }
    }
    return count;
  }, 0);
};

export function ComboboxList({ children, style }: ComboboxListProps) {
  const { searchQuery, setFilteredItemsCount } = useCombobox();

  // Filtering walks the whole children tree (and every group's children) —
  // memoize rather than redoing that walk, plus the item-count reduction
  // over the result, on every render regardless of whether children or the
  // query actually changed.
  const [filteredChildren, itemCount] = useMemo(() => {
    const filtered = Children.toArray(children).filter((child) => {
      if (!searchQuery) return true;

      if (isValidElement(child) && child.type === ComboboxItem) {
        const props = child.props as any;
        const label = getLabelFromChildren(props.children);
        const searchText = props.searchValue || label || props.value || '';
        return searchText.toLowerCase().includes(searchQuery.toLowerCase());
      }

      if (isValidElement(child) && child.type === ComboboxGroup) {
        const groupProps = child.props as any;
        const groupChildren = Children.toArray(groupProps.children);

        return groupChildren.some((groupChild) => {
          if (isValidElement(groupChild) && groupChild.type === ComboboxItem) {
            const itemProps = groupChild.props as any;
            const label = getLabelFromChildren(itemProps.children);
            const searchText =
              itemProps.searchValue || label || itemProps.value || '';
            return searchText.toLowerCase().includes(searchQuery.toLowerCase());
          }
          return false;
        });
      }

      return true;
    });

    return [filtered, countFilteredItems(filtered)] as const;
  }, [children, searchQuery]);

  useEffect(() => {
    setFilteredItemsCount(itemCount);
  }, [itemCount, setFilteredItemsCount]);

  return (
    <ScrollView
      style={[styles.optionsList, style]}
      showsVerticalScrollIndicator={false}
      keyboardShouldPersistTaps='handled'
    >
      {filteredChildren}
    </ScrollView>
  );
}

interface ComboboxEmptyProps {
  children: ReactNode;
  style?: ViewStyle;
}

export function ComboboxEmpty({ children, style }: ComboboxEmptyProps) {
  const { searchQuery, filteredItemsCount } = useCombobox();
  const mutedColor = useColor('textMuted');

  if (filteredItemsCount > 0) return null;

  return (
    <View style={[styles.emptyContainer, style]}>
      {typeof children === 'string' ? (
        <Text style={[styles.emptyText, { color: mutedColor }]}>
          {children}
        </Text>
      ) : (
        children
      )}
    </View>
  );
}

interface ComboboxGroupProps {
  children: ReactNode;
  heading?: string;
}

export function ComboboxGroup({ children, heading }: ComboboxGroupProps) {
  const { searchQuery } = useCombobox();
  const mutedColor = useColor('textMuted');

  const filteredChildren = Children.toArray(children).filter((child) => {
    if (!searchQuery) return true;

    if (isValidElement(child) && child.type === ComboboxItem) {
      const props = child.props as any;
      const label = getLabelFromChildren(props.children);
      const searchText = props.searchValue || label || props.value || '';
      return searchText.toLowerCase().includes(searchQuery.toLowerCase());
    }
    return true;
  });

  if (searchQuery && filteredChildren.length === 0) return null;

  return (
    <View>
      {heading && (
        <Text style={[styles.groupHeading, { color: mutedColor }]}>
          {heading}
        </Text>
      )}
      {filteredChildren}
    </View>
  );
}

interface ComboboxItemProps {
  children: ReactNode;
  value: string; // The unique value is still a string
  onSelect?: (value: OptionType) => void;
  disabled?: boolean;
  searchValue?: string;
  style?: ViewStyle;
}

export function ComboboxItem({
  children,
  value: itemValue,
  onSelect,
  disabled = false,
  style,
}: ComboboxItemProps) {
  const {
    setValue,
    setIsOpen,
    multiple,
    values: selectedValues,
    value: selectedValue,
    haptic,
  } = useCombobox();
  const textColor = useColor('text');
  const primaryColor = useColor('primary');
  const feedback = useHaptics(haptic);

  const isSelected = multiple
    ? selectedValues.some((v) => v.value === itemValue)
    : selectedValue?.value === itemValue;

  const handleSelect = () => {
    if (disabled) return;

    // Multi-select rows toggle, single-select rows pick — they should not feel
    // the same.
    feedback(
      multiple ? (isSelected ? 'toggle-off' : 'toggle-on') : 'selection'
    );

    const label = getLabelFromChildren(children);
    const selectedOption: OptionType = { value: itemValue, label };

    onSelect?.(selectedOption);
    setValue(selectedOption);

    if (!multiple) {
      setIsOpen(false);
    }
  };

  return (
    <TouchableOpacity
      style={[
        styles.option,
        {
          backgroundColor: isSelected ? `${primaryColor}15` : 'transparent',
          opacity: disabled ? 0.5 : 1,
        },
        style,
      ]}
      onPress={handleSelect}
      disabled={disabled}
      activeOpacity={0.7}
      accessibilityRole='menuitem'
      accessibilityState={{ selected: isSelected, disabled }}
    >
      {typeof children === 'string' ? (
        <Text
          style={[
            styles.optionText,
            {
              color: textColor,
              fontWeight: isSelected ? '600' : '400',
            },
          ]}
        >
          {children}
        </Text>
      ) : (
        Children.map(children, (child) => {
          if (isValidElement(child)) {
            return cloneElement(child, { isSelected } as any);
          }
          return child;
        })
      )}
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  trigger: {
    height: HEIGHT,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    paddingHorizontal: 16,
    borderRadius: CORNERS,
    borderWidth: 1,
  },
  triggerContent: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
  },
  valueText: {
    fontSize: FONT_SIZE,
    flex: 1,
  },
  chevron: {
    marginLeft: 8,
  },
  overlay: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.3)',
  },
  dropdown: {
    position: 'absolute',
    borderRadius: BORDER_RADIUS,
    borderWidth: 1,
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5,
  },
  searchContainer: {
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderBottomWidth: 1,
    height: HEIGHT,
  },
  searchInput: {
    fontSize: FONT_SIZE,
    flex: 1,
  },
  optionsList: {
    maxHeight: 400,
  },
  emptyContainer: {
    padding: 16,
    alignItems: 'center',
  },
  emptyText: {
    fontSize: FONT_SIZE,
    fontStyle: 'italic',
  },
  groupHeading: {
    fontSize: 12,
    fontWeight: '600',
    paddingHorizontal: 16,
    paddingVertical: 8,
    textTransform: 'uppercase',
    letterSpacing: 0.5,
  },
  option: {
    flexDirection: 'row',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingVertical: 12,
    minHeight: 44,
  },
  optionText: {
    fontSize: FONT_SIZE,
    flex: 1,
  },
});
```

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

## Usage

When managing state for the combobox, you will work with an `OptionType` object (`{ value: string; label: string; }`) or an array of these objects for multiple selections.

```tsx
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
  OptionType, // Import the type
} from '@/components/ui/combobox';
import { useState } from 'react';

// ...

const [value, setValue] = useState<OptionType | null>(null);

// ...

<Combobox value={value} onValueChange={setValue}>
  <ComboboxTrigger>
    <ComboboxValue placeholder='Select framework...' />
  </ComboboxTrigger>
  <ComboboxContent>
    <ComboboxInput placeholder='Search frameworks...' />
    <ComboboxList>
      <ComboboxEmpty>No framework found.</ComboboxEmpty>
      <ComboboxItem value='react'>React</ComboboxItem>
      <ComboboxItem value='vue'>Vue</ComboboxItem>
      <ComboboxItem value='angular'>Angular</ComboboxItem>
    </ComboboxList>
  </ComboboxContent>
</Combobox>;
```

## Examples

#### Default

**Example:** A basic combobox with search functionality

```tsx
// components/demo/combobox/combobox-demo.tsx
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
  OptionType,
} from '@/components/ui/combobox';
import React, { useState } from 'react';

const frameworks: OptionType[] = [
  { value: 'react', label: 'React' },
  { value: 'vue', label: 'Vue' },
  { value: 'angular', label: 'Angular' },
  { value: 'svelte', label: 'Svelte' },
  { value: 'next', label: 'Next.js' },
  { value: 'nuxt', label: 'Nuxt.js' },
];

export function ComboboxDemo() {
  const [value, setValue] = useState<OptionType | null>(null);

  return (
    <Combobox value={value} onValueChange={setValue}>
      <ComboboxTrigger>
        <ComboboxValue placeholder='Select framework...' />
      </ComboboxTrigger>
      <ComboboxContent>
        <ComboboxInput placeholder='Search frameworks...' />
        <ComboboxList>
          <ComboboxEmpty>No framework found.</ComboboxEmpty>
          {frameworks.map((framework) => (
            <ComboboxItem key={framework.value} value={framework.value}>
              {framework.label}
            </ComboboxItem>
          ))}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}
```

#### With Groups

**Example:** Combobox with grouped options

```tsx
// components/demo/combobox/combobox-groups.tsx
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
  OptionType,
} from '@/components/ui/combobox';
import React, { useState } from 'react';

export function ComboboxGroups() {
  const [value, setValue] = useState<OptionType | null>(null);

  return (
    <Combobox value={value} onValueChange={setValue}>
      <ComboboxTrigger>
        <ComboboxValue placeholder='Select technology...' />
      </ComboboxTrigger>
      <ComboboxContent>
        <ComboboxInput placeholder='Search technologies...' />
        <ComboboxList>
          <ComboboxEmpty>No technology found.</ComboboxEmpty>

          <ComboboxGroup heading='Frontend Frameworks'>
            <ComboboxItem value='react'>React</ComboboxItem>
            <ComboboxItem value='vue'>Vue</ComboboxItem>
            <ComboboxItem value='angular'>Angular</ComboboxItem>
            <ComboboxItem value='svelte'>Svelte</ComboboxItem>
          </ComboboxGroup>

          <ComboboxGroup heading='Backend Frameworks'>
            <ComboboxItem value='express'>Express.js</ComboboxItem>
            <ComboboxItem value='fastify'>Fastify</ComboboxItem>
            <ComboboxItem value='nestjs'>NestJS</ComboboxItem>
            <ComboboxItem value='koa'>Koa</ComboboxItem>
          </ComboboxGroup>

          <ComboboxGroup heading='Mobile'>
            <ComboboxItem value='react-native'>React Native</ComboboxItem>
            <ComboboxItem value='flutter'>Flutter</ComboboxItem>
            <ComboboxItem value='ionic'>Ionic</ComboboxItem>
          </ComboboxGroup>
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}
```

#### Multiple Selection

**Example:** Combobox that allows selecting multiple values

```tsx
// components/demo/combobox/combobox-multiple.tsx
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
  OptionType,
} from '@/components/ui/combobox';
import React, { useState } from 'react';

const skills: OptionType[] = [
  { value: 'javascript', label: 'JavaScript' },
  { value: 'typescript', label: 'TypeScript' },
  { value: 'react', label: 'React' },
  { value: 'vue', label: 'Vue' },
  { value: 'angular', label: 'Angular' },
  { value: 'nodejs', label: 'Node.js' },
  { value: 'python', label: 'Python' },
  { value: 'java', label: 'Java' },
  { value: 'csharp', label: 'C#' },
  { value: 'go', label: 'Go' },
];

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

  return (
    <Combobox multiple values={values} onValuesChange={setValues}>
      <ComboboxTrigger>
        <ComboboxValue placeholder='Select skills...' />
      </ComboboxTrigger>
      <ComboboxContent>
        <ComboboxInput placeholder='Search skills...' />
        <ComboboxList>
          <ComboboxEmpty>No skill found.</ComboboxEmpty>
          {skills.map((skill) => (
            <ComboboxItem key={skill.value} value={skill.value}>
              {skill.label}
            </ComboboxItem>
          ))}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}
```

#### Disabled

**Example:** Disabled combobox component

```tsx
// components/demo/combobox/combobox-disabled.tsx
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
  OptionType,
} from '@/components/ui/combobox';
import React, { useState } from 'react';

const frameworks: OptionType[] = [
  { value: 'react', label: 'React' },
  { value: 'vue', label: 'Vue' },
  { value: 'angular', label: 'Angular' },
];

export function ComboboxDisabled() {
  const [value, setValue] = useState<OptionType | null>({
    value: 'vue',
    label: 'Vue',
  });

  return (
    <Combobox value={value} onValueChange={setValue} disabled>
      <ComboboxTrigger>
        <ComboboxValue placeholder='Select framework...' />
      </ComboboxTrigger>
      <ComboboxContent>
        <ComboboxInput placeholder='Search frameworks...' />
        <ComboboxList>
          <ComboboxEmpty>No framework found.</ComboboxEmpty>
          {frameworks.map((framework) => (
            <ComboboxItem key={framework.value} value={framework.value}>
              {framework.label}
            </ComboboxItem>
          ))}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}
```

#### With Custom Search

**Example:** Combobox with custom search behavior

```tsx
// components/demo/combobox/combobox-search.tsx
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
  OptionType,
} from '@/components/ui/combobox';
import React, { useState } from 'react';

// For clarity, define a type for the country data that includes searchValue
interface CountryOption extends OptionType {
  searchValue: string;
}

const countries: CountryOption[] = [
  {
    value: 'us',
    label: 'United States',
    searchValue: 'united states america usa',
  },
  {
    value: 'uk',
    label: 'United Kingdom',
    searchValue: 'united kingdom england britain uk',
  },
  { value: 'ca', label: 'Canada', searchValue: 'canada canadian' },
  {
    value: 'au',
    label: 'Australia',
    searchValue: 'australia australian aussie',
  },
  { value: 'de', label: 'Germany', searchValue: 'germany german deutschland' },
  { value: 'fr', label: 'France', searchValue: 'france french français' },
  { value: 'jp', label: 'Japan', searchValue: 'japan japanese nihon' },
  { value: 'cn', label: 'China', searchValue: 'china chinese zhongguo' },
];

export function ComboboxSearch() {
  const [value, setValue] = useState<OptionType | null>(null);

  return (
    <Combobox value={value} onValueChange={setValue}>
      <ComboboxTrigger>
        <ComboboxValue placeholder='Select country...' />
      </ComboboxTrigger>
      <ComboboxContent>
        <ComboboxInput placeholder='Search countries...' />
        <ComboboxList>
          <ComboboxEmpty>No country found.</ComboboxEmpty>
          {countries.map((country) => (
            <ComboboxItem
              key={country.value}
              value={country.value}
              searchValue={country.searchValue}
            >
              {country.label}
            </ComboboxItem>
          ))}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}
```

#### Form Integration

**Example:** Combobox integrated with form validation

```tsx
// components/demo/combobox/combobox-form.tsx
import { Button } from '@/components/ui/button';
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
  OptionType,
} from '@/components/ui/combobox';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

const roles: OptionType[] = [
  { value: 'frontend', label: 'Frontend Developer' },
  { value: 'backend', label: 'Backend Developer' },
  { value: 'fullstack', label: 'Full Stack Developer' },
  { value: 'mobile', label: 'Mobile Developer' },
  { value: 'devops', label: 'DevOps Engineer' },
  { value: 'qa', label: 'QA Engineer' },
  { value: 'designer', label: 'UI/UX Designer' },
];

export function ComboboxForm() {
  const [selectedRole, setSelectedRole] = useState<OptionType | null>(null);
  const [error, setError] = useState('');
  const [submitted, setSubmitted] = useState(false);

  const handleSubmit = () => {
    if (!selectedRole) {
      setError('Please select a role');
      return;
    }
    setError('');
    setSubmitted(true);

    // In a real app, you would submit `selectedRole.value`
    console.log('Submitting role:', selectedRole.value);

    // Reset after 2 seconds
    setTimeout(() => {
      setSubmitted(false);
      // 4. Reset the state back to `null`, not an empty string
      setSelectedRole(null);
    }, 2000);
  };

  return (
    <View style={{ gap: 16 }}>
      <View>
        <Text style={{ marginBottom: 8, fontWeight: '600' }}>Job Role *</Text>
        <Combobox
          value={selectedRole}
          onValueChange={(value) => {
            setSelectedRole(value);
            if (value) {
              setError('');
            }
          }}
        >
          <ComboboxTrigger error={!!error}>
            <ComboboxValue placeholder='Select your role...' />
          </ComboboxTrigger>
          <ComboboxContent>
            <ComboboxInput placeholder='Search roles...' />
            <ComboboxList>
              <ComboboxEmpty>No role found.</ComboboxEmpty>
              {roles.map((role) => (
                <ComboboxItem key={role.value} value={role.value}>
                  {role.label}
                </ComboboxItem>
              ))}
            </ComboboxList>
          </ComboboxContent>
        </Combobox>
        {error && (
          <Text style={{ color: 'red', fontSize: 12, marginTop: 4 }}>
            {error}
          </Text>
        )}
      </View>

      <Button onPress={handleSubmit} disabled={submitted}>
        {submitted ? 'Submitted!' : 'Submit'}
      </Button>
    </View>
  );
}
```

#### Large Dataset

**Example:** Combobox handling large datasets efficiently

```tsx
// components/demo/combobox/combobox-large.tsx
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
  OptionType,
} from '@/components/ui/combobox';
import React, { useState } from 'react';

// For clarity, define a more specific type for the dataset items
interface LargeDatasetItem extends OptionType {
  searchValue: string;
}

// Generate a large dataset
const generateLargeDataset = (): LargeDatasetItem[] => {
  const categories = [
    'Technology',
    'Science',
    'Arts',
    'Sports',
    'Business',
    'Health',
  ];
  const adjectives = [
    'Amazing',
    'Innovative',
    'Creative',
    'Dynamic',
    'Efficient',
    'Modern',
  ];
  const nouns = [
    'Solution',
    'Platform',
    'System',
    'Framework',
    'Tool',
    'Service',
  ];

  const items: LargeDatasetItem[] = [];
  for (let i = 0; i < 200; i++) {
    const category = categories[i % categories.length];
    const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
    const noun = nouns[Math.floor(Math.random() * nouns.length)];

    items.push({
      value: `item-${i}`,
      label: `${adjective} ${category} ${noun} ${i + 1}`,
      searchValue: `${category} ${adjective} ${noun}`,
    });
  }

  return items;
};

const largeDataset = generateLargeDataset();

export function ComboboxLarge() {
  const [value, setValue] = useState<OptionType | null>(null);

  return (
    <Combobox value={value} onValueChange={setValue}>
      <ComboboxTrigger>
        <ComboboxValue placeholder='Search from 200+ items...' />
      </ComboboxTrigger>
      <ComboboxContent maxHeight={300}>
        <ComboboxInput placeholder='Type to search...' />
        <ComboboxList>
          <ComboboxEmpty>No items found in dataset.</ComboboxEmpty>
          {largeDataset.map((item) => (
            <ComboboxItem
              key={item.value}
              value={item.value}
              searchValue={item.searchValue}
            >
              {item.label}
            </ComboboxItem>
          ))}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}
```

## API Reference

The Combobox component operates on an `OptionType` object for its state, which has the shape `{ value: string; label: string; }`. The `value` prop of `ComboboxItem` should still be a unique `string`.

### Combobox

The root component that manages the state and context for all child components.

| Prop             | Type                                   | Default | Description                                               |
| ---------------- | -------------------------------------- | ------- | --------------------------------------------------------- |
| `children`       | `ReactNode`                            | -       | The combobox components.                                  |
| `value`          | `OptionType \| null`                   | `null`  | The selected option object (for single selection).        |
| `onValueChange`  | `(option: OptionType \| null) => void` | -       | Callback when a single option object changes.             |
| `values`         | `OptionType[]`                         | `[]`    | An array of selected option objects (multiple selection). |
| `onValuesChange` | `(options: OptionType[]) => void`      | -       | Callback when the array of selected options changes.      |
| `disabled`       | `boolean`                              | `false` | If true, the combobox is disabled.                        |
| `multiple`       | `boolean`                              | `false` | If true, allows multiple selections.                      |

### ComboboxTrigger

The button that triggers the combobox dropdown.

| Prop       | Type        | Default | Description                                  |
| ---------- | ----------- | ------- | -------------------------------------------- |
| `children` | `ReactNode` | -       | The trigger content (usually ComboboxValue). |
| `style`    | `ViewStyle` | -       | Additional styles for the trigger.           |
| `error`    | `boolean`   | `false` | If true, shows error styling on the border.  |

### ComboboxValue

Displays the selected value(s) or placeholder text.

| Prop          | Type        | Default       | Description                            |
| ------------- | ----------- | ------------- | -------------------------------------- |
| `placeholder` | `string`    | `"Select..."` | Placeholder text when no value is set. |
| `style`       | `TextStyle` | -             | Additional styles for the text.        |

### ComboboxContent

The modal container for the dropdown content.

| Prop        | Type        | Default | Description                     |
| ----------- | ----------- | ------- | ------------------------------- |
| `children`  | `ReactNode` | -       | The dropdown content.           |
| `maxHeight` | `number`    | `400`   | Maximum height of the dropdown. |

### ComboboxInput

The search input field within the dropdown.

| Prop          | Type        | Default       | Description                          |
| ------------- | ----------- | ------------- | ------------------------------------ |
| `placeholder` | `string`    | `"Search..."` | Placeholder text for the input.      |
| `style`       | `ViewStyle` | -             | Additional styles for the container. |
| `autoFocus`   | `boolean`   | `true`        | If true, auto-focuses the input.     |

### ComboboxList

A scrollable container for the list of options with filtering capability.

| Prop       | Type        | Default | Description                     |
| ---------- | ----------- | ------- | ------------------------------- |
| `children` | `ReactNode` | -       | The list items and groups.      |
| `style`    | `ViewStyle` | -       | Additional styles for the list. |

### ComboboxEmpty

Displays when no items match the search query.

| Prop       | Type        | Default | Description                          |
| ---------- | ----------- | ------- | ------------------------------------ |
| `children` | `ReactNode` | -       | The empty state content.             |
| `style`    | `ViewStyle` | -       | Additional styles for the container. |

### ComboboxGroup

Groups related options with an optional heading.

| Prop       | Type        | Default | Description                     |
| ---------- | ----------- | ------- | ------------------------------- |
| `children` | `ReactNode` | -       | The group items.                |
| `heading`  | `string`    | -       | Optional heading for the group. |

### ComboboxItem

An individual selectable option within the combobox.

| Prop          | Type                          | Default | Description                                                       |
| ------------- | ----------------------------- | ------- | ----------------------------------------------------------------- |
| `children`    | `ReactNode`                   | -       | The item content. This is used as the `label` for the option.     |
| `value`       | `string`                      | -       | The unique value of the item.                                     |
| `onSelect`    | `(value: OptionType) => void` | -       | Callback when item is selected, receiving the full option object. |
| `disabled`    | `boolean`                     | `false` | If true, the item cannot be selected.                             |
| `searchValue` | `string`                      | -       | A custom string to use for search filtering instead of the label. |
| `style`       | `ViewStyle`                   | -       | Additional styles for the item.                                   |
