import React, { forwardRef, useState, useEffect, useId } from 'react'; import { cn } from '../../utils/cn'; export interface SearchInputProps extends Omit, 'onChange'| 'size'> { /** * Current search value */ value?: string; /** * Callback when value changes */ onChange?: (e: React.ChangeEvent) => void; /** * Callback when clear button is clicked */ onClear?: () => void; /** * Debounce time in milliseconds * @default 300 */ debounceMs?: number; /** * Search icon placement * @default 'left' */ iconPosition?: 'left' | 'right' | 'none'; /** * Size of the input * @default 'md' */ size?: 'sm' | 'md' | 'lg'; /** * Visual variant * @default 'default' */ variant?: 'default' | 'filled' | 'outline' | 'minimal'; /** * Will the input fill its container * @default false */ fullWidth?: boolean; /** * Custom class for the container */ containerClassName?: string; /** * Placeholder when empty * @default 'Search...' */ placeholder?: string; /** * Custom search icon */ searchIcon?: React.ReactNode; /** * Custom clear icon */ clearIcon?: React.ReactNode; /** * Auto focus the input on mount * @default false */ autoFocus?: boolean; /** * Whether to show the clear button * @default true */ showClearButton?: boolean; } export const SearchInput = forwardRef( ( { value: propValue = '', onChange, onClear, debounceMs = 300, iconPosition = 'left', size = 'md', variant = 'default', fullWidth = false, className, containerClassName, placeholder = 'Search...', searchIcon, clearIcon, autoFocus = false, showClearButton = true, disabled, id, ...props }, ref ) => { // Local state for debounced input const [localValue, setLocalValue] = useState(propValue); // Use React's useId hook for stable ID generation const generatedId = useId(); const searchId = id || `search-${generatedId}`; // Update local state when prop value changes useEffect(() => { setLocalValue(propValue); }, [propValue]); // Handle debounced input changes useEffect(() => { if (localValue === propValue) return; const timeoutId = setTimeout(() => { const event = { target: { value: localValue }, } as React.ChangeEvent; onChange?.(event); }, debounceMs); return () => clearTimeout(timeoutId); }, [localValue, onChange, debounceMs, propValue]); // Handle input change const handleChange = (e: React.ChangeEvent) => { setLocalValue(e.target.value); }; // Handle clearing the input const handleClear = () => { setLocalValue(''); onClear?.(); }; // Default search icon const defaultSearchIcon = ( ); // Default clear icon const defaultClearIcon = ( ); return (
{/* Search input */}
{/* Search icon (left position) */} {iconPosition === 'left' && (
{searchIcon || defaultSearchIcon}
)} {/* Search icon (right position) */} {iconPosition === 'right' && (
{searchIcon || defaultSearchIcon}
)} {/* Clear button */} {showClearButton && localValue && ( )}
); } ); SearchInput.displayName = 'SearchInput'; export default SearchInput;