{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "search-field",
  "title": "Search Field",
  "description": "Clearable search with recent queries, loading, empty, and error feedback.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "input",
    "label",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/search-field.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { LoaderCircle, Search, X } from \"lucide-react\";\nimport { type ComponentProps, createContext, useContext, useId, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const SEARCH_FIELD_VARIANTS = [\"rounded\", \"pill\", \"compact\"] as const;\nexport type SearchFieldVariant = (typeof SEARCH_FIELD_VARIANTS)[number];\nexport type SearchFieldStatus = \"idle\" | \"loading\" | \"empty\" | \"error\";\nexport type SearchFieldProps = ComponentProps<\"div\"> & {\n  variant?: SearchFieldVariant;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string) => void;\n  status?: SearchFieldStatus;\n  disabled?: boolean;\n};\ntype SearchContext = {\n  id: string;\n  value: string;\n  change: (value: string) => void;\n  focus: () => void;\n  inputRef: React.RefObject<HTMLInputElement | null>;\n  status: SearchFieldStatus;\n  disabled: boolean;\n  variant: SearchFieldVariant;\n};\nconst Context = createContext<SearchContext | null>(null);\nconst searchFieldVariants = cva(\"grid min-w-0 gap-2 text-[13px]/[18px] text-foreground\", {\n  variants: { variant: { rounded: \"\", pill: \"\", compact: \"\" } },\n});\nfunction useSearch() {\n  const context = useContext(Context);\n  if (!context) throw new Error(\"SearchField children must be used within SearchField\");\n  return context;\n}\nexport function SearchField({\n  variant = \"rounded\",\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  status = \"idle\",\n  disabled = false,\n  children,\n  className,\n  ...props\n}: SearchFieldProps) {\n  const id = useId();\n  const inputRef = useRef<HTMLInputElement>(null);\n  const [internal, setInternal] = useState(defaultValue);\n  const context: SearchContext = {\n    id,\n    inputRef,\n    value: value ?? internal,\n    status,\n    disabled,\n    variant,\n    focus: () => inputRef.current?.focus(),\n    change: (next) => {\n      if (disabled) return;\n      if (value === undefined) setInternal(next);\n      onValueChange?.(next);\n    },\n  };\n  return (\n    <Context.Provider value={context}>\n      <div\n        data-slot=\"search-field\"\n        className={cn(searchFieldVariants({ variant }), className)}\n        {...props}\n        data-variant={variant}\n      >\n        {children}\n      </div>\n    </Context.Provider>\n  );\n}\nexport function SearchFieldLabel({ children, className, ...props }: ComponentProps<\"label\">) {\n  const context = useSearch();\n  return (\n    <Label\n      data-slot=\"search-field-label\"\n      className={cn(\n        \"block text-[length:inherit] leading-[inherit] font-medium select-auto\",\n        className,\n      )}\n      {...props}\n      htmlFor={context.id}\n    >\n      {children}\n    </Label>\n  );\n}\nexport function SearchFieldControl({ className, children, ...props }: ComponentProps<\"div\">) {\n  const context = useSearch();\n  const compact = context.variant === \"compact\";\n  return (\n    <div\n      data-slot=\"search-field-control\"\n      className={cn(\n        \"flex items-center border bg-background text-subtle-foreground transition-[border-color,box-shadow,color] duration-120 ease-[ease-out] hover:border-border-strong focus-within:border-border-strong focus-within:text-muted-foreground focus-within:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_24%,transparent)] data-[status=error]:border-destructive/70 motion-reduce:transition-none\",\n        compact ? \"gap-1.5 py-0.5 pr-1 pl-2.5\" : \"gap-2 py-1 pr-1.5 pl-3\",\n        context.variant === \"pill\" ? \"rounded-full\" : compact ? \"rounded-lg\" : \"rounded-[10px]\",\n        context.disabled ? \"opacity-55\" : \"opacity-100\",\n        className,\n      )}\n      {...props}\n      data-status={context.status}\n    >\n      <span\n        key={context.status === \"loading\" ? \"loading\" : \"idle\"}\n        className=\"inline-flex shrink-0 animate-in fade-in-0 zoom-in-75 duration-150 ease-out-quint motion-reduce:animate-none\"\n      >\n        {context.status === \"loading\" ? (\n          <LoaderCircle\n            className=\"animate-[spin_0.8s_linear_infinite] motion-reduce:animate-none\"\n            size={compact ? 14 : 16}\n            strokeWidth={1.75}\n            aria-hidden=\"true\"\n          />\n        ) : (\n          <Search size={compact ? 14 : 16} strokeWidth={1.75} aria-hidden=\"true\" />\n        )}\n      </span>\n      {children}\n    </div>\n  );\n}\nexport function SearchFieldInput({\n  className,\n  onChange,\n  onKeyDown,\n  ...props\n}: Omit<ComponentProps<\"input\">, \"value\" | \"defaultValue\" | \"id\" | \"disabled\">) {\n  const context = useSearch();\n  return (\n    <Input\n      type=\"search\"\n      data-slot=\"search-field-input\"\n      className={cn(\n        \"h-auto w-full min-w-0 flex-1 rounded-none border-0 bg-transparent p-0 text-foreground shadow-none outline-none placeholder:text-subtle-foreground focus-visible:ring-0 disabled:cursor-default disabled:opacity-100 dark:bg-transparent [&::-webkit-search-cancel-button]:appearance-none\",\n        context.variant === \"compact\"\n          ? \"text-[12.5px]/6 md:text-[12.5px]/6\"\n          : \"text-[13px]/7 md:text-[13px]/7\",\n        className,\n      )}\n      {...props}\n      id={context.id}\n      ref={context.inputRef}\n      value={context.value}\n      disabled={context.disabled}\n      aria-busy={context.status === \"loading\"}\n      aria-invalid={context.status === \"error\" || undefined}\n      aria-describedby={`${context.id}-message`}\n      onChange={(event) => {\n        onChange?.(event);\n        if (!event.defaultPrevented) context.change(event.target.value);\n      }}\n      onKeyDown={(event) => {\n        onKeyDown?.(event);\n        if (!event.defaultPrevented && event.key === \"Escape\") {\n          event.preventDefault();\n          context.change(\"\");\n        }\n      }}\n    />\n  );\n}\nexport function SearchFieldClear({\n  children = <X size={14} className=\"size-3.5\" strokeWidth={1.75} aria-hidden=\"true\" />,\n  onClick,\n  className,\n  ...props\n}: ComponentProps<\"button\">) {\n  const context = useSearch();\n  if (!context.value) return null;\n  return (\n    <Button\n      aria-label=\"Clear search\"\n      data-slot=\"search-field-clear\"\n      variant=\"ghost\"\n      size=\"icon\"\n      className={cn(\n        \"grid shrink-0 cursor-pointer animate-in place-items-center border-0 bg-transparent p-0 text-subtle-foreground duration-180 ease-[cubic-bezier(0.16,1,0.3,1)] [transition:background-color_120ms_ease-out,color_120ms_ease-out,scale_140ms_var(--ease-out-quint)] fade-in-0 zoom-in-80 hover:bg-accent hover:text-foreground focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid active:scale-[0.92] disabled:opacity-100 motion-reduce:animate-none motion-reduce:transition-none dark:hover:bg-accent [&_svg:not([class*='size-'])]:size-3.5\",\n        context.variant === \"compact\" ? \"size-6\" : \"size-7\",\n        context.variant === \"pill\" ? \"rounded-full\" : \"rounded-lg\",\n        className,\n      )}\n      {...props}\n      type=\"button\"\n      disabled={context.disabled || props.disabled}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) {\n          context.change(\"\");\n          context.focus();\n        }\n      }}\n    >\n      {children}\n    </Button>\n  );\n}\nexport function SearchFieldMessage({ children, className, ...props }: ComponentProps<\"p\">) {\n  const context = useSearch();\n  const copy = {\n    idle: \"\",\n    loading: \"Searching…\",\n    empty: \"No results found. Try another search.\",\n    error: \"Search failed. Please try again.\",\n  };\n  return (\n    <p\n      data-slot=\"search-field-message\"\n      className={cn(\n        \"m-0 text-xs/4\",\n        context.status === \"error\" ? \"text-destructive-ink\" : \"text-subtle-foreground\",\n        className,\n      )}\n      {...props}\n      id={`${context.id}-message`}\n      role={context.status === \"error\" ? \"alert\" : \"status\"}\n    >\n      {children ?? copy[context.status]}\n    </p>\n  );\n}\nexport function SearchFieldRecent({ className, ...props }: ComponentProps<\"fieldset\">) {\n  const context = useSearch();\n  if (context.value || context.status !== \"idle\") return null;\n  return (\n    <fieldset\n      aria-label=\"Recent searches\"\n      data-slot=\"search-field-recent\"\n      className={cn(\"m-0 flex min-w-0 flex-wrap gap-1.5 border-0 p-0\", className)}\n      {...props}\n    />\n  );\n}\nexport function SearchFieldRecentItem({\n  value,\n  children,\n  onClick,\n  className,\n  ...props\n}: Omit<ComponentProps<\"button\">, \"value\"> & { value: string }) {\n  const context = useSearch();\n  return (\n    <Button\n      data-slot=\"search-field-recent-item\"\n      variant=\"secondary\"\n      size=\"sm\"\n      className={cn(\n        \"cursor-pointer gap-1.5 rounded-full border-0 bg-secondary px-3 py-0 text-[12.5px] font-medium text-muted-foreground [transition:background-color_120ms_ease-out,color_120ms_ease-out,scale_140ms_var(--ease-out-quint)] hover:bg-[color-mix(in_oklab,var(--secondary)_85%,var(--foreground))] hover:text-foreground focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid active:scale-[0.97] has-[>svg]:px-3 motion-reduce:transition-none\",\n        context.variant === \"compact\" ? \"h-6\" : \"h-7\",\n        className,\n      )}\n      {...props}\n      type=\"button\"\n      disabled={context.disabled || props.disabled}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) {\n          context.change(value);\n          context.focus();\n        }\n      }}\n    >\n      {children ?? value}\n    </Button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/search-field.tsx"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}