{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "search-input",
  "title": "Search Input",
  "description": "Search field with a leading magnifier icon and a trailing clear (✕) button that appears as soon as there is text, empties the field, and puts focus back so typing can continue. Use it above filterable lists and data tables, in sidebars and settings pages, over dropdown and combobox options, for docs and help search, for admin record lookup, and anywhere a \"/\" shortcut focuses a search box. Common asks it answers: \"search input\", \"search bar\", \"search box\", \"filter input\", \"clearable input\", \"input with a clear button\", \"search field with icon\", \"type to filter a list\", \"table search box\", \"searchbar component\". shadcn/ui has no search field: its input is a bare styled <input>, and its input-group is a layout kit of six parts (InputGroup, InputGroupAddon, InputGroupButton, InputGroupText, InputGroupInput, InputGroupTextarea) that pulls in button, input and textarea and hands you slots to hang your own icon and clear control in — you still write the clear button, the show-it-only-when-there-is-text rule, the refocus, and the event plumbing. This is that already assembled, in one import. The part that is easy to get wrong is clearing. Assigning to the input's value does not make React's onChange fire, so a hand-rolled clear button empties the box while the list behind it stays filtered on the old query. This writes through the native value setter and dispatches a bubbling input event, so onChange fires for controlled and uncontrolled usage alike and whatever filtering it drives actually updates. It also hides the WebKit search-cancel button so there is not a second ✕ beside the first, keeps the icon out of the accessibility tree and out of pointer events, gives the clear control a screen-reader label, and passes only one of value/defaultValue through so React never warns about a field switching between controlled and uncontrolled. The clear button is deliberately left out of the tab order, so Tab moves on to the next field instead of into a control that duplicates select-all-and-delete. Standard input props and a forwarded ref pass straight through, so a \"/\" hotkey can focus it. Theme-aware via shadcn tokens; depends only on lucide-react.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/ui/search-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Search, X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface SearchInputProps extends React.ComponentPropsWithoutRef<\"input\"> {\n  /** Called after the clear (✕) button empties the field. */\n  onClear?: () => void\n}\n\nexport const SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(\n  function SearchInput(\n    { className, onClear, onChange, value, defaultValue, disabled, ...props },\n    forwardedRef\n  ) {\n    const innerRef = React.useRef<HTMLInputElement>(null)\n    React.useImperativeHandle(\n      forwardedRef,\n      () => innerRef.current as HTMLInputElement\n    )\n\n    const isControlled = value !== undefined\n    const [hasValue, setHasValue] = React.useState(\n      () => String((isControlled ? value : defaultValue) ?? \"\").length > 0\n    )\n\n    // Keep the clear button's visibility in sync when the value is controlled.\n    React.useEffect(() => {\n      if (isControlled) setHasValue(String(value ?? \"\").length > 0)\n    }, [isControlled, value])\n\n    function handleChange(e: React.ChangeEvent<HTMLInputElement>) {\n      setHasValue(e.target.value.length > 0)\n      onChange?.(e)\n    }\n\n    function handleClear() {\n      const input = innerRef.current\n      if (input) {\n        // Use the prototype's value setter so React's onChange fires for both\n        // controlled and uncontrolled inputs (lets list/table filtering update),\n        // then refocus so the user can keep typing.\n        const setter = Object.getOwnPropertyDescriptor(\n          window.HTMLInputElement.prototype,\n          \"value\"\n        )?.set\n        setter?.call(input, \"\")\n        input.dispatchEvent(new Event(\"input\", { bubbles: true }))\n        input.focus()\n      }\n      setHasValue(false)\n      onClear?.()\n    }\n\n    // Only one of value/defaultValue is ever passed to the input so React never\n    // warns about switching between controlled and uncontrolled.\n    const controlledProps = isControlled ? { value } : { defaultValue }\n\n    return (\n      <div className=\"relative\">\n        <Search\n          className=\"pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground\"\n          aria-hidden=\"true\"\n        />\n        <input\n          ref={innerRef}\n          type=\"search\"\n          disabled={disabled}\n          onChange={handleChange}\n          className={cn(\n            \"flex h-9 w-full rounded-md border border-input bg-transparent py-1 pl-8 pr-9 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&::-webkit-search-cancel-button]:appearance-none\",\n            className\n          )}\n          {...controlledProps}\n          {...props}\n        />\n        {hasValue && !disabled ? (\n          <button\n            type=\"button\"\n            onClick={handleClear}\n            aria-label=\"Clear search\"\n            tabIndex={-1}\n            className=\"absolute right-1 top-1/2 inline-flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n          >\n            <X className=\"h-4 w-4\" aria-hidden=\"true\" />\n          </button>\n        ) : null}\n      </div>\n    )\n  }\n)\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}