{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"autocomplete","type":"registry:ui","title":"Autocomplete","description":"Autocomplete from react-fancy","package":"react-fancy","dependencies":[],"registryDependencies":["portal","inputs"],"files":[{"path":"components\/fancy\/autocomplete\/Autocomplete.tsx","content":"import { forwardRef, useCallback, useEffect, useRef, useState } from \"react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { Portal } from \"..\/Portal\";\r\nimport { useControllableState } from \"..\/..\/hooks\/use-controllable-state\";\r\nimport { useFloatingPosition } from \"..\/..\/hooks\/use-floating-position\";\r\nimport { useOutsideClick } from \"..\/..\/hooks\/use-outside-click\";\r\nimport { useEscapeKey } from \"..\/..\/hooks\/use-escape-key\";\r\nimport { useFieldMode } from \"..\/inputs\/mode\/FieldMode.context\";\r\nimport { useInlineEdit } from \"..\/inputs\/mode\/useInlineEdit\";\r\nimport type { AutocompleteProps } from \".\/Autocomplete.types\";\r\n\r\nexport const Autocomplete = forwardRef<HTMLInputElement, AutocompleteProps>(\r\n  function Autocomplete(\r\n    {\r\n      value: controlledValue,\r\n      defaultValue = \"\",\r\n      onChange,\r\n      onSearch,\r\n      options,\r\n      placeholder,\r\n      loading = false,\r\n      emptyMessage = \"No results found.\",\r\n      disabled = false,\r\n      className,\r\n      mode,\r\n    },\r\n    ref,\r\n  ) {\r\n    const [value, setValue] = useControllableState(\r\n      controlledValue,\r\n      defaultValue,\r\n      onChange,\r\n    );\r\n    const resolvedMode = useFieldMode(mode);\r\n    const { showControl, interactive, enterEdit, exitEdit } = useInlineEdit(resolvedMode, disabled);\r\n    const [query, setQuery] = useState(value);\r\n    const [open, setOpen] = useState(false);\r\n    const [activeIndex, setActiveIndex] = useState(-1);\r\n\r\n    const anchorRef = useRef<HTMLInputElement>(null);\r\n    const listRef = useRef<HTMLDivElement>(null);\r\n    const wrapperRef = useRef<HTMLDivElement>(null);\r\n\r\n    const position = useFloatingPosition(anchorRef, listRef, {\r\n      placement: \"bottom-start\",\r\n      offset: 4,\r\n      enabled: open,\r\n    });\r\n\r\n    const close = useCallback(() => {\r\n      setOpen(false);\r\n      setActiveIndex(-1);\r\n    }, []);\r\n\r\n    useOutsideClick(wrapperRef, close, open);\r\n    useEscapeKey(close, open);\r\n\r\n    const filtered = options.filter((o) =>\r\n      o.label.toLowerCase().includes(query.toLowerCase()),\r\n    );\r\n\r\n    const select = useCallback(\r\n      (val: string) => {\r\n        setValue(val);\r\n        const opt = options.find((o) => o.value === val);\r\n        setQuery(opt?.label ?? val);\r\n        close();\r\n      },\r\n      [setValue, options, close],\r\n    );\r\n\r\n    useEffect(() => {\r\n      onSearch?.(query);\r\n    }, [query, onSearch]);\r\n\r\n    const handleKeyDown = (e: React.KeyboardEvent) => {\r\n      if (!open) {\r\n        if (e.key === \"ArrowDown\") {\r\n          setOpen(true);\r\n          return;\r\n        }\r\n        return;\r\n      }\r\n\r\n      if (e.key === \"ArrowDown\") {\r\n        e.preventDefault();\r\n        setActiveIndex((i) => Math.min(i + 1, filtered.length - 1));\r\n      } else if (e.key === \"ArrowUp\") {\r\n        e.preventDefault();\r\n        setActiveIndex((i) => Math.max(i - 1, 0));\r\n      } else if (e.key === \"Enter\" && activeIndex >= 0) {\r\n        e.preventDefault();\r\n        const item = filtered[activeIndex];\r\n        if (item && !item.disabled) select(item.value);\r\n      }\r\n    };\r\n\r\n    if (!showControl) {\r\n      const matched = options.find((o) => o.value === value);\r\n      return (\r\n        <div\r\n          data-react-fancy-autocomplete=\"\"\r\n          data-mode=\"view\"\r\n          ref={wrapperRef}\r\n          role={interactive ? \"button\" : undefined}\r\n          tabIndex={interactive ? 0 : undefined}\r\n          title={interactive ? \"Click to edit\" : undefined}\r\n          onClick={interactive ? enterEdit : undefined}\r\n          onKeyDown={\r\n            interactive\r\n              ? (e) => {\r\n                  if (e.key === \"Enter\" || e.key === \" \") {\r\n                    e.preventDefault();\r\n                    enterEdit();\r\n                  }\r\n                }\r\n              : undefined\r\n          }\r\n          className={cn(\r\n            \"text-sm text-zinc-900 dark:text-zinc-100\",\r\n            !value && \"text-zinc-400 dark:text-zinc-500\",\r\n            interactive &&\r\n              \"cursor-pointer rounded-md outline-none transition-colors hover:bg-zinc-100 focus-visible:ring-2 focus-visible:ring-blue-500\/40 dark:hover:bg-zinc-800\",\r\n            className,\r\n          )}\r\n        >\r\n          {matched?.label ?? value ?? \"\u2014\"}\r\n        <\/div>\r\n      );\r\n    }\r\n\r\n    return (\r\n      <div\r\n        data-react-fancy-autocomplete=\"\"\r\n        ref={wrapperRef}\r\n        className={cn(\"relative\", className)}\r\n        onBlur={(e) => {\r\n          \/\/ Options live in a Portal; only exit when closed and focus left.\r\n          if (interactive && !open && !e.currentTarget.contains(e.relatedTarget as Node)) {\r\n            exitEdit();\r\n          }\r\n        }}\r\n      >\r\n        <input\r\n          ref={(node) => {\r\n            (anchorRef as React.MutableRefObject<HTMLInputElement | null>).current = node;\r\n            if (typeof ref === \"function\") ref(node);\r\n            else if (ref) (ref as React.MutableRefObject<HTMLInputElement | null>).current = node;\r\n          }}\r\n          type=\"text\"\r\n          value={query}\r\n          onChange={(e) => {\r\n            setQuery(e.target.value);\r\n            setOpen(true);\r\n            setActiveIndex(-1);\r\n          }}\r\n          onFocus={() => setOpen(true)}\r\n          onKeyDown={handleKeyDown}\r\n          placeholder={placeholder}\r\n          disabled={disabled}\r\n          autoFocus={interactive}\r\n          role=\"combobox\"\r\n          aria-expanded={open}\r\n          aria-autocomplete=\"list\"\r\n          className=\"w-full rounded-lg border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-900 placeholder:text-zinc-400 outline-none transition-[border-color,box-shadow] duration-150 focus:border-blue-500 focus:ring-2 focus:ring-blue-500\/40 dark:border-zinc-700 dark:bg-[#1e1e24] dark:text-zinc-100 dark:placeholder:text-zinc-500 dark:focus:border-blue-400 dark:focus:ring-blue-400\/20\"\r\n        \/>\r\n        {open && (\r\n          <Portal>\r\n            <div\r\n              ref={listRef}\r\n              role=\"listbox\"\r\n              className=\"fixed z-50 max-h-60 min-w-[8rem] overflow-y-auto rounded-xl border border-zinc-200 bg-white p-1 shadow-lg dark:border-zinc-700 dark:bg-zinc-900 dark:shadow-zinc-950\/50 fancy-scale-in\"\r\n              style={{\r\n                left: position.x,\r\n                top: position.y,\r\n                width: anchorRef.current?.offsetWidth,\r\n              }}\r\n            >\r\n              {loading ? (\r\n                <div className=\"px-3 py-2 text-sm text-zinc-400\">Loading...<\/div>\r\n              ) : filtered.length === 0 ? (\r\n                <div className=\"px-3 py-2 text-sm text-zinc-400\">\r\n                  {emptyMessage}\r\n                <\/div>\r\n              ) : (\r\n                filtered.map((option, i) => (\r\n                  <button\r\n                    key={option.value}\r\n                    type=\"button\"\r\n                    role=\"option\"\r\n                    aria-selected={i === activeIndex}\r\n                    disabled={option.disabled}\r\n                    onClick={() => select(option.value)}\r\n                    className={cn(\r\n                      \"flex w-full items-center rounded-lg px-3 py-2 text-left text-sm text-zinc-700 transition-colors dark:text-zinc-200\",\r\n                      i === activeIndex\r\n                        ? \"bg-zinc-100 dark:bg-zinc-800\"\r\n                        : \"hover:bg-zinc-50 dark:hover:bg-zinc-800\/50\",\r\n                      option.disabled && \"cursor-not-allowed opacity-50\",\r\n                    )}\r\n                  >\r\n                    <HighlightMatch text={option.label} query={query} \/>\r\n                  <\/button>\r\n                ))\r\n              )}\r\n            <\/div>\r\n          <\/Portal>\r\n        )}\r\n      <\/div>\r\n    );\r\n  },\r\n);\r\n\r\nfunction HighlightMatch({ text, query }: { text: string; query: string }) {\r\n  if (!query) return <>{text}<\/>;\r\n\r\n  const idx = text.toLowerCase().indexOf(query.toLowerCase());\r\n  if (idx === -1) return <>{text}<\/>;\r\n\r\n  return (\r\n    <>\r\n      {text.slice(0, idx)}\r\n      <span className=\"font-semibold text-zinc-900 dark:text-white\">\r\n        {text.slice(idx, idx + query.length)}\r\n      <\/span>\r\n      {text.slice(idx + query.length)}\r\n    <\/>\r\n  );\r\n}\r\n\r\nAutocomplete.displayName = \"Autocomplete\";\r\n","type":"registry:ui","target":"components\/fancy\/autocomplete\/Autocomplete.tsx"},{"path":"components\/fancy\/autocomplete\/Autocomplete.types.ts","content":"import type { ReactNode } from \"react\";\r\nimport type { FieldMode } from \"..\/inputs\/inputs.types\";\r\n\r\nexport interface AutocompleteOption {\r\n  value: string;\r\n  label: string;\r\n  disabled?: boolean;\r\n}\r\n\r\nexport interface AutocompleteProps {\r\n  value?: string;\r\n  defaultValue?: string;\r\n  onChange?: (value: string) => void;\r\n  onSearch?: (query: string) => void;\r\n  options: AutocompleteOption[];\r\n  placeholder?: string;\r\n  loading?: boolean;\r\n  emptyMessage?: ReactNode;\r\n  disabled?: boolean;\r\n  className?: string;\r\n  \/** `\"edit\"` (default) renders the control; `\"view\"` renders the value as text. *\/\r\n  mode?: FieldMode;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/autocomplete\/Autocomplete.types.ts"},{"path":"components\/fancy\/autocomplete\/index.ts","content":"export { Autocomplete } from \".\/Autocomplete\";\r\nexport type { AutocompleteProps, AutocompleteOption } from \".\/Autocomplete.types\";\r\n","type":"registry:ui","target":"components\/fancy\/autocomplete\/index.ts"}]}