{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "search-field",
  "title": "Search Field",
  "description": "A native search input with controlled or uncontrolled state, clear controls, loading feedback, and accessible keyboard behavior.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/rivetui/components/search-field/search-field.tsx",
      "content": "\"use client\";\n\nimport { LoaderCircle, Search, X } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype SearchFieldProps = Omit<\n  React.ComponentProps<\"input\">,\n  \"className\" | \"defaultValue\" | \"onChange\" | \"type\" | \"value\"\n> & {\n  className?: string;\n  clearable?: boolean;\n  clearLabel?: string;\n  defaultValue?: string;\n  inputClassName?: string;\n  loading?: boolean;\n  loadingLabel?: string;\n  onClear?: () => void;\n  onValueChange?: (value: string) => void;\n  value?: string;\n};\n\nconst SearchField = React.forwardRef<HTMLInputElement, SearchFieldProps>(function SearchField(\n  {\n    autoComplete = \"off\",\n    className,\n    clearable = true,\n    clearLabel = \"Clear search\",\n    defaultValue = \"\",\n    disabled,\n    inputClassName,\n    loading = false,\n    loadingLabel = \"Searching\",\n    onClear,\n    onKeyDown,\n    onValueChange,\n    readOnly,\n    value: valueProp,\n    ...props\n  },\n  forwardedRef,\n) {\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);\n  const controlled = valueProp !== undefined;\n  const value = controlled ? valueProp : uncontrolledValue;\n  const canClear = clearable && value.length > 0 && !disabled && !readOnly;\n\n  function setValue(nextValue: string) {\n    if (!controlled) {\n      setUncontrolledValue(nextValue);\n    }\n\n    onValueChange?.(nextValue);\n  }\n\n  function clear() {\n    setValue(\"\");\n    onClear?.();\n    inputRef.current?.focus();\n  }\n\n  return (\n    <div\n      data-slot=\"search-field\"\n      data-disabled={disabled ? \"\" : undefined}\n      data-has-value={value.length > 0 ? \"\" : undefined}\n      data-loading={loading ? \"\" : undefined}\n      className={cn(\"relative w-full\", className)}\n    >\n      {loading ? (\n        <span\n          data-slot=\"search-field-spinner\"\n          className=\"pointer-events-none absolute top-1/2 left-3 z-10 size-4 -translate-y-1/2 animate-spin text-muted-foreground motion-reduce:animate-none\"\n          aria-hidden=\"true\"\n        >\n          <LoaderCircle className=\"size-4\" />\n        </span>\n      ) : (\n        <Search\n          data-slot=\"search-field-icon\"\n          className=\"pointer-events-none absolute top-1/2 left-3 z-10 size-4 -translate-y-1/2 text-muted-foreground\"\n          aria-hidden=\"true\"\n        />\n      )}\n      <input\n        {...props}\n        ref={(node) => {\n          inputRef.current = node;\n          setForwardedRef(forwardedRef, node);\n        }}\n        type=\"search\"\n        value={value}\n        autoComplete={autoComplete}\n        disabled={disabled}\n        readOnly={readOnly}\n        aria-busy={loading || undefined}\n        data-slot=\"search-field-input\"\n        className={cn(\n          \"peer h-10 w-full min-w-0 appearance-none rounded-none border border-input bg-background py-2 pr-10 pl-10 text-sm outline-none transition-[background-color,border-color,box-shadow,color] duration-100 placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground disabled:opacity-60 aria-invalid:border-destructive aria-invalid:focus-visible:border-destructive aria-invalid:focus-visible:ring-destructive/25 [&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none motion-reduce:transition-none\",\n          inputClassName,\n        )}\n        onChange={(event) => {\n          setValue(event.currentTarget.value);\n        }}\n        onKeyDown={(event) => {\n          onKeyDown?.(event);\n\n          if (!event.defaultPrevented && event.key === \"Escape\" && canClear) {\n            event.preventDefault();\n            clear();\n          }\n        }}\n      />\n      {canClear ? (\n        <button\n          type=\"button\"\n          data-slot=\"search-field-clear\"\n          aria-label={clearLabel}\n          className=\"absolute top-px right-px inline-flex h-[calc(100%-2px)] w-[2.375rem] cursor-pointer items-center justify-center border-l border-input bg-background text-muted-foreground outline-none transition-[background-color,border-color,color] duration-100 hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40 peer-aria-invalid:border-destructive motion-reduce:transition-none\"\n          onClick={clear}\n        >\n          <X className=\"size-3.5\" aria-hidden=\"true\" />\n        </button>\n      ) : null}\n      <span role=\"status\" className=\"sr-only\">\n        {loading ? loadingLabel : \"\"}\n      </span>\n    </div>\n  );\n});\n\nfunction setForwardedRef<T>(ref: React.ForwardedRef<T>, value: T | null) {\n  if (typeof ref === \"function\") {\n    ref(value);\n  } else if (ref) {\n    ref.current = value;\n  }\n}\n\nexport type { SearchFieldProps };\nexport { SearchField };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}