> ## 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 Menu – Code

> TreeSelectMenu components attach a hierarchical tree menu to any trigger element.

<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 menu family includes two components for different use cases:

    * **`TreeSelectMenu`** — 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)
    * **`TreeSelectMenuSync`** — For client-side filtering of static tree option arrays

    Both components attach a hierarchical tree dropdown to any trigger element via a `trigger` render prop. They support cascading parent/child selection, keyboard navigation, and adaptive display modes (popover or dialog). Use a tree select menu when you need tree selection behavior outside of a form field context — for example, attaching a dropdown to a button, icon, or custom element.

    <Note>
      Looking for a form field with a built-in label, error state, and selected chips? Use [`TreeSelectField`](/docs/web/components/tree-select-field/code) instead.
    </Note>

    ## TreeSelectMenuSync (Static Options)

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

    ```tsx theme={null}
    import { useState } from "react";
    import { TreeSelectMenuSync, TreeSelectMenuValue } from "@servicetitan/anvil2/beta";
    import { Button } from "@servicetitan/anvil2";

    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<TreeSelectMenuValue[]>([]);

      return (
        <TreeSelectMenuSync
          label="Select departments"
          options={options}
          value={selected}
          onSelectedOptionsChange={setSelected}
          defaultExpandLevel={1}
          trigger={(props) => (
            <Button {...props}>
              {selected.length > 0
                ? `${selected.length} selected`
                : "Select departments"}
            </Button>
          )}
        />
      );
    };
    ```

    ### Filtering and Sorting

    By default, `TreeSelectMenuSync` 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}
    <TreeSelectMenuSync
      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}
    <TreeSelectMenuSync
      options={options}
      filter={(nodes, searchValue) => {
        // Return a filtered tree — parent preservation is your responsibility
        return customTreeFilter(nodes, searchValue);
      }}
      // ...other props
    />
    ```

    ## TreeSelectMenu (Async Loading)

    Use `TreeSelectMenu` 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 { TreeSelectMenu, TreeSelectMenuValue } from "@servicetitan/anvil2/beta";
    import { Button } from "@servicetitan/anvil2";

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

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

    ### 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}
    <TreeSelectMenu
      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}
      trigger={(props) => <Button {...props}>Select location</Button>}
    />
    ```

    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.

    ## The `trigger` Render Prop

    The `trigger` render prop receives a set of props that must be spread onto the element that opens the menu. The trigger is responsible for rendering its own visible label.

    ```tsx theme={null}
    <TreeSelectMenuSync
      label="Departments"
      options={options}
      value={selected}
      onSelectedOptionsChange={setSelected}
      trigger={({ ref, onClick, onKeyDown, "aria-expanded": ariaExpanded, ...rest }) => (
        <Button ref={ref} onClick={onClick} onKeyDown={onKeyDown} aria-expanded={ariaExpanded} {...rest}>
          Departments
        </Button>
      )}
    />
    ```

    The trigger props provide:

    * `ref` — Ref to the trigger element, used for popover positioning and focus restoration when the menu closes.
    * `onClick` — Toggles the menu open and closed.
    * `onKeyDown` — Handles keyboard interactions (e.g., opening the menu with Enter or arrow keys).
    * `aria-haspopup`, `aria-controls`, `aria-expanded` — ARIA attributes describing the relationship between the trigger and the tree dropdown.
    * `data-state` — Either `"open"` or `"close"`, useful for styling the trigger based on menu state.

    ## 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
    <TreeSelectMenuSync
      selectionMode="linked"
      options={options}
      // ...other props
    />

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

    // Single-select mode — one node at a time
    <TreeSelectMenuSync
      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}
    <TreeSelectMenuSync
      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.

    ## Display Modes

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

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

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

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

    ## Popover Width

    Use `popoverWidth` to control the popover's width:

    ```tsx theme={null}
    // Match the trigger's width (useful when the trigger is a wide button or input)
    <TreeSelectMenuSync popoverWidth="reference" {...props} />

    // Fixed pixel width
    <TreeSelectMenuSync popoverWidth={400} {...props} />

    // Any CSS width value
    <TreeSelectMenuSync popoverWidth="20rem" {...props} />
    ```

    ## Caching

    `TreeSelectMenu` 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
    <TreeSelectMenu cache={{ enabled: false }} {...props} />

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

    ### Clearing the Cache

    Use a ref to imperatively clear the cache:

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

    const ExampleComponent = () => {
      const menuRef = useRef<TreeSelectMenuHandle>(null);

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

      return (
        <>
          <TreeSelectMenu ref={menuRef} {...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 = () => {
      menuRef.current?.invalidate();
    };
    ```

    <Note>`TreeSelectMenuSync` 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)
    <TreeSelectMenu initialLoad="immediate" {...props} />

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

    // Auto (currently equivalent to "immediate")
    <TreeSelectMenu 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)
    <TreeSelectMenuSync defaultExpandLevel={0} options={options} {...props} />

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

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

    For full control, use `expandedIds` and `onExpandedIdsChange`:

    ```tsx theme={null}
    const [expandedIds, setExpandedIds] = useState(new Set(["eng", "design"]));

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

    The imperative handle also provides `expandAll()` and `collapseAll()`:

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

    ## 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}
    <TreeSelectMenu
      virtualize
      label="Large tree"
      loadOptions={loadLargeTree}
      value={selected}
      onSelectedOptionsChange={setSelected}
      trigger={(props) => <Button {...props}>Browse</Button>}
    />
    ```

    ## Disabling Search

    Pass `disableSearch` to hide the search input:

    ```tsx theme={null}
    <TreeSelectMenuSync
      disableSearch
      label="Category"
      options={options}
      value={selected}
      onSelectedOptionsChange={setSelected}
      trigger={(props) => <Button {...props}>Category</Button>}
    />
    ```

    ## Close Callbacks

    Distinguish between an explicit close (Escape, clicking the trigger again) and an implicit close (clicking outside the menu) with `onExplicitClose` and `onImplicitClose`:

    ```tsx theme={null}
    <TreeSelectMenuSync
      onExplicitClose={() => analytics.track("tree_menu_dismissed")}
      onImplicitClose={() => analytics.track("tree_menu_blurred")}
      options={options}
      value={selected}
      onSelectedOptionsChange={setSelected}
      // ...other props
    />
    ```

    Use `onMenuKeyDown` to observe keyboard events that occur while the menu is open.

    ## React Accessibility

    * The trigger element exposes `aria-haspopup="tree"`, `aria-controls`, and `aria-expanded` to describe its relationship to the dropdown.
    * The dropdown uses `role="tree"` with the appropriate `role="treeitem"` rows and `aria-expanded` / `aria-selected` attributes.
    * The required `label` prop is used as the accessible name for the menu and as the dialog title in dialog mode.
    * Focus is restored to the trigger element when the menu closes via Escape or by selecting an option in single-select mode.
  </Tab>

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

    ```tsx theme={null}
    import { useState } from "react";
    import { TreeSelectMenu, TreeSelectMenuValue } from "@servicetitan/anvil2/beta";
    import { Button } from "@servicetitan/anvil2";

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

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

    <ParamField path="label" type="string" required>
      Accessible label for the menu. Used by screen readers and as the dialog title in dialog mode.
    </ParamField>

    <ParamField path="loadOptions" type="(searchValue: string, parentNode?: TreeSelectMenuNode) => TreeSelectMenuNode[] | Promise<TreeSelectMenuNode[]>" 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: TreeSelectMenuValue[]) => void" required>
      Callback fired when the selection changes. Receives the new array of selected values.
    </ParamField>

    <ParamField path="trigger" type="(props: TreeSelectMenuTriggerProps) => ReactElement" required>
      Render function that receives trigger props (`ref`, `onClick`, `onKeyDown`, ARIA attributes, `data-state`) and returns the element that opens the menu. Spread the props onto a button or interactive element.
    </ParamField>

    <ParamField path="value" type="TreeSelectMenuValue[]" 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="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="disableSearch" type="boolean">
      Hides the search input.
    </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="expandedIds" type="Set<string | number>">
      Controlled set of expanded branch node IDs. Use with `onExpandedIdsChange` for full control over expansion state.
    </ParamField>

    <ParamField path="id" type="string">
      The id of the menu. Used to link the trigger and the tree element via ARIA.
    </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="onExpandedIdsChange" type="(ids: Set<string | number>) => void">
      Callback fired when expansion state changes. Use with `expandedIds` for controlled expansion.
    </ParamField>

    <ParamField path="onExplicitClose" type="() => void">
      Callback fired when the menu closes due to an explicit user action (Escape, clicking the trigger to dismiss, or selecting an option in single-select mode). Focus is restored to the trigger.
    </ParamField>

    <ParamField path="onImplicitClose" type="() => void">
      Callback fired when the menu closes implicitly (e.g., clicking outside the menu). Focus is not restored to the trigger.
    </ParamField>

    <ParamField path="onMenuKeyDown" type="(event: KeyboardEvent) => void">
      Callback fired for keyboard events that occur while the menu is open.
    </ParamField>

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

    <ParamField path="popoverWidth" type={`"reference" | number | string`}>
      Width of the popover. Pass `"reference"` to match the trigger element's width, a number for pixels, or any CSS width value.
    </ParamField>

    <ParamField path="searchPlaceholder" type="string">
      Placeholder text for the search input.
    </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="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>
  </Tab>

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

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

    ```tsx theme={null}
    import { useState } from "react";
    import { TreeSelectMenuSync, TreeSelectMenuValue } from "@servicetitan/anvil2/beta";
    import { Button } from "@servicetitan/anvil2";

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

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

      return (
        <TreeSelectMenuSync
          label="Select departments"
          onSelectedOptionsChange={setSelected}
          options={options}
          value={selected}
          trigger={(props) => <Button {...props}>Select departments</Button>}
        />
      );
    };
    ```

    <ParamField path="label" type="string" required>
      Accessible label for the menu. Used by screen readers and as the dialog title in dialog mode.
    </ParamField>

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

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

    <ParamField path="trigger" type="(props: TreeSelectMenuTriggerProps) => ReactElement" required>
      Render function that receives trigger props and returns the element that opens the menu.
    </ParamField>

    <ParamField path="value" type="TreeSelectMenuValue[]" required>
      The currently selected values.
    </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="disableSearch" type="boolean">
      Hides the search input.
    </ParamField>

    <ParamField path="displayMenuAs" type={`"auto" | "popover" | "dialog"`} default="auto">
      How to display the options menu.
    </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: TreeSelectMenuNode[], searchValue: string) => TreeSelectMenuNode[]` — 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="id" type="string">
      The id of the menu.
    </ParamField>

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

    <ParamField path="onExplicitClose" type="() => void">
      Callback fired when the menu closes due to an explicit user action.
    </ParamField>

    <ParamField path="onImplicitClose" type="() => void">
      Callback fired when the menu closes implicitly.
    </ParamField>

    <ParamField path="onMenuKeyDown" type="(event: KeyboardEvent) => void">
      Callback fired for keyboard events that occur while the menu is open.
    </ParamField>

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

    <ParamField path="popoverWidth" type={`"reference" | number | string`}>
      Width of the popover.
    </ParamField>

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

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

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

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

    ```tsx theme={null}
    type TreeSelectMenuNode = {
      /** 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?: TreeSelectMenuNode[] | null;

      /** True number of children for a branch. Lets the menu 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 TreeSelectMenuSync. */
      childCount?: number;

      /** Ancestor ids, root → parent. Selection-ancestry metadata, primarily
       *  meaningful on value objects: lets the menu show correct ancestor
       *  check-states for a selection whose branches aren't loaded yet (e.g. a
       *  restored selection). Omit on loadOptions results. */
      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: TreeSelectMenuNode[] = [
      {
        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="TreeSelectMenuValue Type">
    ## `TreeSelectMenuValue` Type

    The value array contains slim objects with just enough data to identify selected nodes:

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

      /** The node's display text */
      label: string;

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

      /** Ancestor ids, root → parent. Supply on a restored selection so the
       *  menu can show correct ancestor check-states before those branches
       *  load. The menu 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<TreeSelectMenuValue[]>([]);

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

    ### Imperative Handle

    Access imperative methods via a ref:

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

    const menuRef = useRef<TreeSelectMenuHandle>(null);

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

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

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