{"$schema":"https://ui.logic2b.com/schema/registry-item.json","name":"autocomplete","type":"registry:ui","title":"Autocomplete","description":"A free-text input with a filtered suggestion list — the editable ARIA combobox pattern. Unlike a select-style combobox it accepts arbitrary text; the list just offers matches. Fully keyboard-driven (↑/↓ move, Enter picks, Esc closes), controlled or uncontrolled, dependency-free.","registryDependencies":["utils"],"files":[{"path":"ui/autocomplete.tsx","type":"registry:ui","content":"\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface AutocompleteProps\n  extends Omit<\n    React.ComponentProps<\"input\">,\n    \"value\" | \"defaultValue\" | \"onChange\"\n  > {\n  /** The suggestion list, filtered against what the user types. */\n  options: string[]\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n  /** Shown when nothing matches. Default `\"No results.\"`. */\n  emptyMessage?: string\n}\n\n/**\n * A free-text input with a filtered suggestion list — the editable ARIA\n * combobox pattern. Unlike a select-style combobox it accepts arbitrary text;\n * the list just offers matches. Fully keyboard-driven (↑/↓ to move, Enter to\n * pick, Esc to close) and dependency-free.\n */\nfunction Autocomplete({\n  className,\n  options,\n  value,\n  defaultValue,\n  onValueChange,\n  emptyMessage = \"No results.\",\n  onFocus,\n  onBlur,\n  onKeyDown,\n  ...props\n}: AutocompleteProps) {\n  const isControlled = value !== undefined\n  const [internal, setInternal] = React.useState(defaultValue ?? \"\")\n  const query = isControlled ? (value as string) : internal\n  const [open, setOpen] = React.useState(false)\n  const [active, setActive] = React.useState(-1)\n  const reactId = React.useId()\n  const listId = `${reactId}-list`\n\n  const filtered = React.useMemo(\n    () =>\n      options.filter((o) => o.toLowerCase().includes(query.toLowerCase())),\n    [options, query]\n  )\n\n  const setQuery = (v: string) => {\n    if (!isControlled) setInternal(v)\n    onValueChange?.(v)\n  }\n\n  const choose = (v: string) => {\n    setQuery(v)\n    setOpen(false)\n    setActive(-1)\n  }\n\n  return (\n    <div data-slot=\"autocomplete\" className={cn(\"relative\", className)}>\n      <input\n        role=\"combobox\"\n        aria-expanded={open}\n        aria-controls={listId}\n        aria-autocomplete=\"list\"\n        aria-activedescendant={\n          open && active >= 0 ? `${reactId}-opt-${active}` : undefined\n        }\n        data-slot=\"autocomplete-input\"\n        value={query}\n        onChange={(e) => {\n          setQuery(e.target.value)\n          setOpen(true)\n          setActive(-1)\n        }}\n        onFocus={(e) => {\n          setOpen(true)\n          onFocus?.(e)\n        }}\n        onBlur={(e) => {\n          setOpen(false)\n          onBlur?.(e)\n        }}\n        onKeyDown={(e) => {\n          onKeyDown?.(e)\n          if (e.key === \"ArrowDown\") {\n            e.preventDefault()\n            setOpen(true)\n            setActive((i) => Math.min(i + 1, filtered.length - 1))\n          } else if (e.key === \"ArrowUp\") {\n            e.preventDefault()\n            setActive((i) => Math.max(i - 1, 0))\n          } else if (e.key === \"Enter\" && open && active >= 0) {\n            e.preventDefault()\n            choose(filtered[active])\n          } else if (e.key === \"Escape\") {\n            setOpen(false)\n            setActive(-1)\n          }\n        }}\n        className={cn(\n          \"border-input placeholder:text-muted-foreground flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none md:text-sm\",\n          \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n          \"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50\"\n        )}\n        {...props}\n      />\n      {open && (\n        <ul\n          role=\"listbox\"\n          id={listId}\n          data-slot=\"autocomplete-list\"\n          className=\"bg-popover text-popover-foreground absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border p-1 shadow-md\"\n        >\n          {filtered.length === 0 ? (\n            <li className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n              {emptyMessage}\n            </li>\n          ) : (\n            filtered.map((opt, i) => (\n              <li\n                key={opt}\n                id={`${reactId}-opt-${i}`}\n                role=\"option\"\n                aria-selected={i === active}\n                data-slot=\"autocomplete-option\"\n                onMouseDown={(e) => {\n                  // Keep focus on the input so onBlur doesn't close before the pick.\n                  e.preventDefault()\n                  choose(opt)\n                }}\n                onMouseEnter={() => setActive(i)}\n                className={cn(\n                  \"cursor-pointer rounded-sm px-2 py-1.5 text-sm outline-none\",\n                  i === active && \"bg-accent text-accent-foreground\"\n                )}\n              >\n                {opt}\n              </li>\n            ))\n          )}\n        </ul>\n      )}\n    </div>\n  )\n}\n\nexport { Autocomplete }\n"}],"categories":["form"],"accessibility":{"support":"authored","pattern":"editable combobox","keyboard":[{"keys":["ArrowUp","ArrowDown"],"action":"Move the active suggestion."},{"keys":["Enter"],"action":"Choose the active suggestion."},{"keys":["Escape"],"action":"Close the open surface and restore focus."}],"aria":["Uses combobox/listbox/option semantics with aria-expanded, aria-controls and aria-activedescendant."],"consumer":["Provide a label and ensure arbitrary typed text is valid for the use case."]},"api":{"source":"src/ui/autocomplete.tsx","exports":[{"name":"AutocompleteProps","kind":"type","definition":"interface AutocompleteProps extends Omit< React.ComponentProps<\"input\">, \"value\" | \"defaultValue\" | \"onChange\" > { /** The suggestion list, filtered against what the user types. */ options: string[] value?: string defaultValue?: string onValueChange?: (value: string) => void /** Shown when nothing matches. Default `\"No results.\"`. */ emptyMessage?: string }"},{"name":"Autocomplete","kind":"component","description":"A free-text input with a filtered suggestion list — the editable ARIA combobox pattern. Unlike a select-style combobox it accepts arbitrary text; the list just offers matches. Fully keyboard-driven (↑/↓ to move, Enter to pick, Esc to close) and dependency-free.","propsType":"AutocompleteProps","props":[{"name":"options","type":"string[]","required":true,"description":"The suggestion list, filtered against what the user types."},{"name":"value","type":"string","required":false},{"name":"defaultValue","type":"string","required":false},{"name":"onValueChange","type":"(value: string) => void","required":false},{"name":"emptyMessage","type":"string","required":false,"default":"\"No results.\"","description":"Shown when nothing matches. Default `\"No results.\"`."}]}]}}