# SearchBar

> A customizable search input with debouncing, loading states, and suggestions.

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

---

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

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

export function SearchBarDemo() {
  const [searchQuery, setSearchQuery] = useState('');

  const handleSearch = (query: string) => {
    console.log('Searching for:', query);
  };

  return (
    <SearchBar
      placeholder='Search for anything...'
      value={searchQuery}
      onChangeText={setSearchQuery}
      onSearch={handleSearch}
    />
  );
}
```

## Installation

### CLI

```bash
npx bna-ui add searchbar
```

### 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/searchbar.tsx
import { Icon } from '@/components/ui/icon';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useColor } from '@/hooks/useColor';
import { CORNERS, FONT_SIZE, HEIGHT } from '@/theme/globals';
import { Search, X } from 'lucide-react-native';
import React, { useCallback, useRef, useState } from 'react';
import {
  ActivityIndicator,
  TextInput,
  TextInputProps,
  TextStyle,
  TouchableOpacity,
  ViewStyle,
} from 'react-native';

interface SearchBarProps extends Omit<TextInputProps, 'style'> {
  loading?: boolean;
  onSearch?: (query: string) => void;
  onClear?: () => void;
  showClearButton?: boolean;
  leftIcon?: React.ReactNode;
  rightIcon?: React.ReactNode;
  containerStyle?: ViewStyle | ViewStyle[];
  inputStyle?: TextStyle | TextStyle[];
  debounceMs?: number;
}

export function SearchBar({
  loading = false,
  onSearch,
  onClear,
  showClearButton = true,
  leftIcon,
  rightIcon,
  containerStyle,
  inputStyle,
  debounceMs = 300,
  placeholder = 'Search...',
  value,
  onChangeText,
  ...props
}: SearchBarProps) {
  const [internalValue, setInternalValue] = useState(value || '');
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const inputRef = useRef<TextInput>(null);

  // Theme colors
  const cardColor = useColor('card');
  const textColor = useColor('text');
  const muted = useColor('textMuted');
  const icon = useColor('icon');

  // Handle text change with debouncing
  const handleTextChange = useCallback(
    (text: string) => {
      setInternalValue(text);
      onChangeText?.(text);

      if (onSearch && debounceMs > 0) {
        if (debounceRef.current) {
          clearTimeout(debounceRef.current);
        }
        (debounceRef.current as any) = setTimeout(() => {
          onSearch(text);
        }, debounceMs);
      } else if (onSearch) {
        onSearch(text);
      }
    },
    [onChangeText, onSearch, debounceMs]
  );

  // Handle clear button press
  const handleClear = useCallback(() => {
    setInternalValue('');
    onChangeText?.('');
    onClear?.();
    onSearch?.('');
    if (debounceRef.current) {
      clearTimeout(debounceRef.current);
    }
  }, [onChangeText, onClear, onSearch]);

  // Get container style based on variant and size
  const baseStyle: ViewStyle = {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: cardColor,
    height: HEIGHT,
    paddingHorizontal: 16,
    borderRadius: CORNERS,
  };

  const baseInputStyle = {
    flex: 1,
    fontSize: FONT_SIZE,
    color: textColor,
    marginHorizontal: 8,
  };

  const displayValue = value !== undefined ? value : internalValue;
  const showClear = showClearButton && displayValue.length > 0;

  return (
    <View style={[baseStyle, containerStyle]}>
      {/* Left Icon */}
      {leftIcon || <Icon name={Search} size={16} color={muted} />}

      {/* Text Input */}
      <TextInput
        ref={inputRef}
        style={[baseInputStyle, inputStyle]}
        placeholder={placeholder}
        placeholderTextColor={muted}
        value={displayValue}
        onChangeText={handleTextChange}
        accessibilityRole='search'
        {...props}
      />

      {/* Loading Indicator */}
      {loading && (
        <ActivityIndicator
          size='small'
          color={muted}
          style={{ marginRight: 4 }}
        />
      )}

      {/* Clear Button */}
      {showClear && !loading && (
        <TouchableOpacity
          onPress={handleClear}
          style={{
            backgroundColor: icon,
            padding: 4,
            borderRadius: CORNERS,
            opacity: 0.6,
          }}
          activeOpacity={0.7}
          hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
          accessibilityRole='button'
          accessibilityLabel='Clear search'
        >
          <Icon name={X} size={16} color={cardColor} strokeWidth={2} />
        </TouchableOpacity>
      )}

      {/* Right Icon */}
      {rightIcon && !showClear && !loading && rightIcon}
    </View>
  );
}

// SearchBar with suggestions dropdown
interface SearchBarWithSuggestionsProps extends SearchBarProps {
  suggestions?: string[];
  onSuggestionPress?: (suggestion: string) => void;
  maxSuggestions?: number;
  showSuggestions?: boolean;
}

export function SearchBarWithSuggestions({
  suggestions = [],
  onSuggestionPress,
  maxSuggestions = 5,
  showSuggestions = true,
  containerStyle,
  ...searchBarProps
}: SearchBarWithSuggestionsProps) {
  const [isExpanded, setIsExpanded] = useState(false);
  const cardColor = useColor('card');
  const borderColor = useColor('border');

  const filteredSuggestions = suggestions
    .filter((suggestion) =>
      suggestion
        .toLowerCase()
        .includes((searchBarProps.value || '').toLowerCase())
    )
    .slice(0, maxSuggestions);

  const shouldShowSuggestions =
    showSuggestions &&
    isExpanded &&
    filteredSuggestions.length > 0 &&
    (searchBarProps.value || '').length > 0;

  const handleSuggestionPress = (suggestion: string) => {
    onSuggestionPress?.(suggestion);
    setIsExpanded(false);
  };

  return (
    <View style={[{ width: '100%' }, containerStyle]}>
      <SearchBar
        {...searchBarProps}
        onFocus={(e) => {
          setIsExpanded(true);
          searchBarProps.onFocus?.(e);
        }}
        onBlur={(e) => {
          // Delay hiding suggestions to allow for suggestion tap
          setTimeout(() => setIsExpanded(false), 150);
          searchBarProps.onBlur?.(e);
        }}
      />

      {/* Suggestions Dropdown */}
      {shouldShowSuggestions && (
        <View
          style={{
            position: 'absolute',
            top: '100%',
            left: 0,
            right: 0,
            backgroundColor: cardColor,
            marginTop: 8,
            borderRadius: 12,
            maxHeight: 200,
            zIndex: 999,
          }}
        >
          {filteredSuggestions.map((suggestion, index) => (
            <TouchableOpacity
              key={`${suggestion}-${index}`}
              onPress={() => handleSuggestionPress(suggestion)}
              style={{
                paddingHorizontal: 16,
                paddingVertical: 12,
                borderBottomWidth:
                  index < filteredSuggestions.length - 1 ? 0.6 : 0,
                borderBottomColor: borderColor,
              }}
              activeOpacity={0.7}
            >
              <Text>{suggestion}</Text>
            </TouchableOpacity>
          ))}
        </View>
      )}
    </View>
  );
}
```

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

