{
  "name": "command-menu",
  "layer": "application",
  "type": "component",
  "title": "Command menu components",
  "description": "Powerful and customizable React command menu components built for modern applications and websites. These command menus are built using React Aria and styled with Tailwind CSS.",
  "files": [
    {
      "path": "components/application/command-menu/command-menu.tsx",
      "target": "components/application/command-menu/command-menu.tsx",
      "type": "component",
      "content": "\"use client\";\n\nimport type { ComponentPropsWithRef, ComponentType, FC, ReactNode } from \"react\";\nimport { createContext, useContext } from \"react\";\nimport { useFilter } from \"react-aria\";\nimport type {\n    AutocompleteProps as AriaAutocompleteProps,\n    MenuItemProps as AriaMenuItemProps,\n    MenuProps as AriaMenuProps,\n    MenuSectionProps as AriaMenuSectionProps,\n    PopoverProps as AriaPopoverProps,\n    SearchFieldProps as AriaSearchFieldProps,\n} from \"react-aria-components\";\nimport {\n    Autocomplete as AriaAutocomplete,\n    Dialog as AriaDialog,\n    DialogTrigger as AriaDialogTrigger,\n    Header as AriaHeader,\n    Input as AriaInput,\n    Menu as AriaMenu,\n    MenuItem as AriaMenuItem,\n    MenuSection as AriaMenuSection,\n    Popover as AriaPopover,\n    SearchField as AriaSearchField,\n} from \"react-aria-components\";\nimport { SearchLg } from \"@properui/icons\";\nimport { cx, sortCx } from \"@/utils/cx\";\nimport { Avatar } from \"../../base/avatar/avatar\";\nimport { FeaturedIcon } from \"../../foundations/featured-icon/featured-icon\";\n\n/**\n * How a command menu item arranges its label and supporting text.\n *\n * - `inline` — label and supporting text sit on one line, supporting text pushed to the end.\n * - `stacked` — supporting text sits on a second line underneath the label.\n */\nexport type CommandMenuLayout = \"inline\" | \"stacked\";\n\ntype IconComponentType = ComponentType<{ className?: string; \"aria-hidden\"?: boolean | \"true\" | \"false\" }>;\n\nconst styles = sortCx({\n    root: [\n        // Card surface — same elevation vocabulary as the dropdown popover, one step larger.\n        \"bg-primary ring-secondary_alt flex w-full max-w-160 flex-col overflow-hidden rounded-xl shadow-xl ring-1\",\n    ].join(\" \"),\n    layouts: {\n        inline: {\n            item: \"gap-2 py-2\",\n            text: \"flex-row items-center gap-2\",\n            icon: \"size-5\",\n        },\n        stacked: {\n            item: \"gap-3 py-2.5\",\n            text: \"flex-col gap-0.5\",\n            icon: \"mt-0.5 size-5 self-start\",\n        },\n    },\n});\n\n/** Avatar scale per layout — inline rows keep a single-line height, stacked rows fit two lines. */\nconst avatarSizes: Record<CommandMenuLayout, \"xs\" | \"md\"> = { inline: \"xs\", stacked: \"md\" };\n\nconst CommandMenuContext = createContext<{ layout: CommandMenuLayout }>({ layout: \"inline\" });\n\nexport interface CommandMenuProps extends Omit<AriaAutocompleteProps, \"children\" | \"filter\"> {\n    /**\n     * How every item in the menu lays out its label and supporting text.\n     * @default \"inline\"\n     */\n    layout?: CommandMenuLayout;\n    /** Predicate deciding whether an item matches the search value. Defaults to a case/accent-insensitive \"contains\". */\n    filter?: (textValue: string, inputValue: string) => boolean;\n    /** Class name merged onto the card. */\n    className?: string;\n    /** The search field, list and footer of the menu. */\n    children: ReactNode;\n}\n\nconst CommandMenuRoot = ({ layout = \"inline\", filter, className, children, ...props }: CommandMenuProps) => {\n    const { contains } = useFilter({ sensitivity: \"base\" });\n\n    return (\n        <div className={cx(styles.root, className)}>\n            <CommandMenuContext.Provider value={{ layout }}>\n                <AriaAutocomplete filter={filter ?? contains} {...props}>\n                    {children}\n                </AriaAutocomplete>\n            </CommandMenuContext.Provider>\n        </div>\n    );\n};\n\nexport interface CommandMenuSearchProps extends Omit<AriaSearchFieldProps, \"children\" | \"className\"> {\n    /** Placeholder shown while the field is empty. */\n    placeholder?: string;\n    /** Icon rendered before the field. Pass `null` to remove it. */\n    icon?: IconComponentType | null;\n    /** Class name merged onto the field wrapper. */\n    className?: string;\n}\n\nconst CommandMenuSearch = ({ placeholder = \"Search\", icon: Icon = SearchLg, className, ...props }: CommandMenuSearchProps) => (\n    <AriaSearchField aria-label=\"Search\" {...props} className={cx(\"border-secondary relative flex w-full items-center border-b\", className)}>\n        {Icon && <Icon aria-hidden=\"true\" className=\"text-fg-quaternary pointer-events-none absolute start-4 size-5\" />}\n\n        <AriaInput\n            placeholder={placeholder}\n            className={cx(\n                \"text-md text-primary placeholder:text-placeholder m-0 w-full bg-transparent py-3.5 outline-hidden [&::-webkit-search-cancel-button]:hidden\",\n                Icon ? \"ps-12 pe-4\" : \"px-4\",\n            )}\n        />\n    </AriaSearchField>\n);\n\nexport interface CommandMenuListProps<T extends object> extends AriaMenuProps<T> {\n    /** Class name merged onto the scroll container. */\n    className?: string;\n}\n\nconst CommandMenuList = <T extends object>({ className, ...props }: CommandMenuListProps<T>) => (\n    <AriaMenu\n        aria-label={props[\"aria-label\"] ?? (props[\"aria-labelledby\"] ? undefined : \"Suggestions\")}\n        {...props}\n        className={cx(\"flex max-h-100 flex-col overflow-y-auto p-2 outline-hidden select-none\", className)}\n    />\n);\n\nexport interface CommandMenuGroupProps<T extends object> extends Omit<AriaMenuSectionProps<T>, \"children\"> {\n    /** Heading rendered above the group. */\n    label?: ReactNode;\n    /** Class name merged onto the group. */\n    className?: string;\n    /** The items of the group. */\n    children?: ReactNode;\n}\n\nconst CommandMenuGroup = <T extends object>({ label, className, children, ...props }: CommandMenuGroupProps<T>) => (\n    <AriaMenuSection {...props} className={cx(\"flex flex-col gap-0.5 first:mt-0\", label ? \"mt-3\" : \"mt-1\", className)}>\n        {label && <AriaHeader className=\"text-quaternary px-3 pb-1 text-xs font-semibold\">{label}</AriaHeader>}\n        {children}\n    </AriaMenuSection>\n);\n\nexport interface CommandMenuItemProps extends Omit<AriaMenuItemProps, \"children\"> {\n    /** The primary label of the item. */\n    label: ReactNode;\n    /** Supporting text — at the end of the row when `inline`, below the label when `stacked`. */\n    description?: ReactNode;\n    /** Icon rendered before the label. */\n    icon?: IconComponentType;\n    /** Avatar image rendered before the label. */\n    avatarUrl?: string;\n    /** Alt text for `avatarUrl`. Empty by default, since the label already names the row. */\n    avatarAlt?: string;\n    /** Keyboard shortcut rendered at the end of the row. */\n    shortcut?: string;\n    /** Arbitrary content (badge, meta text, …) rendered at the end of the row. */\n    addon?: ReactNode;\n    /** Overrides the layout inherited from the root. */\n    layout?: CommandMenuLayout;\n}\n\nconst CommandMenuItem = ({ label, description, icon: Icon, avatarUrl, avatarAlt, shortcut, addon, layout, className, ...props }: CommandMenuItemProps) => {\n    const { layout: rootLayout } = useContext(CommandMenuContext);\n    const itemLayout = layout ?? rootLayout;\n    const layoutStyles = styles.layouts[itemLayout];\n\n    // Searching should match the supporting text too (an email, a description…), so both feed the text value.\n    const textValue = props.textValue ?? ([label, description].filter((part) => typeof part === \"string\").join(\" \") || undefined);\n\n    return (\n        <AriaMenuItem\n            {...props}\n            textValue={textValue}\n            className={(state) =>\n                cx(\n                    \"outline-focus-ring flex cursor-pointer items-center rounded-md px-3 outline-hidden transition duration-100 ease-linear\",\n                    layoutStyles.item,\n                    !state.isDisabled && \"hover:bg-primary_hover\",\n                    state.isFocused && \"bg-primary_hover\",\n                    state.isFocusVisible && \"outline-2 -outline-offset-2\",\n                    state.isDisabled && \"cursor-not-allowed opacity-50\",\n                    typeof className === \"function\" ? className(state) : className,\n                )\n            }\n        >\n            {avatarUrl && (\n                // Decorative by default — the label already announces who the row is about.\n                <Avatar size={avatarSizes[itemLayout]} src={avatarUrl} alt={avatarAlt ?? \"\"} className=\"shrink-0\" />\n            )}\n\n            {!avatarUrl && Icon && <Icon aria-hidden=\"true\" className={cx(\"text-fg-quaternary shrink-0\", layoutStyles.icon)} />}\n\n            <div className={cx(\"flex min-w-0 flex-1\", layoutStyles.text)}>\n                <span className=\"text-secondary truncate text-sm font-medium\">{label}</span>\n                {description && <span className={cx(\"text-tertiary truncate text-sm\", itemLayout === \"inline\" && \"ms-auto\")}>{description}</span>}\n            </div>\n\n            {addon && <div className=\"flex shrink-0 items-center gap-2\">{addon}</div>}\n\n            {shortcut && <CommandMenuShortcut>{shortcut}</CommandMenuShortcut>}\n        </AriaMenuItem>\n    );\n};\n\nexport type CommandMenuShortcutProps = ComponentPropsWithRef<\"kbd\">;\n\nconst CommandMenuShortcut = ({ className, ...props }: CommandMenuShortcutProps) => (\n    <kbd\n        {...props}\n        className={cx(\n            \"text-quaternary ring-secondary flex shrink-0 items-center rounded px-1 py-px font-sans text-xs font-medium ring-1 select-none ring-inset\",\n            className,\n        )}\n    />\n);\n\nexport interface CommandMenuEmptyProps extends Omit<ComponentPropsWithRef<\"div\">, \"title\"> {\n    /** Icon shown inside the featured icon. */\n    icon?: FC<{ className?: string }>;\n    /** Headline of the empty state. */\n    title?: ReactNode;\n    /** Supporting copy of the empty state. */\n    description?: ReactNode;\n}\n\nconst CommandMenuEmpty = ({ icon = SearchLg, title = \"No results found\", description, children, className, ...props }: CommandMenuEmptyProps) => (\n    <div {...props} className={cx(\"flex flex-col items-center justify-center gap-3 px-6 py-10 text-center\", className)}>\n        <FeaturedIcon icon={icon} size=\"lg\" color=\"gray\" theme=\"modern\" />\n\n        <div className=\"flex max-w-88 flex-col gap-1\">\n            <p className=\"text-primary text-sm font-semibold\">{title}</p>\n            {description && <p className=\"text-tertiary text-sm\">{description}</p>}\n        </div>\n\n        {children}\n    </div>\n);\n\nconst CommandMenuFooter = ({ className, ...props }: ComponentPropsWithRef<\"div\">) => (\n    <div {...props} className={cx(\"border-secondary bg-secondary flex flex-wrap items-center gap-x-4 gap-y-2 border-t px-4 py-3\", className)} />\n);\n\nexport interface CommandMenuPopoverProps extends Omit<AriaPopoverProps, \"children\"> {\n    /** Accessible name of the popover dialog. */\n    \"aria-label\"?: string;\n    /** The `CommandMenu` card to anchor to the trigger. */\n    children: ReactNode;\n}\n\nconst CommandMenuPopover = ({ \"aria-label\": ariaLabel = \"Command menu\", className, children, ...props }: CommandMenuPopoverProps) => (\n    <AriaPopover\n        placement=\"bottom start\"\n        offset={8}\n        {...props}\n        className={(state) =>\n            cx(\n                \"origin-(--trigger-anchor-point) will-change-transform\",\n                state.isEntering &&\n                    \"animate-in fade-in placement-right:slide-in-from-left-0.5 placement-top:slide-in-from-bottom-0.5 placement-bottom:slide-in-from-top-0.5 duration-150 ease-out\",\n                state.isExiting &&\n                    \"animate-out fade-out placement-right:slide-out-to-left-0.5 placement-top:slide-out-to-bottom-0.5 placement-bottom:slide-out-to-top-0.5 duration-100 ease-in\",\n                typeof className === \"function\" ? className(state) : className,\n            )\n        }\n    >\n        <AriaDialog aria-label={ariaLabel} className=\"flex outline-hidden\">\n            {children}\n        </AriaDialog>\n    </AriaPopover>\n);\n\nconst CommandMenu = CommandMenuRoot as typeof CommandMenuRoot & {\n    /** Wraps a trigger element and a `CommandMenu.Popover` so the menu opens as a popover. */\n    Trigger: typeof AriaDialogTrigger;\n    /** Positions a `CommandMenu` under its trigger. */\n    Popover: typeof CommandMenuPopover;\n    /** The search field that filters the list. */\n    Search: typeof CommandMenuSearch;\n    /** The scrollable list of results. */\n    List: typeof CommandMenuList;\n    /** A labelled group of items inside the list. */\n    Group: typeof CommandMenuGroup;\n    /** A single result row. */\n    Item: typeof CommandMenuItem;\n    /** The keyboard-shortcut chip used by items and the footer. */\n    Shortcut: typeof CommandMenuShortcut;\n    /** The \"no results\" block — pass it to `List` via `renderEmptyState`. */\n    Empty: typeof CommandMenuEmpty;\n    /** The hint bar pinned under the list. */\n    Footer: typeof CommandMenuFooter;\n};\n\nCommandMenu.Trigger = AriaDialogTrigger;\nCommandMenu.Popover = CommandMenuPopover;\nCommandMenu.Search = CommandMenuSearch;\nCommandMenu.List = CommandMenuList;\nCommandMenu.Group = CommandMenuGroup;\nCommandMenu.Item = CommandMenuItem;\nCommandMenu.Shortcut = CommandMenuShortcut;\nCommandMenu.Empty = CommandMenuEmpty;\nCommandMenu.Footer = CommandMenuFooter;\n\nexport { CommandMenu };\n",
      "dependencies": [
        "@properui/icons",
        "react",
        "react-aria",
        "react-aria-components"
      ]
    }
  ],
  "registryDependencies": [
    "avatar",
    "cx",
    "featured-icon"
  ],
  "optionalRegistryDependencies": [],
  "dependencies": [
    "@properui/icons",
    "react",
    "react-aria",
    "react-aria-components"
  ],
  "cssVars": [],
  "examples": [
    "actions",
    "actions-stacked",
    "command-menu-example",
    "empty-state",
    "integrations-menu",
    "integrations-menu-stacked",
    "users",
    "users-menu",
    "users-menu-stacked",
    "users-stacked"
  ],
  "docs": "/components/command-menus",
  "intent": "Give power users a keyboard-first, searchable palette of actions/destinations (⌘K-style).",
  "avoid_when": [
    "the action set is small enough for a regular dropdown or visible toolbar"
  ],
  "composes_with": [
    "dropdown",
    "avatar",
    "badges"
  ],
  "a11y_contract": [
    "the dialog and search field ship default aria-labels (\"Command menu\", \"Search\"); the results listbox defaults to aria-label \"Suggestions\" unless the caller supplies aria-label/aria-labelledby"
  ],
  "requires_data": [
    "a searchable list of items with label and action; typically opened via a global keyboard shortcut the host app wires up"
  ],
  "token_contract": [
    "bg-primary",
    "bg-primary_hover",
    "bg-secondary",
    "border-secondary",
    "text-fg-quaternary",
    "text-placeholder",
    "text-primary",
    "text-quaternary",
    "text-secondary",
    "text-tertiary"
  ],
  "changelog": [],
  "platforms": [
    "react",
    "next"
  ]
}
