> ## Documentation Index
> Fetch the complete documentation index at: https://anvil.servicetitan.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tree Select Field – Code

> TreeSelectField components provide hierarchical tree selection with support for async loading, cascading selection, and client-side filtering.

<Note>
  **Beta Feature**

  This feature is currently in beta, and needs to be imported from `@servicetitan/anvil2/beta`.

  While we hope to minimize breaking changes, they may occur due to feedback we receive or other improvements. These will always be documented in the changelog and communicated in Slack.

  Please reach out in the [#ask-designsystem](https://servicetitan.enterprise.slack.com/archives/CBSRGHTRS) channel with any questions or feedback!
</Note>

<Tabs>
  <Tab title="Implementation">
    ## Overview

    The tree select field family includes two components for different use cases:

    * **`TreeSelectField`** — For async data loading with support for lazy branch expansion
      * Includes automatic debouncing of the search input (configurable via the `debounceMs` prop)
      * Includes automatic LRU caching of search results and loaded children (configurable via the `cache` prop)
    * **`TreeSelectFieldSync`** — For client-side filtering of static tree option arrays

    Both components provide a hierarchical tree dropdown with cascading parent/child selection, keyboard navigation, and adaptive display modes (popover or dialog).

    ## TreeSelectFieldSync (Static Options)

    Use `TreeSelectFieldSync` when you have a static tree of options that can be filtered client-side.

    ```tsx theme={null}
    import { useState } from "react";
    import { TreeSelectFieldSync, TreeSelectFieldValue } from "@servicetitan/anvil2/beta";

    const options = [
      {
        id: "eng",
        label: "Engineering",
        children: [
          { id: "fe", label: "Frontend" },
          { id: "be", label: "Backend" },
          { id: "infra", label: "Infrastructure" },
        ],
      },
      {
        id: "design",
        label: "Design",
        children: [
          { id: "ux", label: "UX Design" },
          { id: "visual", label: "Visual Design" },
        ],
      },
      { id: "hr", label: "Human Resources" },
    ];

    const ExampleComponent = () => {
      const [selected, setSelected] = useState<TreeSelectFieldValue[]>([]);

      return (
        <TreeSelectFieldSync
          label="Select departments"
          options={options}
          value={selected}
          onSelectedOptionsChange={setSelected}
          defaultExpandLevel={1}
        />
      );
    };
    ```

    ### Filtering and Sorting

    By default, `TreeSelectFieldSync` uses [match-sorter](https://github.com/kentcdodds/match-sorter) to filter options by their `label` and `searchText` fields. The tree structure is preserved: parent nodes of any matching node remain visible so the hierarchy is clear.

    You can customize this behavior in two ways:

    #### Using match-sorter options

    Pass a [match-sorter options object](https://github.com/kentcdodds/match-sorter#options) to customize the default filtering and sorting behavior:

    ```tsx theme={null}
    <TreeSelectFieldSync
      options={options}
      filter={{
        keys: ["label", "searchText"],
        // ...other match-sorter options
      }}
      // ...other props
    />
    ```

    #### Using a custom filter function

    Pass a function for full control over both filtering and tree structure. The returned array determines the exact tree shown in the dropdown:

    ```tsx theme={null}
    <TreeSelectFieldSync
      options={options}
      filter={(nodes, searchValue) => {
        // Return a filtered tree — parent preservation is your responsibility
        return customTreeFilter(nodes, searchValue);
      }}
      // ...other props
    />
    ```

    ## TreeSelectField (Async Loading)

    Use `TreeSelectField` when tree data needs to be fetched from an API or when branches load their children on demand.

    ### Basic Async Loading

    ```tsx theme={null}
    import { useState } from "react";
    import { TreeSelectField, TreeSelectFieldValue } from "@servicetitan/anvil2/beta";

    const ExampleComponent = () => {
      const [selected, setSelected] = useState<TreeSelectFieldValue[]>([]);

      return (
        <TreeSelectField
          label="Select categories"
          loadOptions={async (searchValue) => {
            const response = await fetch(`/api/categories?q=${searchValue}`);
            return response.json();
          }}
          value={selected}
          onSelectedOptionsChange={setSelected}
        />
      );
    };
    ```

    ### Lazy Branch Loading

    Branches with `children: null` are treated as unloaded. When expanded, `loadOptions` is called with the parent node to fetch its children:

    ```tsx theme={null}
    import { useState } from "react";
    import {
      TreeSelectField,
      TreeSelectFieldNode,
      TreeSelectFieldValue,
    } from "@servicetitan/anvil2/beta";

    const ExampleComponent = () => {
      const [selected, setSelected] = useState<TreeSelectFieldValue[]>([]);

      return (
        <TreeSelectField
          label="Select location"
          loadOptions={async (searchValue, parentNode) => {
            if (parentNode) {
              // Load children for the expanded branch
              const response = await fetch(`/api/locations/${parentNode.id}/children`);
              return response.json();
            }
            // Load root nodes
            const response = await fetch(`/api/locations?q=${searchValue}`);
            return response.json();
          }}
          value={selected}
          onSelectedOptionsChange={setSelected}
        />
      );
    };
    ```

    Root nodes returned by `loadOptions` should use `children: null` for branches whose children haven't been loaded yet:

    ```tsx theme={null}
    // Example response from /api/locations
    [
      {
        id: "us",
        label: "United States",
        children: null, // Children will be loaded when this branch is expanded
      },
      {
        id: "ca",
        label: "Canada",
        children: null,
      },
    ]
    ```

    When a user selects a branch with unloaded descendants in linked selection mode, all descendant branches are recursively loaded before the cascade is applied. A loading spinner appears on the checkbox during this process.

    ### Progressive (Multi-Level) Loading

    Lazy loading is recursive: when the children returned for one branch are themselves
    branches with `children: null`, each level loads on demand as the user drills in.
    This is the common shape for a file browser or a deep location hierarchy where you
    never want to fetch the whole tree up front.

    ```tsx theme={null}
    import { useState } from "react";
    import {
      TreeSelectField,
      TreeSelectFieldNode,
      TreeSelectFieldValue,
    } from "@servicetitan/anvil2/beta";

    const FilePicker = () => {
      const [selected, setSelected] = useState<TreeSelectFieldValue[]>([]);

      const loadOptions = async (
        searchValue: string,
        parentNode?: TreeSelectFieldNode,
      ): Promise<TreeSelectFieldNode[]> => {
        // Expanding a folder: fetch only its immediate entries. Sub-folders come
        // back with `children: null` so they load on their own expansion.
        if (parentNode) {
          const entries = await fetchFolder(parentNode.id);
          return entries.map((entry) => ({
            id: entry.id,
            label: entry.name,
            children: entry.isFolder ? null : undefined,
            childCount: entry.isFolder ? entry.count : undefined,
          }));
        }
        // Roots — or a server-side search across the whole tree.
        if (searchValue) return searchAllFolders(searchValue);
        return fetchFolder("root");
      };

      return (
        <TreeSelectField
          label="Attach a file"
          loadOptions={loadOptions}
          value={selected}
          onSelectedOptionsChange={setSelected}
          selectionMode="single"
          // Don't fetch anything until the user opens the menu.
          initialLoad="open"
          // Keep recently-opened folders warm so re-expanding is instant.
          cache={{ enabled: true, maxSize: 50 }}
        />
      );
    };
    ```

    After a mutation that changes the underlying data (an upload, a rename, a delete),
    call `invalidate()` on the ref to drop the cache and re-fetch from the top:

    ```tsx theme={null}
    const fieldRef = useRef<TreeSelectFieldHandle>(null);

    const handleUploadComplete = () => {
      fieldRef.current?.invalidate();
    };
    ```

    ### Restoring a Selection Under Unloaded Branches

    With lazy loading, a selected node can sit under branches that haven't been
    fetched yet — most commonly when you **restore a persisted selection** on
    mount, before the user has expanded anything. In linked mode the field shows
    a parent as checked or indeterminate by looking at its children, but those
    children aren't loaded, so the ancestor checkboxes can't reflect the
    selection until the branch is expanded.

    Provide the selected value's ancestry via the optional **`path`** field
    (ancestor ids, root → parent) so the field can mark the correct ancestor
    branches immediately, with no extra fetching:

    ```tsx theme={null}
    // A selection restored from storage. `path` lets the field show
    // "Engineering" and "Frontend" as indeterminate before they're loaded.
    const [selected, setSelected] = useState<TreeSelectFieldValue[]>([
      {
        id: "alice",
        label: "Alice Chen",
        path: ["engineering", "frontend"],
      },
    ]);
    ```

    You don't have to build `path` by hand for selections made through the UI:
    when the field emits a selection whose ancestors are loaded, it **stamps
    `path` onto the value for you**. Persisting `value` as-is and restoring it
    later round-trips the ancestry automatically.

    Without `path`, the field still resolves ancestry for any branch the user has
    expanded this session (it remembers child→parent relationships as branches
    load). A selection whose ancestors have never been loaded simply shows its
    branches unchecked until they're expanded — at which point the state
    self-corrects. Supplying `path` is what makes a cold restore correct up front.

    <Note>
      During search, set `childCount` on branch nodes so the field can tell a
      partially-matched branch from a fully-selected one — see
      [Search and `childCount`](#search-and-childcount).
    </Note>

    ## Selection Modes

    The `selectionMode` prop controls how nodes relate to each other during selection:

    * **`"linked"`** (default) — Parent-child cascading. Selecting a parent checks all children; selecting all children checks the parent. The `valueConsistsOf` prop controls which nodes appear in the value array.
    * **`"independent"`** — Each node is selected independently with no cascading.
    * **`"single"`** — Only one node can be selected at a time. The menu closes after selection.

    ```tsx theme={null}
    // Linked mode (default) — cascading selection
    <TreeSelectFieldSync
      selectionMode="linked"
      options={options}
      // ...other props
    />

    // Independent mode — no cascading
    <TreeSelectFieldSync
      selectionMode="independent"
      options={options}
      // ...other props
    />

    // Single-select mode — one node at a time
    <TreeSelectFieldSync
      selectionMode="single"
      options={options}
      // ...other props
    />
    ```

    ## Value Strategies

    In linked selection mode, the `valueConsistsOf` prop controls which checked nodes appear in the `value` array:

    | Strategy                    | Selectable Nodes         | Value Contains                                   |
    | --------------------------- | ------------------------ | ------------------------------------------------ |
    | `"ALL"`                     | Any node                 | All checked nodes                                |
    | `"BRANCH_PRIORITY"`         | Any node                 | Branches replace their leaves when fully checked |
    | `"BRANCH_ONLY"`             | Branches only            | Only branches (leaves are visible but inert)     |
    | `"LEAF_PRIORITY"` (default) | Leaves + expand branches | Leaves and childless branches                    |
    | `"LEAF_ONLY"`               | Leaves only              | Strictly leaf nodes                              |

    ```tsx theme={null}
    <TreeSelectFieldSync
      selectionMode="linked"
      valueConsistsOf="BRANCH_PRIORITY"
      options={options}
      // ...other props
    />
    ```

    In single-select mode, `valueConsistsOf` restricts which node types can be selected: `"LEAF_ONLY"` allows only leaves, `"BRANCH_ONLY"` allows only branches, and the other strategies allow any node.

    ### What ends up in `value`

    The same user action produces a different `value` array depending on the
    strategy. Given this tree, with the user checking **Building B** (which checks
    its only floor and both of that floor's rooms):

    ```
    Headquarters
    └─ Building B          ← checked
       └─ 3rd Floor
          ├─ Conference Room
          └─ Focus Room
    ```

    | `valueConsistsOf`   | Resulting `value` (by `label`)                             |
    | ------------------- | ---------------------------------------------------------- |
    | `"ALL"`             | Building B, 3rd Floor, Conference Room, Focus Room         |
    | `"BRANCH_PRIORITY"` | Building B (the branch replaces its fully-checked subtree) |
    | `"BRANCH_ONLY"`     | Building B, 3rd Floor (only branches; rooms are inert)     |
    | `"LEAF_PRIORITY"`   | Conference Room, Focus Room (leaves only)                  |
    | `"LEAF_ONLY"`       | Conference Room, Focus Room (leaves only)                  |

    `BRANCH_PRIORITY` is the right choice when "select the whole branch" should
    read as a single chip; `LEAF_ONLY` is right when the consumer only ever wants
    concrete items (e.g. people, not teams) in the submitted value.

    ## Display Modes

    Control how the options menu is displayed using the `displayMenuAs` prop:

    ```tsx theme={null}
    // Automatically choose based on device (default)
    <TreeSelectFieldSync displayMenuAs="auto" {...props} />

    // Always show as popover (not recommended for mobile)
    <TreeSelectFieldSync displayMenuAs="popover" {...props} />

    // Always show as dialog
    <TreeSelectFieldSync displayMenuAs="dialog" {...props} />
    ```

    ## Caching

    `TreeSelectField` caches `loadOptions` results by default using two separate LRU caches: one for search results and one for lazy-loaded children. Configure caching behavior:

    ```tsx theme={null}
    // Disable caching
    <TreeSelectField cache={{ enabled: false }} {...props} />

    // Configure max cache size (default: 15)
    <TreeSelectField cache={{ maxSize: 100 }} {...props} />
    ```

    ### Clearing the Cache

    Use a ref to imperatively clear the cache:

    ```tsx theme={null}
    import { useRef } from "react";
    import { TreeSelectField, TreeSelectFieldHandle } from "@servicetitan/anvil2/beta";

    const ExampleComponent = () => {
      const fieldRef = useRef<TreeSelectFieldHandle>(null);

      const handleRefresh = () => {
        fieldRef.current?.clearCache();
      };

      return (
        <>
          <TreeSelectField ref={fieldRef} {...props} />
          <button onClick={handleRefresh}>Refresh Options</button>
        </>
      );
    };
    ```

    ### Invalidating Options

    Call `invalidate()` to clear the cache and reload options from the data source:

    ```tsx theme={null}
    const handleDataSourceChange = () => {
      fieldRef.current?.invalidate();
    };
    ```

    <Note>`TreeSelectFieldSync` handles this automatically when its `options` prop reference changes.</Note>

    ## Initial Load Behavior

    Control when options are first loaded with the `initialLoad` prop:

    ```tsx theme={null}
    // Load immediately on mount (default)
    <TreeSelectField initialLoad="immediate" {...props} />

    // Load when user opens the dropdown
    <TreeSelectField initialLoad="open" {...props} />

    // Auto (currently equivalent to "immediate")
    <TreeSelectField initialLoad="auto" {...props} />
    ```

    ## Controlled Expansion

    By default, expansion state is managed internally. Use `defaultExpandLevel` to set the initial depth:

    ```tsx theme={null}
    // Start with all branches collapsed (default)
    <TreeSelectFieldSync defaultExpandLevel={0} options={options} {...props} />

    // Start with first level expanded
    <TreeSelectFieldSync defaultExpandLevel={1} options={options} {...props} />

    // Start fully expanded
    <TreeSelectFieldSync defaultExpandLevel={Infinity} options={options} {...props} />
    ```

    For full control, use `expandedIds` and `onExpandedIdsChange`. Because the set
    lives in your state, external UI can drive expansion directly — for example, a
    button that drills the tree open to a specific path:

    ```tsx theme={null}
    const [expandedIds, setExpandedIds] = useState<Set<string | number>>(
      () => new Set(["hq", "bldg-b", "b-3"]),
    );

    <TreeSelectFieldSync
      expandedIds={expandedIds}
      onExpandedIdsChange={setExpandedIds}
      options={options}
      // ...other props
    />

    // Jump straight to Building B › 3rd Floor
    <button onClick={() => setExpandedIds(new Set(["hq", "bldg-b", "b-3"]))}>
      Go to 3rd Floor
    </button>

    // Collapse everything
    <button onClick={() => setExpandedIds(new Set())}>Collapse all</button>
    ```

    The imperative handle also provides `expandAll()` and `collapseAll()` for when
    you don't need to track the exact set:

    ```tsx theme={null}
    fieldRef.current?.expandAll();
    fieldRef.current?.collapseAll();
    ```

    ## Controlled Search

    By default the search input is uncontrolled. Provide `searchValue` and
    `onSearchChange` to keep it in sync with your own state — useful when an
    external search box should drive the field, or when you filter `loadOptions`
    server-side and want to react to the query yourself.

    ```tsx theme={null}
    const [search, setSearch] = useState("");

    <input
      value={search}
      onChange={(e) => setSearch(e.target.value)}
      placeholder="Search people…"
    />

    <TreeSelectFieldSync
      options={options}
      value={selected}
      onSelectedOptionsChange={setSelected}
      searchValue={search}
      onSearchChange={setSearch}
      // ...other props
    />
    ```

    Typing inside the field calls `onSearchChange`, and updating `searchValue`
    externally re-filters the tree — the two stay in lockstep.

    ## Search and `childCount`

    When a search filter hides some of a branch's children, the field only sees
    the matches that remain. To avoid showing a branch as fully checked when an
    unselected child is merely filtered out of view, set **`childCount`** (the
    branch's true number of children) on branch nodes. The field compares the
    loaded children against `childCount`: if fewer are present than the real
    total, a branch with some — but not provably all — children selected renders
    as indeterminate rather than checked.

    ```tsx theme={null}
    // childCount reflects the full child set, independent of the current filter.
    {
      id: "sales-marketing",
      label: "Sales & Marketing",
      childCount: 2, // even when only "Sales" matches the search
      children: null,
    }
    ```

    `TreeSelectFieldSync` derives `childCount` automatically from its static
    `options`, so client-side filtering is always accurate. For async
    `TreeSelectField`, return `childCount` from `loadOptions` (it should reflect
    the unfiltered count). Without it, a partially-filtered branch falls back to
    reflecting only its visible children.

    ## Field States

    ### Error State

    ```tsx theme={null}
    <TreeSelectFieldSync
      label="Department"
      options={options}
      value={selected}
      onSelectedOptionsChange={setSelected}
      error="Please select at least one department"
    />
    ```

    ### Hint and Description

    ```tsx theme={null}
    <TreeSelectFieldSync
      label="Department"
      options={options}
      value={selected}
      onSelectedOptionsChange={setSelected}
      hint="Select the departments this applies to"
      description="Used for routing and permissions"
    />
    ```

    ### Required Field

    ```tsx theme={null}
    <TreeSelectFieldSync
      label="Department"
      options={options}
      value={selected}
      onSelectedOptionsChange={setSelected}
      required
    />
    ```

    ### Disabled and ReadOnly

    When `disabled` is set, users cannot interact with the field.

    ```tsx theme={null}
    <TreeSelectFieldSync
      label="Department"
      options={options}
      value={selected}
      onSelectedOptionsChange={setSelected}
      disabled
    />
    ```

    When `readOnly` is set, users can browse the tree but cannot change the selection.

    ```tsx theme={null}
    <TreeSelectFieldSync
      label="Department"
      options={options}
      value={selected}
      onSelectedOptionsChange={setSelected}
      readOnly
    />
    ```

    ### Prefix and Suffix

    ```tsx theme={null}
    <TreeSelectFieldSync
      label="Region"
      options={regions}
      value={selected}
      onSelectedOptionsChange={setSelected}
      prefix="Location:"
    />
    ```

    ### Markdown in labels

    The `label` prop supports inline markdown: bold (`**text**`), italic (`*text*`), highlight (`==text==`), and code (`` `text` ``).

    ### Hide the label

    Use `hideLabel` to visually hide the label. The `label` string remains accessible to screen readers.

    ```tsx theme={null}
    <TreeSelectFieldSync
      label="Department"
      hideLabel
      placeholder="Select departments..."
      options={options}
      value={selected}
      onSelectedOptionsChange={setSelected}
    />
    ```

    ## Disabled Nodes

    Individual nodes can be disabled by setting `disabled: true`:

    ```tsx theme={null}
    const options = [
      {
        id: "eng",
        label: "Engineering",
        children: [
          { id: "fe", label: "Frontend" },
          { id: "be", label: "Backend", disabled: true },
        ],
      },
    ];
    ```

    Disabled nodes appear with reduced opacity and cannot be selected. In linked mode, disabled nodes are excluded from cascade selection.

    ## Virtualization

    Pass `virtualize` to enable windowed rendering for large trees. Only visible rows are mounted to the DOM.

    ```tsx theme={null}
    <TreeSelectField
      virtualize
      label="Large tree"
      loadOptions={loadLargeTree}
      value={selected}
      onSelectedOptionsChange={setSelected}
    />
    ```

    ## Disabling Search

    Pass `disableSearch` to hide the search input:

    ```tsx theme={null}
    <TreeSelectFieldSync
      disableSearch
      label="Category"
      options={options}
      value={selected}
      onSelectedOptionsChange={setSelected}
    />
    ```

    ## Chip Display

    In multi-select mode, selected values appear as removable chips. Control chip layout with `singleRow` and `maxChips`:

    ```tsx theme={null}
    // Single row with overflow count
    <TreeSelectFieldSync
      singleRow
      options={options}
      // ...other props
    />

    // Limit visible chips
    <TreeSelectFieldSync
      maxChips={3}
      options={options}
      // ...other props
    />
    ```
  </Tab>

  <Tab title="TreeSelectField Props">
    ## `TreeSelectField` Props

    ```tsx theme={null}
    import { useState } from "react";
    import { TreeSelectField, TreeSelectFieldValue } from "@servicetitan/anvil2/beta";

    const ExampleComponent = () => {
      const [selected, setSelected] = useState<TreeSelectFieldValue[]>([]);

      return (
        <TreeSelectField
          label="Select categories"
          loadOptions={async (searchValue, parentNode) => {
            if (parentNode) {
              return fetchChildren(parentNode.id);
            }
            return fetchRoots(searchValue);
          }}
          onSelectedOptionsChange={setSelected}
          value={selected}
        />
      );
    };
    ```

    <ParamField path="label" type="string" required>
      The label of the tree select field. Supports inline markdown formatting.
    </ParamField>

    <ParamField path="loadOptions" type="(searchValue: string, parentNode?: TreeSelectFieldNode) => TreeSelectFieldNode[] | Promise<TreeSelectFieldNode[]>" required>
      Function to load tree nodes. Called with an empty string on initial load and with the search value when the user types. When a branch with `children: null` is expanded, called again with the parent node as the second argument to load its children.
    </ParamField>

    <ParamField path="onSelectedOptionsChange" type="(nodes: TreeSelectFieldValue[]) => void" required>
      Callback fired when the selection changes. Receives the new array of selected values.
    </ParamField>

    <ParamField path="value" type="TreeSelectFieldValue[]" required>
      The currently selected values. Always an array, even in single-select mode.
    </ParamField>

    <ParamField path="cache" type="{ enabled?: boolean; maxSize?: number }">
      Configuration for caching `loadOptions` results. Two separate LRU caches are maintained: one for search results and one for lazy-loaded children.

      * `enabled` — Whether caching is enabled (default: `true`)
      * `maxSize` — Maximum entries per cache before eviction (default: `15`)
    </ParamField>

    <ParamField path="className" type="string">
      Custom CSS class name for the wrapper element.
    </ParamField>

    <ParamField path="debounceMs" type="number" default="200">
      Milliseconds to debounce search input before calling `loadOptions`.
    </ParamField>

    <ParamField path="defaultExpandLevel" type="number">
      Initial expansion depth. `0` keeps all branches collapsed, `1` expands the first level, `Infinity` expands everything. Applied when tree data first loads.
    </ParamField>

    <ParamField path="description" type="ReactElement | string">
      Description text displayed below the input field.
    </ParamField>

    <ParamField path="disableSearch" type="boolean">
      Hides the search input.
    </ParamField>

    <ParamField path="disabled" type="boolean">
      Whether the field is disabled. When disabled, the field cannot be interacted with and the dropdown cannot open.
    </ParamField>

    <ParamField path="displayMenuAs" type={`"auto" | "popover" | "dialog"`} default="auto">
      How to display the options menu:

      * `auto` — Popover on desktop, dialog on mobile
      * `popover` — Always display as popover
      * `dialog` — Always display as dialog
    </ParamField>

    <ParamField path="error" type="boolean | string | string[] | ReactElement">
      Error state for the field. When `true`, shows error styling. When a string, string array, or ReactElement, also displays as an error message below the field.
    </ParamField>

    <ParamField path="expandedIds" type="Set<string | number>">
      Controlled set of expanded branch node IDs. Use with `onExpandedIdsChange` for full control over expansion state.
    </ParamField>

    <ParamField path="hideLabel" type="boolean">
      Visually hides the label while keeping it accessible to screen readers.
    </ParamField>

    <ParamField path="hint" type="ReactElement | string">
      Hint text displayed below the input field.
    </ParamField>

    <ParamField path="id" type="string">
      The id of the tree select field.
    </ParamField>

    <ParamField path="initialLoad" type={`"auto" | "immediate" | "open"`}>
      Controls when `loadOptions` is first called:

      * `auto` — Currently equivalent to `immediate`
      * `immediate` — Load on component mount
      * `open` — Load when dropdown is opened
    </ParamField>

    <ParamField path="labelNode" type="ReactNode">
      Custom label content. Overrides the `label` string for rendering but `label` is still used for accessibility.
    </ParamField>

    <ParamField path="maxChips" type="number">
      Maximum number of chips to display in multi-select mode before showing an overflow count.
    </ParamField>

    <ParamField path="onExpandedIdsChange" type="(ids: Set<string | number>) => void">
      Callback fired when expansion state changes. Use with `expandedIds` for controlled expansion.
    </ParamField>

    <ParamField path="onSearchChange" type="(value: string) => void">
      Callback fired when the search input value changes. Use with `searchValue` for controlled search.
    </ParamField>

    <ParamField path="placeholder" type="string">
      Placeholder text for the search input.
    </ParamField>

    <ParamField path="prefix" type="string | ReactElement">
      Content to display before the input field.
    </ParamField>

    <ParamField path="readOnly" type="boolean">
      Whether the field is read-only. When read-only, the dropdown can open to browse the tree but selections cannot be changed.
    </ParamField>

    <ParamField path="required" type="boolean">
      Whether the field is required. Shows a red asterisk (\*) next to the label.
    </ParamField>

    <ParamField path="searchValue" type="string">
      Controlled search input value. Use with `onSearchChange` for controlled search.
    </ParamField>

    <ParamField path="selectionMode" type={`"single" | "independent" | "linked"`} default="linked">
      Controls how nodes relate to each other during selection:

      * `single` — One node at a time, menu closes after selection
      * `independent` — No parent-child cascading
      * `linked` — Parent-child cascading with configurable value shape via `valueConsistsOf`
    </ParamField>

    <ParamField path="singleRow" type="boolean">
      When `true`, selected chips display in a single row with horizontal overflow.
    </ParamField>

    <ParamField path="size" type={`"small" | "medium" | "large"`}>
      The size of the tree select field.
    </ParamField>

    <ParamField path="style" type="CSSProperties">
      Custom inline styles for the wrapper element.
    </ParamField>

    <ParamField path="suffix" type="string | ReactElement">
      Content to display after the input field.
    </ParamField>

    <ParamField path="valueConsistsOf" type={`"ALL" | "BRANCH_PRIORITY" | "BRANCH_ONLY" | "LEAF_PRIORITY" | "LEAF_ONLY"`} default="LEAF_PRIORITY">
      Controls which nodes are selectable and how the `value` array is shaped in linked selection mode:

      * `ALL` — Any node selectable, all checked nodes in value
      * `BRANCH_PRIORITY` — Any node selectable, branches replace leaves when fully checked
      * `BRANCH_ONLY` — Only branches selectable, leaves visible but inert
      * `LEAF_PRIORITY` — Leaves and childless branches in value
      * `LEAF_ONLY` — Strictly leaf nodes in value
    </ParamField>

    <ParamField path="virtualize" type="boolean">
      Enables windowed rendering for the tree dropdown. Only visible rows are mounted to the DOM.
    </ParamField>

    <ParamField path="warning" type="string | string[]">
      Warning message(s) displayed below the input field.
    </ParamField>
  </Tab>

  <Tab title="TreeSelectFieldSync Props">
    ## `TreeSelectFieldSync` Props

    `TreeSelectFieldSync` accepts all props from `TreeSelectField` except `loadOptions`, `debounceMs`, `cache`, and `initialLoad`, plus the following:

    ```tsx theme={null}
    import { useState } from "react";
    import { TreeSelectFieldSync, TreeSelectFieldValue } from "@servicetitan/anvil2/beta";

    const options = [
      {
        id: "eng",
        label: "Engineering",
        children: [
          { id: "fe", label: "Frontend" },
          { id: "be", label: "Backend" },
        ],
      },
    ];

    const ExampleComponent = () => {
      const [selected, setSelected] = useState<TreeSelectFieldValue[]>([]);

      return (
        <TreeSelectFieldSync
          label="Select departments"
          onSelectedOptionsChange={setSelected}
          options={options}
          value={selected}
        />
      );
    };
    ```

    <ParamField path="label" type="string" required>
      The label of the tree select field. Supports inline markdown formatting.
    </ParamField>

    <ParamField path="onSelectedOptionsChange" type="(nodes: TreeSelectFieldValue[]) => void" required>
      Callback fired when the selection changes.
    </ParamField>

    <ParamField path="options" type="TreeSelectFieldNode[]" required>
      The static tree of options to display.
    </ParamField>

    <ParamField path="value" type="TreeSelectFieldValue[]" required>
      The currently selected values.
    </ParamField>

    <ParamField path="className" type="string">
      Custom CSS class name for the wrapper element.
    </ParamField>

    <ParamField path="defaultExpandLevel" type="number">
      Initial expansion depth. `0` keeps all branches collapsed, `1` expands the first level, `Infinity` expands everything.
    </ParamField>

    <ParamField path="description" type="ReactElement | string">
      Description text displayed below the input field.
    </ParamField>

    <ParamField path="disableSearch" type="boolean">
      Hides the search input.
    </ParamField>

    <ParamField path="disabled" type="boolean">
      Whether the field is disabled.
    </ParamField>

    <ParamField path="displayMenuAs" type={`"auto" | "popover" | "dialog"`} default="auto">
      How to display the options menu.
    </ParamField>

    <ParamField path="error" type="boolean | string | string[] | ReactElement">
      Error state for the field.
    </ParamField>

    <ParamField path="expandedIds" type="Set<string | number>">
      Controlled set of expanded branch node IDs.
    </ParamField>

    <ParamField path="filter" type="function | MatchSorterOptions">
      Controls how options are filtered when the user types a search value. Can be:

      * A function: `(nodes: TreeSelectFieldNode[], searchValue: string) => TreeSelectFieldNode[]` — receives all options and returns the filtered tree
      * A [match-sorter options object](https://github.com/kentcdodds/match-sorter#options) to customize the default match-sorter behavior

      Default: Filters by `label` and `searchText` using match-sorter, preserving parent nodes of any match.
    </ParamField>

    <ParamField path="hideLabel" type="boolean">
      Visually hides the label while keeping it accessible to screen readers.
    </ParamField>

    <ParamField path="hint" type="ReactElement | string">
      Hint text displayed below the input field.
    </ParamField>

    <ParamField path="id" type="string">
      The id of the tree select field.
    </ParamField>

    <ParamField path="labelNode" type="ReactNode">
      Custom label content.
    </ParamField>

    <ParamField path="maxChips" type="number">
      Maximum number of chips to display before showing an overflow count.
    </ParamField>

    <ParamField path="onExpandedIdsChange" type="(ids: Set<string | number>) => void">
      Callback fired when expansion state changes.
    </ParamField>

    <ParamField path="onSearchChange" type="(value: string) => void">
      Callback fired when the search input value changes.
    </ParamField>

    <ParamField path="placeholder" type="string">
      Placeholder text for the search input.
    </ParamField>

    <ParamField path="prefix" type="string | ReactElement">
      Content to display before the input field.
    </ParamField>

    <ParamField path="readOnly" type="boolean">
      Whether the field is read-only.
    </ParamField>

    <ParamField path="required" type="boolean">
      Whether the field is required.
    </ParamField>

    <ParamField path="searchValue" type="string">
      Controlled search input value.
    </ParamField>

    <ParamField path="selectionMode" type={`"single" | "independent" | "linked"`} default="linked">
      Controls how nodes relate to each other during selection.
    </ParamField>

    <ParamField path="singleRow" type="boolean">
      When `true`, selected chips display in a single row.
    </ParamField>

    <ParamField path="size" type={`"small" | "medium" | "large"`}>
      The size of the tree select field.
    </ParamField>

    <ParamField path="style" type="CSSProperties">
      Custom inline styles for the wrapper element.
    </ParamField>

    <ParamField path="suffix" type="string | ReactElement">
      Content to display after the input field.
    </ParamField>

    <ParamField path="valueConsistsOf" type={`"ALL" | "BRANCH_PRIORITY" | "BRANCH_ONLY" | "LEAF_PRIORITY" | "LEAF_ONLY"`} default="LEAF_PRIORITY">
      Controls which nodes are selectable and how the `value` array is shaped.
    </ParamField>

    <ParamField path="virtualize" type="boolean">
      Enables windowed rendering for the tree dropdown.
    </ParamField>

    <ParamField path="warning" type="string | string[]">
      Warning message(s) displayed below the input field.
    </ParamField>
  </Tab>

  <Tab title="TreeSelectFieldNode Type">
    ## `TreeSelectFieldNode` Type

    Nodes passed to or returned from TreeSelectField components must conform to this shape:

    ```tsx theme={null}
    type TreeSelectFieldNode = {
      /** Unique identifier for the node */
      id: string | number;

      /** Display text for the node */
      label: string;

      /** Optional text used for search matching (in addition to label) */
      searchText?: string;

      /** Whether the node is disabled and cannot be selected */
      disabled?: boolean;

      /** Optional extra data to associate with the node */
      extra?: Record<string, unknown>;

      /** Child nodes — determines the node type:
       *  - undefined: leaf node
       *  - []: empty branch (expanded shows "No items")
       *  - [...]: branch with children
       *  - null: unloaded lazy branch (children loaded on expand)
       */
      children?: TreeSelectFieldNode[] | null;

      /** True number of children for a branch. Lets the field compute accurate
       *  parent check-state when children are filtered out by search or not yet
       *  loaded. Returned from loadOptions for async trees; derived
       *  automatically by TreeSelectFieldSync. */
      childCount?: number;

      /** Ancestor ids, root → parent. Selection-ancestry metadata, primarily
       *  meaningful on value objects: lets the field show correct ancestor
       *  check-states for a selection whose branches aren't loaded yet (e.g. a
       *  restored selection). Omit on loadOptions results — it isn't a
       *  structural attribute. */
      path?: (string | number)[];
    };
    ```

    ### Node Types

    The `children` property determines the node's role in the tree:

    | `children` value | Node type       | Behavior                                                             |
    | ---------------- | --------------- | -------------------------------------------------------------------- |
    | `undefined`      | Leaf            | Terminal node, no expand icon                                        |
    | `[]`             | Empty branch    | Shows expand icon, displays "No items" when expanded                 |
    | `[...]`          | Branch          | Shows expand icon, renders children when expanded                    |
    | `null`           | Unloaded branch | Shows expand icon, triggers `loadOptions` with parent node on expand |

    ### Example with Mixed Node Types

    ```tsx theme={null}
    const nodes: TreeSelectFieldNode[] = [
      {
        id: "eng",
        label: "Engineering",
        children: [
          { id: "fe", label: "Frontend" },           // leaf
          { id: "be", label: "Backend" },             // leaf
          { id: "infra", label: "Infrastructure" },   // leaf
        ],
      },
      {
        id: "design",
        label: "Design",
        children: null,    // lazy branch — children loaded on expand
      },
      {
        id: "archive",
        label: "Archived Teams",
        children: [],      // empty branch
      },
      { id: "hr", label: "Human Resources" },        // leaf (no children property)
    ];
    ```
  </Tab>

  <Tab title="TreeSelectFieldValue Type">
    ## `TreeSelectFieldValue` Type

    The value array contains slim objects with just enough data to render chips and identify nodes:

    ```tsx theme={null}
    type TreeSelectFieldValue = {
      /** The node's unique identifier */
      id: string | number;

      /** The node's display text (used for chip labels) */
      label: string;

      /** Optional extra data from the node */
      extra?: Record<string, unknown>;

      /** Ancestor ids, root → parent. Supply on a restored selection so the
       *  field can show correct ancestor check-states before those branches
       *  load. The field stamps it automatically on selections it emits, so a
       *  persisted-then-restored value round-trips the ancestry. */
      path?: (string | number)[];
    };
    ```

    ### Usage

    The `value` prop is always an array, even in single-select mode:

    ```tsx theme={null}
    // Multi-select
    const [selected, setSelected] = useState<TreeSelectFieldValue[]>([]);

    // Single-select (still an array, but contains 0 or 1 items)
    const [selected, setSelected] = useState<TreeSelectFieldValue[]>([]);
    ```

    ### Imperative Handle

    Access imperative methods via a ref:

    ```tsx theme={null}
    import { useRef } from "react";
    import { TreeSelectFieldHandle } from "@servicetitan/anvil2/beta";

    const fieldRef = useRef<TreeSelectFieldHandle>(null);

    // Clear the options cache
    fieldRef.current?.clearCache();

    // Clear cache and reload options
    fieldRef.current?.invalidate();

    // Expand or collapse all branches
    fieldRef.current?.expandAll();
    fieldRef.current?.collapseAll();
    ```
  </Tab>
</Tabs>