## Usage

```tsx
import { SearchBar, SearchBarWithSuggestions } from '@/components/ui/searchbar';
```

```tsx
<SearchBar
  placeholder='Search for anything...'
  onSearch={(query) => console.log('Searching for:', query)}
  loading={false}
/>
```

## Examples

#### Default

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

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

export function SearchBarDemo() {
  const [searchQuery, setSearchQuery] = useState('');

  const handleSearch = (query: string) => {
    console.log('Searching for:', query);
  };

  return (
    <SearchBar
      placeholder='Search for anything...'
      value={searchQuery}
      onChangeText={setSearchQuery}
      onSearch={handleSearch}
    />
  );
}
```

#### With Loading State

**Example:** Search bar with loading indicator

```tsx
// components/demo/searchbar/searchbar-loading.tsx
import { SearchBar } from '@/components/ui/searchbar';
import React, { useState } from 'react';

export function SearchBarLoading() {
  const [searchQuery, setSearchQuery] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSearch = (query: string) => {
    if (query.trim()) {
      setLoading(true);
      // Simulate API call
      setTimeout(() => {
        setLoading(false);
        console.log('Search completed for:', query);
      }, 2000);
    }
  };

  return (
    <SearchBar
      placeholder='Search with loading state...'
      value={searchQuery}
      onChangeText={setSearchQuery}
      onSearch={handleSearch}
      loading={loading}
    />
  );
}
```

#### Custom Icons

**Example:** Search bar with custom left and right icons

```tsx
// components/demo/searchbar/searchbar-icons.tsx
import { Icon } from '@/components/ui/icon';
import { SearchBar } from '@/components/ui/searchbar';
import { View } from '@/components/ui/view';
import { useColor } from '@/hooks/useColor';
import { Filter, MapPin, User } from 'lucide-react-native';
import React, { useState } from 'react';

