> ## 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.

# Multi Select Menu – Code

> MultiSelectMenu components provide a searchable dropdown menu for selecting multiple options, attachable to any trigger element, with support for async data loading and client-side filtering.

export const LiveCode = ({children, customHeight, clickToLoad, example, fullWidth, fullHeight, hideCodeInLiveCode, screenshot, screenshotOnly, showCode: showCodeProp}) => {
  const SCREENSHOTS_BASE = "https://servicetitan.github.io/anvil2-docs-live-code/screenshots";
  const STACKBLITZ_BASE = "https://stackblitz.com/github/servicetitan/anvil2-docs-live-code/tree/main/examples";
  const [showCodeBlock, setShowCodeBlock] = useState(showCodeProp ?? false);
  const [isLocalOverride, setIsLocalOverride] = useState(false);
  useEffect(() => {
    const examplePath = `/images/live-code-screenshots-tmp/${example}.png`;
    fetch(examplePath, {
      method: "HEAD"
    }).then(r => {
      if (r.ok) setIsLocalOverride(true);
    }).catch(() => {});
  }, [example]);
  const screenshotBase = isLocalOverride ? "/images/live-code-screenshots-tmp" : SCREENSHOTS_BASE;
  if (screenshotOnly) {
    return <Frame className="flex flex-col">
        <div className="flex dark:hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#FFFFFF"
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>
        <div className="hidden dark:flex" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined,
      background: "#141414"
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>
      </Frame>;
  }
  if (screenshot) {
    return <Frame className="flex flex-col -mb-2">
        <div className="flex dark:hidden bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}.png, ${screenshotBase}/${example}-2x.png 2x`} src={`${screenshotBase}/${example}.png`} alt={example} noZoom />
        </div>

        <div className="hidden dark:flex bg-white dark:bg-codeblock border border-gray-950/10 dark:border-white/10 dark:twoslash-dark rounded-2xl overflow-hidden" style={{
      background: "#141414",
      justifyContent: "center",
      alignItems: "center",
      width: fullWidth ? "100%" : "50%",
      minHeight: fullHeight ? "284px" : undefined
    }}>
          <img srcset={`${screenshotBase}/${example}-dark.png, ${screenshotBase}/${example}-dark-2x.png 2x`} src={`${screenshotBase}/${example}-dark.png`} alt={example} noZoom />
        </div>

        <div className="flex justify-end items-center text-xs py-2 px-1 gap-4">
          {!showCodeProp ? <button className="inline-flex justify-end items-center text-gray-700 dark:text-gray-50 hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1 cursor-pointer" onClick={() => setShowCodeBlock(!showCodeBlock)} style={{
      appearance: "none"
    }}>
              <Icon icon="code" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
              <span>{showCodeBlock ? "Hide code" : "Show code"}</span>
            </button> : null}

          <a className="inline-flex justify-end items-center hover:text-blue-500 dark:hover:text-blue-300 transition-colors group self-end gap-1" href={`${STACKBLITZ_BASE}/${example}?file=src/App.tsx`} target="_blank" rel="noreferrer">
            <Icon icon="bolt" size="12px" className="group-hover:bg-blue-500 dark:group-hover:bg-blue-300" />
            <span>StackBlitz demo</span>
          </a>
        </div>

        <div className="grid transition-[grid-template-rows] duration-300 ease-in-out overflow-auto overflow-y-hidden overflow-x-auto" style={showCodeBlock ? {
      gridTemplateRows: "1fr"
    } : {
      gridTemplateRows: "0fr"
    }}>
          <div style={{
      minHeight: 0,
      overflowX: "auto",
      overflowY: "hidden",
      marginBlockStart: "-1.25rem",
      marginBlockEnd: "-1.5rem"
    }}>
            {children}
          </div>
        </div>
      </Frame>;
  } else {
    return <div style={{
      display: "flex",
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px",
      resize: "vertical",
      overflow: "auto"
    }}>
        <iframe title={example} style={{
      flex: 1,
      width: fullWidth ? "100%" : "50%",
      minHeight: customHeight ? customHeight : "316px"
    }} src={`${STACKBLITZ_BASE}/${example}?embed=1&hideNavigation=1&hideExplorer=1&terminalHeight=0&file=src/App.tsx${clickToLoad ? "&ctl=1" : ""}${hideCodeInLiveCode ? "&view=preview" : ""}`} allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts" />
      </div>;
  }
};

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

    * **`MultiSelectMenu`** — For async data loading with support for pagination (page-based, offset-based, or group-based lazy loading)
      * Includes automatic debouncing of the search input (configurable via the `debounceMs` prop)
      * Includes automatic caching of the search input (configurable via the `cache` prop)
    * **`MultiSelectMenuSync`** — For client-side filtering of static option arrays

    Both components attach a searchable dropdown to any trigger element via a `trigger` render prop. The menu stays open after selecting an option, allowing multiple selections. Use `MultiSelectMenu` when you need multi-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 chip display? Use [`MultiSelectField`](/docs/web/components/multi-select-field/code) instead.
    </Note>

    ## MultiSelectMenuSync (Static Options)

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

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

    const options = [
      { id: 1, label: "Option One" },
      { id: 2, label: "Option Two" },
      { id: 3, label: "Option Three" },
    ];

    const ExampleComponent = () => {
      const [selectedOptions, setSelectedOptions] = useState([]);

      return (
        <MultiSelectMenuSync
          label="Options"
          options={options}
          value={selectedOptions}
          onSelectedOptionsChange={setSelectedOptions}
          trigger={(props) => (
            <Button {...props}>
              {selectedOptions.length > 0
                ? `${selectedOptions.length} selected`
                : "Select options"}
            </Button>
          )}
        />
      );
    };
    ```

    ### Filtering and Sorting

    By default, `MultiSelectMenuSync` uses [match-sorter](https://github.com/kentcdodds/match-sorter) to filter options by their `label` and `searchText` fields. Results are also ranked by match quality, so the best matches appear first. Before any search is performed, options appear in the order they are supplied.
    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 (e.g., change which keys are matched or adjust ranking):

    ```tsx theme={null}
    <MultiSelectMenuSync
      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 sort order. The returned array determines the exact order options appear in the dropdown:

    ```tsx theme={null}
    <MultiSelectMenuSync
      options={options}
      filter={(options, searchValue) => {
        return options.filter((option) =>
          option.label?.toLowerCase().includes(searchValue.toLowerCase())
        );
      }}
      // ...other props
    />
    ```

    ## MultiSelectMenu (Async Loading)

    Use `MultiSelectMenu` when options need to be fetched from an API or when dealing with large datasets that require server-side filtering.

    ### Basic Usage

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

    const ExampleComponent = () => {
      const [selectedOptions, setSelectedOptions] = useState([]);

      return (
        <MultiSelectMenu
          label="Tags"
          searchPlaceholder="Search tags..."
          loadOptions={async (searchValue) => {
            const response = await fetch(`/api/tags?q=${searchValue}`);
            const tags = await response.json();
            return tags.map((tag) => ({
              id: tag.id,
              label: tag.name,
            }));
          }}
          value={selectedOptions}
          onSelectedOptionsChange={setSelectedOptions}
          trigger={(props) => (
            <Button {...props}>
              {selectedOptions.length > 0
                ? `${selectedOptions.length} tags selected`
                : "Select tags"}
            </Button>
          )}
        />
      );
    };
    ```

    ### Lazy Loading Modes

    `MultiSelectMenu` supports three lazy loading modes for paginated data:

    #### Page-based Pagination

    ```tsx theme={null}
    <MultiSelectMenu
      lazy="page"
      lazyOptions={{ pageSize: 10 }}
      loadOptions={async (searchValue, pageNumber, pageSize) => {
        const response = await fetch(
          `/api/items?q=${searchValue}&page=${pageNumber}&size=${pageSize}`
        );
        const { data, totalCount } = await response.json();
        return {
          options: data.map((item) => ({ id: item.id, label: item.name })),
          hasMore: pageNumber * pageSize + data.length < totalCount,
        };
      }}
      // ...other props
    />
    ```

    #### Offset-based Pagination

    ```tsx theme={null}
    <MultiSelectMenu
      lazy="offset"
      lazyOptions={{ limit: 15 }}
      loadOptions={async (searchValue, offset, limit) => {
        const response = await fetch(
          `/api/items?q=${searchValue}&offset=${offset}&limit=${limit}`
        );
        const { data, totalCount } = await response.json();
        return {
          options: data.map((item) => ({ id: item.id, label: item.name })),
          hasMore: offset + limit < totalCount,
        };
      }}
      // ...other props
    />
    ```

    #### Group-based Loading

    For loading grouped options incrementally:

    ```tsx theme={null}
    <MultiSelectMenu
      lazy="group"
      groupToString={(groupValue) => String(groupValue)}
      loadOptions={async (searchValue, previousGroupKey) => {
        const response = await fetch(
          `/api/items?q=${searchValue}&afterGroup=${previousGroupKey || ""}`
        );
        const { data, hasMore } = await response.json();
        return {
          options: data.map((item) => ({
            id: item.id,
            label: item.name,
            group: item.category,
          })),
          hasMore,
        };
      }}
      // ...other props
    />
    ```

    ## Select All

    Enable bulk selection with the `selectAll` prop.

    <Note>
      Select All is shown only when the search input is empty.
    </Note>

    ### MultiSelectMenu

    With `MultiSelectMenu`, the parent component is responsible for handling the select/deselect logic via `onClick` and managing the `checkState`:

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

    const allOptions = [
      { id: 1, label: "Option One" },
      { id: 2, label: "Option Two" },
      { id: 3, label: "Option Three" },
    ];

    const ExampleComponent = () => {
      const [selectedOptions, setSelectedOptions] = useState([]);

      const handleSelectAll = () => {
        if (selectedOptions.length === allOptions.length) {
          setSelectedOptions([]);
        } else {
          setSelectedOptions(allOptions);
        }
      };

      return (
        <MultiSelectMenu
          label="Items"
          loadOptions={async () => allOptions}
          value={selectedOptions}
          onSelectedOptionsChange={setSelectedOptions}
          selectAll={{
            label: "Select All Items",
            onClick: handleSelectAll,
            checkState:
              selectedOptions.length === allOptions.length
                ? "checked"
                : selectedOptions.length > 0
                  ? "indeterminate"
                  : "unchecked",
          }}
          trigger={(props) => (
            <Button {...props}>
              {selectedOptions.length > 0
                ? `${selectedOptions.length} selected`
                : "Select items"}
            </Button>
          )}
        />
      );
    };
    ```

    ### MultiSelectMenuSync

    `MultiSelectMenuSync` provides a simplified `selectAll` prop. Click handling and check state are managed automatically:

    ```tsx theme={null}
    // Enable with default label
    <MultiSelectMenuSync
      label="Tags"
      options={options}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
      selectAll
      trigger={(props) => <Button {...props}>Select tags</Button>}
    />

    // Enable with custom label
    <MultiSelectMenuSync
      label="Tags"
      options={options}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
      selectAll={{ label: "Select All Tags" }}
      trigger={(props) => <Button {...props}>Select tags</Button>}
    />

    // Enable with dynamic label based on check state
    <MultiSelectMenuSync
      label="Tags"
      options={options}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
      selectAll={{ label: (checked) => checked ? "Deselect All" : "Select All" }}
      trigger={(props) => <Button {...props}>Select tags</Button>}
    />
    ```

    ## Select Filtered

    Enable selection of options matching the current search term with the `selectFiltered` prop. Select All and Select Filtered are mutually exclusive: Select All is shown when the search input is empty, and Select Filtered is shown when a search term is active.

    ### MultiSelectMenu

    With `MultiSelectMenu`, provide a function that receives the current `searchValue` and returns a config object with `onClick`, `checkState`, and an optional `label`:

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

    const allOptions = [
      { id: 1, label: "Apple" },
      { id: 2, label: "Apricot" },
      { id: 3, label: "Banana" },
      { id: 4, label: "Cherry" },
    ];

    const filterBySearch = (searchValue: string) =>
      allOptions.filter((opt) =>
        opt.label.toLowerCase().includes(searchValue.toLowerCase()),
      );

    const ExampleComponent = () => {
      const [selectedOptions, setSelectedOptions] = useState([]);
      const selectedIds = new Set(selectedOptions.map((o) => o.id));

      return (
        <MultiSelectMenu
          label="Fruits"
          loadOptions={async (searchValue) => filterBySearch(searchValue)}
          value={selectedOptions}
          onSelectedOptionsChange={setSelectedOptions}
          selectFiltered={(searchValue) => {
            const filtered = filterBySearch(searchValue);
            const allFilteredSelected =
              filtered.length > 0 &&
              filtered.every((o) => selectedIds.has(o.id));
            const someFilteredSelected = filtered.some((o) =>
              selectedIds.has(o.id),
            );
            return {
              onClick: () => {
                if (allFilteredSelected) {
                  const filteredIds = new Set(filtered.map((o) => o.id));
                  setSelectedOptions(
                    selectedOptions.filter((o) => !filteredIds.has(o.id)),
                  );
                } else {
                  const merged = [...selectedOptions];
                  for (const opt of filtered) {
                    if (!selectedIds.has(opt.id)) merged.push(opt);
                  }
                  setSelectedOptions(merged);
                }
              },
              checkState: allFilteredSelected
                ? "checked"
                : someFilteredSelected
                  ? "indeterminate"
                  : "unchecked",
            };
          }}
          trigger={(props) => (
            <Button {...props}>
              {selectedOptions.length > 0
                ? `${selectedOptions.length} selected`
                : "Select fruits"}
            </Button>
          )}
        />
      );
    };
    ```

    ### MultiSelectMenuSync

    `MultiSelectMenuSync` provides a simplified `selectFiltered` prop. Click handling and check state are managed automatically based on the filtered options and current selection:

    ```tsx theme={null}
    // Enable with default dynamic label
    <MultiSelectMenuSync
      label="Fruits"
      options={options}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
      selectFiltered
      trigger={(props) => <Button {...props}>Select fruits</Button>}
    />

    // Enable with custom label based on search value
    <MultiSelectMenuSync
      label="Fruits"
      options={options}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
      selectFiltered={(searchValue) => ({
        label: `Select items matching "${searchValue}"`,
      })}
      trigger={(props) => <Button {...props}>Select fruits</Button>}
    />

    // Combine with selectAll for full bulk selection
    <MultiSelectMenuSync
      label="Fruits"
      options={options}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
      selectAll
      selectFiltered
      trigger={(props) => <Button {...props}>Select fruits</Button>}
    />
    ```

    ## Display Modes

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

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

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

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

    ## Popover Width

    Control the width of the popover using the `width` prop:

    ```tsx theme={null}
    // Fixed pixel width
    <MultiSelectMenu width={300} {...props} />

    // CSS string value
    <MultiSelectMenu width="20rem" {...props} />
    ```

    ## Caching

    `MultiSelectMenu` caches `loadOptions` results by default. Configure caching behavior:

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

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

    ### Clearing the Cache

    Use a ref to imperatively clear the cache:

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

    const ExampleComponent = () => {
      const multiSelectMenuRef = useRef(null);

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

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

    ### Invalidating Options

    Call `invalidate()` to clear the cache and reload options from the data source. Use this when the underlying data has changed and the component needs to reflect the update:

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

    const ExampleComponent = () => {
      const multiSelectMenuRef = useRef(null);

      const handleDataSourceChange = () => {
        multiSelectMenuRef.current?.invalidate();
      };

      return (
        <MultiSelectMenu ref={multiSelectMenuRef} {...props} />
      );
    };
    ```

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

    ## Initial Load Behavior

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

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

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

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

    ## Disabling Search

    Pass `disableSearch` to remove the search input from inside the menu. The menu renders only the option list, and keyboard focus moves directly to the list container.

    This is useful when the option list is short and well-known or you are integrating with an API that does not support search.

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

    const options = [
      { id: 1, label: "Red" },
      { id: 2, label: "Green" },
      { id: 3, label: "Blue" },
      { id: 4, label: "Yellow" },
    ];

    const ExampleComponent = () => {
      const [selectedOptions, setSelectedOptions] = useState([]);

      return (
        <MultiSelectMenuSync
          disableSearch
          selectAll
          label="Colors"
          options={options}
          value={selectedOptions}
          onSelectedOptionsChange={setSelectedOptions}
          trigger={(props) => (
            <Button {...props}>
              {selectedOptions.length > 0
                ? `${selectedOptions.length} colors selected`
                : "Select colors"}
            </Button>
          )}
        />
      );
    };
    ```

    <Note>
      When `disableSearch` is enabled, `selectFiltered` has no effect since there is no search input to produce filtered results.
    </Note>

    ## Disabled Options

    Individual options can be disabled by setting `disabled: true` on the option:

    ```tsx theme={null}
    const options = [
      { id: 1, label: "Available Option" },
      { id: 2, label: "Unavailable Option", disabled: true },
      { id: 3, label: "Another Available Option" },
    ];

    <MultiSelectMenuSync
      label="Select options"
      options={options}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
      trigger={(props) => <Button {...props}>Choose</Button>}
    />
    ```

    ## Pinned Options

    Pin frequently used or suggested options above the list using the `pinned` prop. Each pinned section requires a `label` and an `options` value, which can be a static array or a dynamic loader function.

    ### Static Pinned Options

    Pass an object with `label` and a static `options` array:

    ```tsx theme={null}
    <MultiSelectMenu
      label="Books"
      pinned={{
        label: "Favorites",
        options: [
          { id: "fav-1", label: "The Martian" },
          { id: "fav-2", label: "Dune" },
        ],
      }}
      loadOptions={fetchBooks}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
      trigger={(props) => <Button {...props}>Select books</Button>}
    />
    ```

    ### Dynamic Pinned Options

    Pass a function as `options` to compute pinned options based on the current search value:

    ```tsx theme={null}
    <MultiSelectMenu
      label="Books"
      pinned={{
        label: "AI Suggestions",
        options: async (searchValue) => {
          const suggestions = await fetchAISuggestions(searchValue);
          return suggestions.map((s) => ({ id: s.id, label: s.title }));
        },
      }}
      loadOptions={fetchBooks}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
      trigger={(props) => <Button {...props}>Select books</Button>}
    />
    ```

    By default, the loader re-runs whenever the search value changes. Set `searchReactive: false` to call the loader once and reuse the result across all search values:

    ```tsx theme={null}
    <MultiSelectMenu
      label="Books"
      pinned={{
        label: "Your Favorites",
        options: async () => {
          return await fetchFavorites();
        },
        searchReactive: false,
      }}
      loadOptions={fetchBooks}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
      trigger={(props) => <Button {...props}>Select books</Button>}
    />
    ```

    ### Multiple Pinned Sections

    Pass an array of pinned section objects:

    ```tsx theme={null}
    <MultiSelectMenu
      label="Books"
      pinned={[
        {
          label: "AI Suggestions",
          options: async (searchValue) => {
            const suggestions = await fetchAISuggestions(searchValue);
            return suggestions.map((s) => ({ id: s.id, label: s.title }));
          },
        },
        {
          label: "Your Favorites",
          options: [
            { id: "fav-1", label: "Dune" },
            { id: "fav-2", label: "Foundation" },
          ],
        },
      ]}
      loadOptions={fetchBooks}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
      trigger={(props) => <Button {...props}>Select books</Button>}
    />
    ```

    ## Grouping Options

    Options can be organized into visual groups by adding a `group` property to each option. Groups appear as labeled sections in the dropdown.

    ### Basic Grouping

    Add a `group` property to options and provide a `groupToString` function to display group labels:

    ```tsx theme={null}
    const options = [
      { id: 1, label: "Apple", group: "fruits" },
      { id: 2, label: "Banana", group: "fruits" },
      { id: 3, label: "Carrot", group: "vegetables" },
      { id: 4, label: "Broccoli", group: "vegetables" },
    ];

    <MultiSelectMenu
      label="Food"
      loadOptions={async () => options}
      groupToString={(group) =>
        group === "fruits" ? "Fruits" : "Vegetables"
      }
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
      trigger={(props) => <Button {...props}>Select food</Button>}
    />
    ```

    ### Group Sorting

    Use `groupSorter` to control the order of groups. By default, groups appear in the order they are first encountered in the options array. This prop is available on `MultiSelectMenuSync` and non-lazy `MultiSelectMenu`:

    <Warning>
      Avoid using `groupSorter` with `lazy="group"`. When groups load incrementally from the server and get re-sorted, the menu content shifts unexpectedly, creating a disorienting user experience. Instead, have your server return groups in the desired order.
    </Warning>

    ```tsx theme={null}
    const options = [
      { id: 1, label: "Item A", group: 3 },
      { id: 2, label: "Item B", group: 1 },
      { id: 3, label: "Item C", group: 2 },
    ];

    <MultiSelectMenu
      label="Items"
      loadOptions={async () => options}
      groupToString={(group) => `Priority ${group}`}
      groupSorter={(a, b) => Number(a) - Number(b)}
      value={selectedOptions}
      onSelectedOptionsChange={setSelectedOptions}
      trigger={(props) => <Button {...props}>Select items</Button>}
    />
    ```

    ## Virtualization

    By default, all dropdown options render to the DOM at once. This works well for typical lists but degrades performance with very large option sets. Pass `virtualize` to enable windowed rendering, which only renders the items currently visible in the scroll viewport plus a small overscan buffer.

    Consider enabling `virtualize` when the dropdown feels sluggish to open or keyboard navigation becomes laggy. These symptoms typically appear around 200-500 items depending on device performance and item complexity.

    Virtualization works with all existing features including lazy loading, pinned options, grouping, select-all/select-filtered bulk actions, and keyboard navigation.

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

    const options = Array.from({ length: 5000 }, (_, i) => ({
      id: i,
      label: `Option ${i + 1}`,
    }));

    const ExampleComponent = () => {
      const [selectedOptions, setSelectedOptions] = useState([]);

      return (
        <MultiSelectMenuSync
          virtualize
          selectAll
          label="Large dataset"
          options={options}
          value={selectedOptions}
          onSelectedOptionsChange={setSelectedOptions}
          trigger={(props) => (
            <Button {...props}>
              {selectedOptions.length > 0
                ? `${selectedOptions.length} selected`
                : "Select options"}
            </Button>
          )}
        />
      );
    };
    ```

    ## Adding New Items

    Provide an `onAddNewItem` handler to render an "Add new item" button below the option list. This affordance lets users create a value that does not exist yet — a new tag or filter value — quickly from the menu.

    The button sits in a footer region inside the menu. Clicking or activating it via keyboard closes the menu and invokes the `onAddNewItem` handler with the current search text.

    From here, it is your responsibility to launch a `Dialog` (or `Drawer`, or any other overlay) for collecting the new item's details, append the result to the option source, invalidate the cache, and update selection state.

    You may pair `onAddNewItem` with `addItemLabel` to control the button text. The label accepts a static node or a function of the current search text — for example, returning `Add "foo"` when the user has typed `foo`.

    <LiveCode showCode example="multiselectmenu-add-new-item" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { useEffect, useRef, useState } from "react";
      import {
        MultiSelectMenu,
        MultiSelectMenuHandle,
        MultiSelectMenuOption,
      } from "@servicetitan/anvil2/beta";
      import { Button, Dialog, Flex, TextField } from "@servicetitan/anvil2";

      type Tag = MultiSelectMenuOption & { color: string };

      function App() {
        const [tags, setTags] = useState<Tag[]>([
          { id: "bug", label: "bug", color: "#e53935" },
          { id: "feature", label: "feature", color: "#1e88e5" },
          { id: "browser", label: "browser", color: "#7b1fa2" },
          { id: "performance", label: "performance", color: "#f9a825" },
          { id: "docs", label: "docs", color: "#43a047" },
        ]);
        const [activeFilters, setActiveFilters] = useState<Tag[]>([]);
        const menuRef = useRef<MultiSelectMenuHandle>(null);
        const containerRef = useRef<HTMLDivElement>(null);

        const [dialogOpen, setDialogOpen] = useState(false);
        const [draftLabel, setDraftLabel] = useState("");

        // Open the menu on mount so the Add-new button is visible in screenshots.
        useEffect(() => {
          const timer = setTimeout(() => {
            const trigger = containerRef.current?.querySelector("button");
            trigger?.focus();
            trigger?.click();
          }, 100);
          return () => clearTimeout(timer);
        }, []);

        const openTagDialog = (initialLabel: string) => {
          setDraftLabel(initialLabel.trim().toLowerCase().replace(/\s+/g, "-"));
          setDialogOpen(true);
        };

        const saveTag = () => {
          const newTag: Tag = {
            id: draftLabel,
            label: draftLabel,
            color: "#43a047",
          };
          setTags((prev) => [...prev, newTag]);
          setActiveFilters((prev) => [...prev, newTag]);
          menuRef.current?.invalidate();
          setDialogOpen(false);
        };

        return (
          <div ref={containerRef} style={{ minWidth: "384px", minHeight: "420px" }}>
            <MultiSelectMenu
              ref={menuRef}
              label="Filter by tag"
              searchPlaceholder="Search or create a tag..."
              initialLoad="open"
              loadOptions={async (searchValue) =>
                searchValue
                  ? tags.filter((t) =>
                      t.label.toLowerCase().includes(searchValue.toLowerCase()),
                    )
                  : tags
              }
              value={activeFilters}
              onSelectedOptionsChange={(opts) => setActiveFilters(opts as Tag[])}
              addItemLabel={(searchText) =>
                searchText ? `Create tag "${searchText}"` : "Create new tag"
              }
              onAddNewItem={openTagDialog}
              trigger={(props) => (
                <Button {...props} appearance="secondary">
                  {activeFilters.length > 0
                    ? `Filter (${activeFilters.length})`
                    : "Filter"}
                </Button>
              )}
            />

            <Dialog open={dialogOpen} onClose={() => setDialogOpen(false)}>
              <Dialog.Header>Create new tag</Dialog.Header>
              <Dialog.Content>
                <Flex direction="column" gap="3">
                  <TextField
                    label="Tag name"
                    description="Lowercase, hyphenated."
                    required
                    value={draftLabel}
                    onChange={(e) => setDraftLabel(e.target.value)}
                  />
                </Flex>
              </Dialog.Content>
              <Dialog.Footer>
                <Dialog.CancelButton appearance="ghost">Cancel</Dialog.CancelButton>
                <Button
                  appearance="primary"
                  onClick={saveTag}
                  disabled={!draftLabel.trim()}
                >
                  Create tag
                </Button>
              </Dialog.Footer>
            </Dialog>
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    <Note>
      Call `invalidate()` on the imperative handle after appending the new option.
      This clears the cached search results and forces a refetch the next time the
      menu opens, so the new item appears immediately. `clearCache()` alone wipes
      the cache but keeps the previously displayed options on screen until
      something else triggers a reload.
    </Note>
  </Tab>

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

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

    const ExampleComponent = () => {
      const [selectedOptions, setSelectedOptions] = useState([]);

      return (
        <MultiSelectMenu
          label="Tags"
          loadOptions={async (searchValue) => {
            const response = await fetch(`/api/tags?q=${searchValue}`);
            const tags = await response.json();
            return tags.map((tag) => ({
              id: tag.id,
              label: tag.name,
            }));
          }}
          onSelectedOptionsChange={setSelectedOptions}
          value={selectedOptions}
          trigger={(props) => (
            <Button {...props}>
              {selectedOptions.length > 0
                ? `${selectedOptions.length} selected`
                : "Select tags"}
            </Button>
          )}
        />
      );
    };
    ```

    <ParamField path="label" type="string" required>
      Accessible label for the menu. Used by screen readers and as the dialog title on mobile. Not rendered visually in the trigger.
    </ParamField>

    <ParamField path="loadOptions" type="function" required>
      Function to load options. The signature depends on the `lazy` mode:

      * **Non-lazy**: `(searchValue: string) => MultiSelectMenuOption[] | Promise<MultiSelectMenuOption[]>`
      * **Page-based**: `(searchValue: string, pageNumber: number, pageSize: number) => { options: MultiSelectMenuOption[], hasMore?: boolean }`
      * **Offset-based**: `(searchValue: string, offset: number, limit: number) => { options: MultiSelectMenuOption[], hasMore?: boolean }`
      * **Group-based**: `(searchValue: string, previousGroupKey: string | number | null) => { options: MultiSelectMenuGroupedOption[], hasMore?: boolean }`
    </ParamField>

    <ParamField path="onSelectedOptionsChange" type="(options: MultiSelectMenuOption[]) => void" required>
      Callback fired when the selected options change. Receives the new array of selected options.
    </ParamField>

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

    <ParamField path="value" type="MultiSelectMenuOption[]" required>
      The currently selected options. Must be controlled state.
    </ParamField>

    <ParamField path="addItemLabel" type="ReactNode | (searchText: string) => ReactNode">
      Label rendered inside the "Add new item" button. May be a static node or a callback that receives the current search text and returns a dynamic label (for example, `Add "${searchText}"`). Defaults to `"Add new item"` when omitted but `onAddNewItem` is provided.
    </ParamField>

    <ParamField path="cache" type="MultiSelectMenuCacheOptions">
      Configuration for caching `loadOptions` results:

      * `enabled` — Whether caching is enabled (default: `true`)
      * `maxSize` — Maximum number of search values to cache before clearing (default: `15`)
    </ParamField>

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

    <ParamField path="disableSearch" type="boolean" default="false">
      Removes the search input from inside the menu. When true, the menu renders only the option list and keyboard focus moves to the list container. The `selectAll` bulk action remains available; `selectFiltered` has no effect.
    </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="groupSorter" type="(a: MultiSelectMenuGroupByValue, b: MultiSelectMenuGroupByValue) => number">
      Function to compare two group values for sorting. When provided, groups are sorted using this comparator. Without this, groups appear in the order they are first encountered. Best used with `MultiSelectMenuSync` or non-lazy `MultiSelectMenu`. Avoid using with `lazy="group"` as it causes the menu to shift unexpectedly when new groups load.
    </ParamField>

    <ParamField path="groupToString" type="(groupValue: MultiSelectMenuGroupByValue) => string">
      Function to convert group values to display labels. Only used with grouped options. `MultiSelectMenuGroupByValue` is `string | number`.
    </ParamField>

    <ParamField path="id" type="string">
      The id of the multi-select menu.
    </ParamField>

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

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

    <ParamField path="lazy" type={`"page" | "offset" | "group" | false`} default="false">
      Enables lazy loading with the specified pagination strategy.
    </ParamField>

    <ParamField path="lazyOptions" type="object">
      Configuration for lazy loading:

      * **Page mode**: `{ pageSize?: number }` (default pageSize: 20)
      * **Offset mode**: `{ limit?: number }` (default limit: 20)
      * **Group mode**: `{}`
    </ParamField>

    <ParamField path="onAddNewItem" type="(searchText: string) => void">
      Click handler for the "Add new item" button. The button is only rendered when this prop is provided. Receives the current search text. The menu closes synchronously on click and the handler runs immediately after — typically opening a Dialog or Drawer to collect the new item's details. Consumers own overlay presentation, focus return, option list updates, and selection state. See the [Adding New Items](#adding-new-items) section for a full example.
    </ParamField>

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

    <ParamField path="pinned" type="MultiSelectMenuPinnedOptions">
      Options to pin to the top of the list. Accepts a single section object or an array of section objects. Each section requires:

      * `label` — Display label for the section header
      * `options` — Static array (`MultiSelectMenuOption[]`) or dynamic loader function (`(searchValue: string) => MultiSelectMenuOption[] | Promise<MultiSelectMenuOption[]>`)
      * `searchReactive` — (Optional) Whether to re-call the loader when the search value changes. Defaults to `true`. Set to `false` to call the loader once and reuse the result.
      * `cacheSize` — (Optional) Maximum number of search results to cache per section. Defaults to `15`. Only applies when `searchReactive` is `true`.
    </ParamField>

    <ParamField path="searchPlaceholder" type="string">
      Placeholder text shown in the search input inside the menu.
    </ParamField>

    <ParamField path="searchValue" type="string">
      Controlled search value. In most cases you do not need this — lean on the `searchValue` parameter in `loadOptions` instead.
    </ParamField>

    <ParamField path="selectAll" type="object">
      Configuration for the "Select All" option:

      * `label` — The label to display (default: "Select All")
      * `onClick` — Callback when clicked. Parent component handles select/deselect logic.
      * `checkState` — State: `true`/`"checked"`, `false`/`"unchecked"`, `"indeterminate"`, or `"loading"`
    </ParamField>

    <ParamField path="selectFiltered" type="(searchValue: string) => { label?, onClick, checkState }">
      Function that receives the current search value and returns configuration for the "Select Filtered" option (mutually exclusive with `selectAll`, shown when a search term is active):

      * `label` — The label to display. Default: `Select items matching "${searchValue}"`
      * `onClick` — Callback when clicked. Parent component handles select/deselect logic for filtered items.
      * `checkState` — State: `true`/`"checked"`, `false`/`"unchecked"`, `"indeterminate"`, or `"loading"`
    </ParamField>

    <ParamField path="virtualize" type="boolean" default="false">
      Enables windowed rendering for the dropdown list. Improves performance for large option sets by only rendering visible items. Consider enabling when the dropdown feels sluggish to open or keyboard navigation becomes laggy, typically around 200-500+ items.
    </ParamField>

    <ParamField path="width" type="number | string" default="320">
      Width of the popover menu. Accepts a number (pixels) or a CSS string value (e.g., `"20rem"`).
    </ParamField>
  </Tab>

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

    `MultiSelectMenuSync` accepts all props from `MultiSelectMenu` except `loadOptions`, `lazy`, `debounceMs`, `cache`, `initialLoad`, `selectAll`, and `selectFiltered`, plus the following:

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

    const options = [
      { id: 1, label: "Option One" },
      { id: 2, label: "Option Two" },
      { id: 3, label: "Option Three" },
    ];

    const ExampleComponent = () => {
      const [selectedOptions, setSelectedOptions] = useState([]);

      return (
        <MultiSelectMenuSync
          label="Options"
          onSelectedOptionsChange={setSelectedOptions}
          options={options}
          value={selectedOptions}
          trigger={(props) => (
            <Button {...props}>
              {selectedOptions.length > 0
                ? `${selectedOptions.length} selected`
                : "Select options"}
            </Button>
          )}
        />
      );
    };
    ```

    <ParamField path="label" type="string" required>
      Accessible label for the menu.
    </ParamField>

    <ParamField path="onSelectedOptionsChange" type="(options: MultiSelectMenuOption[]) => void" required>
      Callback fired when the selected options change. Receives the new array of selected options.
    </ParamField>

    <ParamField path="options" type="MultiSelectMenuOption[]" required>
      The array of options to display in the multi-select menu.
    </ParamField>

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

    <ParamField path="value" type="MultiSelectMenuOption[]" required>
      The currently selected options. Must be controlled state.
    </ParamField>

    <ParamField path="addItemLabel" type="ReactNode | (searchText: string) => ReactNode">
      Label rendered inside the "Add new item" button. May be a static node or a callback that receives the current search text and returns a dynamic label (for example, `Add "${searchText}"`). Defaults to `"Add new item"` when omitted but `onAddNewItem` is provided.
    </ParamField>

    <ParamField path="disableSearch" type="boolean" default="false">
      Removes the search input from inside the menu. When true, the menu renders only the option list. The `selectAll` bulk action remains available; `selectFiltered` has no effect.
    </ParamField>

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

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

      * A function: `(options: MultiSelectMenuOption[], searchValue: string) => MultiSelectMenuOption[]` — receives all options and returns the filtered subset in the desired display order
      * A [match-sorter options object](https://github.com/kentcdodds/match-sorter#options) to customize the default match-sorter behavior (e.g., which keys to match against, ranking strategy)

      Before any search is performed, options appear in the order they are supplied. Default: Filters by `label` and `searchText` fields using match-sorter, with results ranked by match quality.
    </ParamField>

    <ParamField path="groupSorter" type="(a: MultiSelectMenuGroupByValue, b: MultiSelectMenuGroupByValue) => number">
      Function to compare two group values for sorting. When provided, options are sorted by group using this comparator, then by match-sort order within each group. Ungrouped options appear last.
    </ParamField>

    <ParamField path="groupToString" type="(groupValue: MultiSelectMenuGroupByValue) => string">
      Function to convert group values to display labels. Only used with grouped options. `MultiSelectMenuGroupByValue` is `string | number`.
    </ParamField>

    <ParamField path="id" type="string">
      The id of the multi-select menu.
    </ParamField>

    <ParamField path="onAddNewItem" type="(searchText: string) => void">
      Click handler for the "Add new item" button. The button is only rendered when this prop is provided. Receives the current search text. The menu closes synchronously on click and the handler runs immediately after — typically opening a Dialog or Drawer to collect the new item's details. Consumers own overlay presentation, focus return, option list updates, and selection state. See the [Adding New Items](#adding-new-items) section for a full example.
    </ParamField>

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

    <ParamField path="pinned" type="MultiSelectMenuPinnedOptions">
      Options to pin to the top of the list. Accepts the same section object format as `MultiSelectMenu`'s `pinned` prop.
    </ParamField>

    <ParamField path="searchPlaceholder" type="string">
      Placeholder text shown in the search input inside the menu.
    </ParamField>

    <ParamField path="searchValue" type="string">
      Controlled search value. In most cases you do not need this.
    </ParamField>

    <ParamField path="selectAll" type="boolean | { label?: string | ((checked: boolean) => string) }">
      Enables the "Select All" option at the top of the list. Can be:

      * `true` — Enable with default label "Select All"
      * `{ label: "Custom Label" }` — Enable with custom static label
      * `{ label: (checked) => checked ? "Deselect All" : "Select All" }` — Enable with dynamic label based on check state

      Click handling and check state are managed automatically based on comparing `options` with `value`.
    </ParamField>

    <ParamField path="selectFiltered" type="boolean | (searchValue: string) => { label?: string }">
      Enables the "Select Filtered" option when a search term is active (mutually exclusive with `selectAll`). Can be:

      * `true` — Enable with default dynamic label
      * `(searchValue) => ({ label })` — A function that receives the search value and returns a config with an optional custom label

      Click handling and check state are managed automatically based on comparing filtered options with `value`.
    </ParamField>

    <ParamField path="virtualize" type="boolean" default="false">
      Enables windowed rendering for the dropdown list. Improves performance for large option sets by only rendering visible items.
    </ParamField>

    <ParamField path="width" type="number | string">
      Width of the popover menu. Accepts a number (pixels) or a CSS string value.
    </ParamField>
  </Tab>

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

    Options passed to or returned from `MultiSelectMenu` components must conform to this shape. This is the same type as `SelectFieldOption`.

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

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

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

      /** Group key for grouped options (required when using lazy="group") */
      group?: string | number;

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

      /** Optional rich content configuration */
      content?: {
        title?: string;
        description?: string;
        chips?: Pick<ChipProps, "label" | "color" | "textWrap">[];
        avatar?: Pick<AvatarProps, "name" | "status" | "color" | "image">;
        icon?: Pick<IconProps, "svg" | "color">;
      };
    };
    ```

    ### Content Properties

    The `content` object supports the following optional properties:

    * `title` — Replaces the `label` as the primary display text in the dropdown.
    * `description` — Secondary text displayed below the title.
    * `chips` — Array of chip configurations rendered below the title and description. Each chip accepts `label`, `color`, and `textWrap`.
    * `avatar` — Renders an avatar to the left of the option text. Accepts `name`, `status`, `color`, and `image`.
    * `icon` — Renders an icon to the right of the option text. Accepts `svg` and `color`.

    ### Example with Rich Content

    ```tsx theme={null}
    const options: MultiSelectMenuOption[] = [
      {
        id: 1,
        label: "John Doe",
        searchText: "john.doe@example.com",
        content: {
          title: "John Doe",
          description: "Engineering",
        },
      },
      {
        id: 2,
        label: "Jane Smith",
        searchText: "jane.smith@example.com",
        content: {
          title: "Jane Smith",
          description: "Design",
        },
      },
    ];
    ```

    ### Example with Grouped Options

    When using `lazy="group"`, options must include the `group` property:

    ```tsx theme={null}
    const groupedOptions: MultiSelectMenuOption[] = [
      {
        id: 1,
        label: "John Doe",
        group: "Engineering",
      },
      {
        id: 2,
        label: "Jane Smith",
        group: "Design",
      },
      {
        id: 3,
        label: "Bob Johnson",
        group: "Engineering",
      },
    ];
    ```

    ### Example with Disabled Options

    ```tsx theme={null}
    const options: MultiSelectMenuOption[] = [
      {
        id: 1,
        label: "Available Option",
      },
      {
        id: 2,
        label: "Unavailable Option",
        disabled: true,
      },
      {
        id: 3,
        label: "Another Available Option",
      },
    ];
    ```
  </Tab>
</Tabs>
