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

# Menu – Code

> The Menu provides configured action, navigation, and selection options in a positioned popover.

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>;
  }
};

<Tabs>
  <Tab title="Implementation">
    <LiveCode showCode example="ai-kit-menu" fullWidth screenshot>
      ```tsx lines expandable theme={null}
      import { Button, Menu } from "@servicetitan/anvil2-ai-kit";

      function App() {
        return (
          <div style={{ paddingTop: 16, display: "flex", justifyContent: "center" }}>
            <Menu
              trigger={<Button label="Options" variant="secondary" />}
              items={[
                { id: "rename", label: "Rename" },
                { id: "duplicate", label: "Duplicate" },
                { id: "archive", label: "Move to archive" },
                { id: "delete", label: "Delete" },
              ]}
              defaultOpen
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    ## Common Examples

    To use the `Menu`, pass a Carto `Button` through `trigger` and configured `MenuOption` objects through `items`. The kit forbids composed menu-item children.

    ```tsx theme={null}
    import { Button, Menu } from "@servicetitan/anvil2-ai-kit";

    function ExampleComponent() {
      return (
        <Menu
          trigger={<Button label="Options" variant="secondary" />}
          items={[
            { id: "rename", label: "Rename", onAction: rename },
            { id: "delete", label: "Delete", onAction: remove },
          ]}
        />
      );
    }
    ```

    ### Selection

    The default `selectionMode="none"` creates an action menu that closes after activation. Use `"single"` or `"multiple"` for persistent checked selections.

    <LiveCode showCode example="ai-kit-menu-selection-mode-single" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { Button, Menu } from "@servicetitan/anvil2-ai-kit";

      function App() {
        return (
          <div style={{ paddingTop: 16, display: "flex", justifyContent: "center" }}>
            <Menu
              trigger={<Button label="Sort by" variant="secondary" />}
              items={[
                { id: "name", label: "Name" },
                { id: "modified", label: "Last modified" },
                { id: "created", label: "Date created" },
                { id: "size", label: "Size" },
              ]}
              selectionMode="single"
              defaultSelectedKeys={["modified"]}
              defaultOpen
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    <LiveCode showCode example="ai-kit-menu-selection-mode-multi" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { Button, Menu } from "@servicetitan/anvil2-ai-kit";

      function App() {
        return (
          <div style={{ paddingTop: 16, display: "flex", justifyContent: "center" }}>
            <Menu
              trigger={<Button label="Columns" variant="secondary" />}
              items={[
                { id: "name", label: "Name" },
                { id: "modified", label: "Last modified" },
                { id: "created", label: "Date created" },
                { id: "size", label: "Size" },
              ]}
              selectionMode="multiple"
              defaultSelectedKeys={["name", "size"]}
              defaultOpen
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Icons and disabled options

    Pass an icon from `@servicetitan/anvil2-icons` through an option's `icon` field. Set `isDisabled` on an option to prevent focus, selection, and activation.

    <LiveCode showCode example="ai-kit-menu-icons" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { Button, Menu } from "@servicetitan/anvil2-ai-kit";
      import { IconCopy, IconInbox, IconPencil } from "@servicetitan/anvil2-icons";

      function App() {
        return (
          <div style={{ paddingTop: 16, display: "flex", justifyContent: "center" }}>
            <Menu
              trigger={<Button label="Options" variant="secondary" />}
              items={[
                { id: "rename", label: "Rename", icon: <IconPencil /> },
                { id: "duplicate", label: "Duplicate", icon: <IconCopy /> },
                { id: "archive", label: "Move to archive", icon: <IconInbox /> },
              ]}
              defaultOpen
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    <LiveCode showCode example="ai-kit-menu-disabled-options" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { Button, Menu } from "@servicetitan/anvil2-ai-kit";

      function App() {
        return (
          <div style={{ paddingTop: 16, display: "flex", justifyContent: "center" }}>
            <Menu
              trigger={<Button label="Options" variant="secondary" />}
              selectionMode="multiple"
              defaultSelectedKeys={["archive"]}
              items={[
                { id: "rename", label: "Rename" },
                { id: "duplicate", label: "Duplicate", isDisabled: true },
                { id: "archive", label: "Move to archive", isDisabled: true },
                { id: "delete", label: "Delete" },
              ]}
              defaultOpen
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    ### Link options

    Pass `href` to render an option as a navigation link. An option with `target="_blank"` receives a safe `rel` default and a localized new-tab announcement.

    ### Placement and long lists

    `placement` opens the popover above or below its trigger with a fixed 4px gap. Long labels wrap at the 360px width ceiling, and long lists scroll within available viewport height.

    <LiveCode showCode example="ai-kit-menu-placement" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { Button, Menu } from "@servicetitan/anvil2-ai-kit";

      function App() {
        return (
          <div
            style={{
              display: "flex",
              justifyContent: "center",
              padding: "160px 0 16px",
            }}
          >
            <Menu
              trigger={<Button label="Options" variant="secondary" />}
              items={[
                { id: "rename", label: "Rename" },
                { id: "duplicate", label: "Duplicate" },
                { id: "delete", label: "Delete" },
              ]}
              placement="top"
              defaultOpen
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    <LiveCode showCode example="ai-kit-menu-scrolling-list" screenshot fullWidth>
      ```tsx lines expandable theme={null}
      import { Button, Menu } from "@servicetitan/anvil2-ai-kit";

      function App() {
        return (
          <div style={{ paddingTop: 16, display: "flex", justifyContent: "center" }}>
            <Menu
              trigger={<Button label="Options" variant="secondary" />}
              items={Array.from({ length: 20 }, (_, index) => ({
                id: `option-${index}`,
                label: `Option ${index + 1}`,
              }))}
              defaultOpen
            />
          </div>
        );
      }

      export default App;
      ```
    </LiveCode>

    ## Internationalization

    Menu option labels are consumer content and must be localized before they are passed in `items`. External link options use `ai-kit.menu.opensInNewTab` for the hidden new-tab announcement.

    ```tsx theme={null}
    <AiKitIntlProvider
      messages={{
        "ai-kit.menu.opensInNewTab": "(opens in a separate tab)",
      }}
    >
      <Menu
        trigger={<Button label="Help" />}
        items={[
          {
            id: "docs",
            label: "Documentation",
            href: "https://example.com",
            target: "_blank",
          },
        ]}
      />
    </AiKitIntlProvider>
    ```

    ## React Accessibility

    * React Aria supplies keyboard navigation, typeahead, focus management, and selection semantics.
    * Arrow keys move between options. Enter or Space activates the focused option. Escape closes the menu and restores focus to the trigger.
    * External link options include a localized hidden new-tab announcement.
  </Tab>

  <Tab title="Menu Props">
    ```tsx theme={null}
    <Menu
      trigger={<Button label="Options" />}
      items={[{ id: "rename", label: "Rename" }]}
      placement="bottom"
      selectionMode="none"
    />
    ```

    ## `Menu` Props

    The `Menu` accepts the following props and forwards remaining React Aria `MenuTrigger` and `Menu` props except their composed children and render slots.

    <ParamField path="items" type="MenuOption[]" required>
      Options to render from top to bottom.
    </ParamField>

    <ParamField path="trigger" type="ReactElement<ButtonProps>" required>
      Carto `Button` that opens the menu.
    </ParamField>

    <ParamField path="className" type="string">
      Class merged onto the popover surface.
    </ParamField>

    <ParamField path="defaultOpen" type="boolean">
      Whether the menu starts open when uncontrolled.
    </ParamField>

    <ParamField path="isOpen" type="boolean">
      Controlled open state. Pair with `onOpenChange`.
    </ParamField>

    <ParamField path="onOpenChange" type="(isOpen: boolean) => void">
      Called when the menu's open state changes.
    </ParamField>

    <ParamField path="onSelectionChange" type="(keys: Selection) => void">
      Called when selected option keys change.
    </ParamField>

    <ParamField path="placement" type={`"top" | "bottom"`} default="bottom">
      Side where the popover opens relative to its trigger.
    </ParamField>

    <ParamField path="selectedKeys" type="Selection">
      Controlled selected option keys.
    </ParamField>

    <ParamField path="selectionMode" type={`"none" | "single" | "multiple"`} default="none">
      `"none"` creates an action menu. `"single"` and `"multiple"` persist
      checked selection.
    </ParamField>
  </Tab>

  <Tab title="MenuOption">
    ```tsx theme={null}
    const option: MenuOption = {
      id: "rename",
      label: "Rename",
      icon: <IconPencil />,
      onAction: rename,
    };
    ```

    ## `MenuOption`

    `MenuOption` extends React Aria `MenuItem` props with configured content.

    <ParamField path="id" type="Key" required>
      Selection key reported through the menu's selection APIs.
    </ParamField>

    <ParamField path="label" type="string" required>
      Visible label and accessible name.
    </ParamField>

    <ParamField path="href" type="Href">
      Destination that renders the option as a navigation link.
    </ParamField>

    <ParamField path="icon" type="IconElement">
      Leading icon rendered before the label.
    </ParamField>

    <ParamField path="isDisabled" type="boolean">
      Prevents focus, selection, and activation.
    </ParamField>

    <ParamField path="onAction" type="() => void">
      Handles activation for an action option.
    </ParamField>

    <ParamField path="target" type="string">
      Link target. `"_blank"` adds the localized new-tab announcement.
    </ParamField>
  </Tab>
</Tabs>