export function SearchBarIcons() {
  const [locationQuery, setLocationQuery] = useState('');
  const [userQuery, setUserQuery] = useState('');
  const icon = useColor('icon');

  return (
    <View style={{ gap: 16 }}>
      {/* Location search with map pin icon */}
      <SearchBar
        placeholder='Search locations...'
        value={locationQuery}
        onChangeText={setLocationQuery}
        leftIcon={<Icon name={MapPin} size={16} color={icon} />}
        onSearch={(query) => console.log('Location search:', query)}
      />

      {/* User search with custom icons */}
      <SearchBar
        placeholder='Search users...'
        value={userQuery}
        onChangeText={setUserQuery}
        leftIcon={<Icon name={User} size={16} color={icon} />}
        rightIcon={<Icon name={Filter} size={16} color={icon} />}
        showClearButton={false}
        onSearch={(query) => console.log('User search:', query)}
      />
    </View>
  );
}
```

#### With Suggestions

**Example:** Search bar with dropdown suggestions

```tsx
// components/demo/searchbar/searchbar-suggestions.tsx
import { SearchBarWithSuggestions } from '@/components/ui/searchbar';
import React, { useState } from 'react';

export function SearchBarSuggestions() {
  const [searchQuery, setSearchQuery] = useState('');

  const suggestions = [
    'React Native',
    'React Navigation',
    'React Hook Form',
    'Redux Toolkit',
    'Expo Router',
    'TypeScript',
    'JavaScript',
    'Node.js',
    'Next.js',
    'Tailwind CSS',
  ];

  const handleSearch = (query: string) => {
    console.log('Searching for:', query);
  };

  const handleSuggestionPress = (suggestion: string) => {
    setSearchQuery(suggestion);
    handleSearch(suggestion);
  };

  return (
    <SearchBarWithSuggestions
      placeholder='Type to see suggestions...'
      value={searchQuery}
      onChangeText={setSearchQuery}
      onSearch={handleSearch}
      suggestions={suggestions}
      onSuggestionPress={handleSuggestionPress}
      maxSuggestions={8}
    />
  );
}
```

#### Custom Styling

**Example:** Search bar with custom styling and colors

```tsx
// components/demo/searchbar/searchbar-styled.tsx
import { SearchBar } from '@/components/ui/searchbar';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function SearchBarStyled() {
  const [query1, setQuery1] = useState('');
  const [query2, setQuery2] = useState('');
  const [query3, setQuery3] = useState('');

  return (
    <View style={{ gap: 16 }}>
      {/* Rounded with gradient-like background */}
      <SearchBar
        placeholder='Rounded search...'
        value={query1}
        onChangeText={setQuery1}
        containerStyle={{
          borderRadius: 25,
          borderWidth: 1.5,
          borderColor: 'green',
        }}
        inputStyle={{
          color: 'green',
          fontWeight: '500',
        }}
      />

      {/* Minimal flat design */}
      <SearchBar
        placeholder='Minimal search...'
        value={query2}
        onChangeText={setQuery2}
        containerStyle={{
          backgroundColor: 'transparent',
          borderBottomWidth: 1,
          borderBottomColor: '#374151',
          borderRadius: 0,
          paddingHorizontal: 0,
        }}
        inputStyle={{
          fontSize: 16,
          fontWeight: '400',
        }}
      />

      {/* Dark theme with custom height */}
      <SearchBar
        placeholder='Custom dark search...'
        value={query3}
        onChangeText={setQuery3}
        containerStyle={{
          backgroundColor: '#1f2937',
          borderRadius: 12,
          height: 56,
          borderWidth: 1,
          borderColor: '#374151',
        }}
        inputStyle={{
          color: '#f9fafb',
          fontSize: 16,
        }}
      />
    </View>
  );
}
```

#### Without Clear Button

**Example:** Search bar without the clear button

```tsx
// components/demo/searchbar/searchbar-no-clear.tsx
import { SearchBar } from '@/components/ui/searchbar';
import React, { useState } from 'react';

export function SearchBarNoClear() {
  const [searchQuery, setSearchQuery] = useState('');

  const handleSearch = (query: string) => {
    console.log('Searching without clear button:', query);
  };

  return (
    <SearchBar
      placeholder='Search without clear button...'
      value={searchQuery}
      onChangeText={setSearchQuery}
      onSearch={handleSearch}
      showClearButton={false}
    />
  );
}
```

#### Instant Search

**Example:** Search bar with no debounce for instant search

```tsx
// components/demo/searchbar/searchbar-instant.tsx
import { SearchBar } from '@/components/ui/searchbar';
import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import React, { useState } from 'react';

