{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "autocomplete-loading",
  "type": "registry:ui",
  "title": "Autocomplete Loading",
  "description": "Debounced async Autocomplete with a loading indicator and race-safe request handling.",
  "dependencies": [
    "@mantine/core@^9"
  ],
  "docs": "Adapted from Mantine UI AutocompleteLoading at ffbf61c559f374a7ea28fcf00355e84dcbe9a908; the fake setTimeout fetch became a loadOptions prop, the pending timeout is now cleared on unmount, requests are race-safe via a monotonic request id, and a pass-through filter was added so a real search's results aren't silently re-filtered client-side.",
  "registryDependencies": [
    "@house/mantine-ui-license"
  ],
  "files": [
    {
      "path": "registry/mantine-ui/autocomplete-loading/autocomplete-loading.tsx",
      "type": "registry:ui",
      "content": "/**\n * Adapted from Mantine UI's AutocompleteLoading at\n * ffbf61c559f374a7ea28fcf00355e84dcbe9a908. MIT licensed; see\n * LICENSES/MANTINE-UI.txt after installation.\n *\n * Controlled/uncontrolled contract (settled across floating-label-input,\n * password-strength, autocomplete-loading — the tranche's three form\n * controls): whether the public `value`/`onChange` pair is event-based or\n * value-based is decided by the props surface, not by preference.\n *   - Drop-in wrappers — `Props extends Omit<XProps, ...>` with X's own ref\n *     type, rendering nothing but `<X {...} />` — keep X's exact contract,\n *     because a consumer swapping `<X>` for the wrapper shouldn't have to\n *     rewrite their handler. TextInput/PasswordInput/Textarea are\n *     event-based (`ChangeEventHandler<HTMLInputElement>`);\n *     Autocomplete/Select and the rest of the combobox family are\n *     value-based (`(value: string) => void`).\n *   - Composite components that own their own props surface (their own\n *     BoxProps/ElementProps, their own ref) aren't a drop-in for any single\n *     base, so they expose the simple value-based contract instead.\n * Both controlled and uncontrolled work everywhere. Event-based drop-ins\n * hand-roll `useState` + `isControlled = value !== undefined`, because\n * `useUncontrolled`'s onChange payload type is tied to the tracked value\n * type and can't emit a raw DOM event. Everything else uses\n * `@mantine/hooks`' `useUncontrolled`.\n *\n * This component is a drop-in for Autocomplete (it renders nothing but\n * `<Autocomplete {...} />`, and its props extend\n * `Omit<AutocompleteProps, ...>`): value-based onChange, matching\n * Autocomplete's own. Controlled/uncontrolled state is hand-rolled here\n * rather than routed through `useUncontrolled`, so this item's `npm` list\n * doesn't gain a `@mantine/hooks` dependency the registry entry doesn't\n * already declare.\n */\n\nimport { Autocomplete, type AutocompleteProps, Loader } from \"@mantine/core\";\nimport { useEffect, useRef, useState } from \"react\";\n\nconst DEFAULT_EMAIL_DOMAINS = [\"gmail.com\", \"outlook.com\", \"yahoo.com\"];\n\n/**\n * Demo-only source used when the consumer doesn't supply `loadOptions`: it\n * fakes a short round trip and suggests the query at three common email\n * domains, so the component is useful to look at with zero configuration.\n * Replace it with a real request in production.\n */\nasync function defaultLoadOptions(query: string): Promise<string[]> {\n  await new Promise((resolve) => window.setTimeout(resolve, 300));\n  return DEFAULT_EMAIL_DOMAINS.map((domain) => `${query}@${domain}`);\n}\n\nfunction defaultShouldQuery(query: string): boolean {\n  return query.trim().length > 0;\n}\n\nexport interface AutocompleteLoadingProps\n  extends Omit<AutocompleteProps, \"data\" | \"value\" | \"defaultValue\" | \"onChange\" | \"rightSection\"> {\n  /**\n   * Controlled value. When provided, this component is fully controlled and\n   * `onChange` is the only way its displayed value changes. Composes with\n   * the async `loadOptions` flow: an external write to `value` cancels any\n   * pending debounce/request for whatever was last typed rather than\n   * letting a stale response land against the new value (see the\n   * cancel-only effect below). Ordinary controlled-input semantics apply:\n   * if the consumer doesn't write the typed value back through `onChange`,\n   * the displayed text snaps back to `value` even though the query for\n   * what was typed still ran to completion — that's expected, not a bug in\n   * this component.\n   */\n  value?: string;\n  /** Initial value for uncontrolled usage. Ignored once `value` is provided. @default '' */\n  defaultValue?: string;\n  /** Called on every keystroke with the raw input value, not debounced. */\n  onChange?: (value: string) => void;\n  /** Delay, in milliseconds, after typing stops before `loadOptions` runs. @default 1000 */\n  debounceMs?: number;\n  /**\n   * Fetches suggestions for the current query. Receives the raw input value\n   * and resolves to the options to display. Defaults to a demo implementation\n   * that suggests the query at common email domains; pass your own to query a\n   * real source.\n   */\n  loadOptions?: (query: string) => Promise<string[]>;\n  /**\n   * Decides whether `loadOptions` should run for a given query, e.g. to skip\n   * an empty value or one that's already a complete answer. Returning `false`\n   * clears any pending request and turns the loader off.\n   * @default (query) => query.trim().length > 0\n   */\n  shouldQuery?: (query: string) => boolean;\n}\n\n// `loadOptions` results are already the server's answer, so the dropdown\n// shouldn't re-filter them against the query with Mantine's default\n// substring match — a real search (typo-tolerant, ranked, whatever) can\n// legitimately return options that don't literally contain the query text.\nfunction passthroughFilter<T>({ options }: { options: T }): T {\n  return options;\n}\n\nexport function AutocompleteLoading({\n  value,\n  defaultValue = \"\",\n  onChange,\n  debounceMs = 1000,\n  loadOptions = defaultLoadOptions,\n  shouldQuery = defaultShouldQuery,\n  filter = passthroughFilter,\n  label = \"Async Autocomplete data\",\n  placeholder = \"Your email\",\n  ...others\n}: AutocompleteLoadingProps) {\n  const isControlled = value !== undefined;\n  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);\n  const currentValue = isControlled ? value : uncontrolledValue;\n\n  const [loading, setLoading] = useState(false);\n  const [options, setOptions] = useState<string[]>([]);\n\n  const timeoutRef = useRef<number | undefined>(undefined);\n  // Bumped on every query attempt (including skipped ones) so a response from\n  // an earlier, slower request can never overwrite a later one.\n  const requestIdRef = useRef(0);\n  // The value the *handler* last saw, kept separate from `currentValue` so\n  // the cancel-only effect below can tell \"the parent just wrote a new\n  // controlled value out-of-band\" apart from \"the handler already dealt\n  // with this value\" (which would otherwise re-fire on every render).\n  const lastHandledValueRef = useRef(currentValue);\n\n  useEffect(() => () => window.clearTimeout(timeoutRef.current), []);\n\n  // Query initiation happens ONLY in handleChange, below — this effect\n  // never starts a query, it only cancels one. That single-initiation-point\n  // invariant is what keeps requestIdRef a reliable \"latest wins\" guard; if\n  // both this effect and handleChange could bump it to *start* a request,\n  // controlled usage could race two in-flight queries against each other.\n  //\n  // Why this effect exists at all: in controlled mode the input's value can\n  // change without handleChange ever running (a parent clears it after a\n  // selection, a reset button, etc). Without this, a debounce armed for the\n  // old text would still fire ~debounceMs later and populate options for\n  // text that's no longer in the field.\n  useEffect(() => {\n    if (!isControlled) return;\n    if (value === lastHandledValueRef.current) return;\n\n    window.clearTimeout(timeoutRef.current);\n    requestIdRef.current += 1; // invalidate any in-flight response\n    setLoading(false);\n    setOptions([]);\n    lastHandledValueRef.current = value;\n    // Deliberately NOT depending on `lastHandledValueRef` (a ref, stable\n    // identity, reading it here doesn't need a dep) or on the cancellation\n    // side-effects themselves — this effect's only job is \"did the\n    // controlled `value` change out from under the handler,\" so it depends\n    // on exactly `isControlled` and `value`.\n  }, [isControlled, value]);\n\n  const handleChange = (nextValue: string) => {\n    window.clearTimeout(timeoutRef.current);\n    if (!isControlled) setUncontrolledValue(nextValue);\n    lastHandledValueRef.current = nextValue;\n    setOptions([]);\n    onChange?.(nextValue);\n\n    const requestId = ++requestIdRef.current;\n\n    if (!shouldQuery(nextValue)) {\n      setLoading(false);\n      return;\n    }\n\n    setLoading(true);\n\n    timeoutRef.current = window.setTimeout(() => {\n      loadOptions(nextValue)\n        .then((results) => {\n          if (requestIdRef.current !== requestId) return; // a newer query already won\n          setLoading(false);\n          setOptions(results);\n        })\n        .catch(() => {\n          if (requestIdRef.current !== requestId) return;\n          setLoading(false);\n          setOptions([]);\n        });\n    }, debounceMs);\n  };\n\n  return (\n    <Autocomplete\n      value={currentValue}\n      data={options}\n      onChange={handleChange}\n      filter={filter}\n      rightSection={loading ? <Loader size={16} aria-label=\"Loading suggestions\" /> : null}\n      label={label}\n      placeholder={placeholder}\n      {...others}\n    />\n  );\n}\n"
    }
  ],
  "meta": {
    "mantine": {
      "requires": ">=9",
      "provider": "MantineProvider",
      "props": {
        "AutocompleteLoading": [
          {
            "name": "value",
            "type": "string",
            "required": false,
            "description": "Controlled value. When provided, this component is fully controlled and onChange is the only way its displayed value changes; composes with the async loadOptions flow so an external write cancels any pending request for the previous text."
          },
          {
            "name": "defaultValue",
            "type": "string",
            "required": false,
            "default": "\"\"",
            "description": "Initial value for uncontrolled usage. Ignored once value is provided."
          },
          {
            "name": "onChange",
            "type": "(value: string) => void",
            "required": false,
            "description": "Called on every keystroke with the raw input value, not debounced."
          },
          {
            "name": "debounceMs",
            "type": "number",
            "required": false,
            "default": "1000",
            "description": "Delay, in milliseconds, after typing stops before loadOptions runs."
          },
          {
            "name": "loadOptions",
            "type": "(query: string) => Promise<string[]>",
            "required": false,
            "description": "Fetches suggestions for the current query. Defaults to a demo implementation that suggests the query at common email domains; replace with a real request in production."
          },
          {
            "name": "shouldQuery",
            "type": "(query: string) => boolean",
            "required": false,
            "default": "(query) => query.trim().length > 0",
            "description": "Decides whether loadOptions should run for a given query; returning false clears any pending request and turns the loader off."
          },
          {
            "name": "filter",
            "type": "OptionsFilter<string>",
            "required": false,
            "default": "({ options }) => options",
            "description": "Forwarded to Autocomplete's own client-side filter. Defaults to a pass-through since loadOptions results are already server-filtered."
          },
          {
            "name": "label",
            "type": "ReactNode",
            "required": false,
            "default": "\"Async Autocomplete data\"",
            "description": "Autocomplete label."
          },
          {
            "name": "placeholder",
            "type": "string",
            "required": false,
            "default": "\"Your email\"",
            "description": "Autocomplete placeholder."
          },
          {
            "name": "...others",
            "type": "Omit<AutocompleteProps, \"data\" | \"value\" | \"defaultValue\" | \"onChange\" | \"rightSection\">",
            "description": "Every other Autocomplete prop is forwarded, including classNames/styles/unstyled/vars (Autocomplete's own Styles API), size, disabled, error, comboboxProps, limit, maxDropdownHeight, renderOption, clearable, etc."
          }
        ]
      },
      "usage": {
        "path": "registry/mantine-ui/autocomplete-loading/autocomplete-loading.usage.tsx",
        "content": "import { AutocompleteLoading } from \"@/components/ui/autocomplete-loading\";\n\nasync function searchTeamMembers(query: string): Promise<string[]> {\n  const response = await fetch(`/api/team-members?q=${encodeURIComponent(query)}`);\n  if (!response.ok) return [];\n  const results: { name: string }[] = await response.json();\n  return results.map((member) => member.name);\n}\n\nexport function TeamMemberPicker() {\n  return (\n    <AutocompleteLoading\n      label=\"Assign to\"\n      placeholder=\"Search team members\"\n      debounceMs={300}\n      loadOptions={searchTeamMembers}\n      shouldQuery={(query) => query.trim().length >= 2}\n      onChange={(value) => console.log(\"value changed\", value)}\n    />\n  );\n}\n"
      }
    }
  }
}