export function SearchBarInstant() {
  const [searchQuery, setSearchQuery] = useState('');
  const [searchResults, setSearchResults] = useState<string[]>([]);

  const mockData = [
    'Apple',
    'Banana',
    'Cherry',
    'Date',
    'Elderberry',
    'Fig',
    'Grape',
    'Honeydew',
    'Kiwi',
    'Lemon',
  ];

  const handleInstantSearch = (query: string) => {
    if (query.trim()) {
      const results = mockData.filter((item) =>
        item.toLowerCase().includes(query.toLowerCase())
      );
      setSearchResults(results);
    } else {
      setSearchResults([]);
    }
  };

  return (
    <View style={{ gap: 16 }}>
      <SearchBar
        placeholder='Instant search (no debounce)...'
        value={searchQuery}
        onChangeText={setSearchQuery}
        onSearch={handleInstantSearch}
        debounceMs={0} // No debounce for instant search
      />

      {searchResults.length > 0 && (
        <View style={{ gap: 8 }}>
          <Text variant='caption' style={{ opacity: 0.7 }}>
            Found {searchResults.length} results:
          </Text>
          {searchResults.map((result, index) => (
            <Text key={index} style={{ paddingLeft: 16 }}>
              • {result}
            </Text>
          ))}
        </View>
      )}
    </View>
  );
}
```

## API Reference

### SearchBar

The main search input component with debouncing and customization options. All other `TextInputProps` are also supported.

| Prop              | Type                       | Default     | Description                                            |
| ----------------- | -------------------------- | ----------- | ------------------------------------------------------ |
| `loading`         | `boolean`                  | `false`     | Shows loading indicator when true.                     |
| `onSearch`        | `(query: string) => void`  | -           | Callback fired when search is triggered (debounced).   |
| `onClear`         | `() => void`               | -           | Callback fired when clear button is pressed.           |
| `showClearButton` | `boolean`                  | `true`      | Whether to show the clear button when text is present. |
| `leftIcon`        | `ReactNode`                | Search icon | Custom left icon component.                            |
| `rightIcon`       | `ReactNode`                | -           | Custom right icon component.                           |
| `containerStyle`  | `ViewStyle \| ViewStyle[]` | -           | Additional styles for the container.                   |
| `inputStyle`      | `TextStyle \| TextStyle[]` | -           | Additional styles for the text input.                  |
| `debounceMs`      | `number`                   | `300`       | Debounce delay in milliseconds for search callbacks.   |
| `placeholder`     | `string`                   | 'Search...' | Placeholder text for the input.                        |
| `value`           | `string`                   | -           | Controlled value of the input.                         |
| `onChangeText`    | `(text: string) => void`   | -           | Callback fired when text changes.                      |

### SearchBarWithSuggestions

An enhanced search bar with dropdown suggestions functionality. All `SearchBar` props are also supported.

| Prop                | Type                           | Default | Description                                   |
| ------------------- | ------------------------------ | ------- | --------------------------------------------- |
| `suggestions`       | `string[]`                     | `[]`    | Array of suggestion strings to display.       |
| `onSuggestionPress` | `(suggestion: string) => void` | -       | Callback fired when a suggestion is selected. |
| `maxSuggestions`    | `number`                       | `5`     | Maximum number of suggestions to display.     |
| `showSuggestions`   | `boolean`                      | `true`  | Whether to show the suggestions dropdown.     |

## Features

- **Debounced Search**: Configurable debounce delay to prevent excessive API calls
- **Loading States**: Built-in loading indicator support
- **Clear Functionality**: Optional clear button with customizable behavior
- **Custom Icons**: Support for custom left and right icons
- **Suggestions Dropdown**: Enhanced variant with autocomplete suggestions
- **Flexible Styling**: Comprehensive styling options for container and input
- **Controlled/Uncontrolled**: Supports both controlled and uncontrolled usage patterns
- **Accessibility**: Built with accessibility best practices

## Accessibility

The SearchBar component follows accessibility guidelines:

- Proper focus management and keyboard navigation
- Screen reader compatible with appropriate labels
- Clear button is properly labeled for assistive technologies
- Suggestions dropdown supports keyboard navigation
- High contrast support for better visibility
