Glrk UI

Context Menu

A simplified wrapper for Shadcn Context Menu with flexible option handling and automatic type conversion

Installation

npx shadcn@latest add @glrk-ui/context-menu

If you haven't set up the prerequisites yet, check out Prerequest section.

Update following code in ui/context-menu.tsx

function ContextMenuCheckboxItem({
  className,
  children,
  checked,
  indicatorAt = "right",
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem> & { indicatorAt?: indicatorAtT }) {
  return (
    <ContextMenuPrimitive.CheckboxItem
      data-slot="context-menu-checkbox-item"
      className={cn(
        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
        indicatorAt === "right" ? "pr-8 pl-2" : "pr-2 pl-8",
      )}
      checked={checked}
      {...props}
    >
      <span className={cn("pointer-events-none absolute flex size-3.5 items-center justify-center", indicatorAt === "right" ? "right-2" : "left-2")}>
        <ContextMenuPrimitive.ItemIndicator>
          <CheckIcon className="size-4" />
        </ContextMenuPrimitive.ItemIndicator>
      </span>
      {children}
    </ContextMenuPrimitive.CheckboxItem>
  )
}

function ContextMenuRadioItem({
  className,
  children,
  indicatorAt = "right",
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem> & { indicatorAt?: indicatorAtT }) {
  return (
    <ContextMenuPrimitive.RadioItem
      data-slot="context-menu-radio-item"
      className={cn(
        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
        indicatorAt === "right" ? "pr-8 pl-2" : "pr-2 pl-8",
      )}
      {...props}
    >
      <span className={cn("pointer-events-none absolute flex size-3.5 items-center justify-center", indicatorAt === "right" ? "right-2" : "left-2")}>
        <ContextMenuPrimitive.ItemIndicator>
          <CircleIcon className="size-2 fill-current" />
        </ContextMenuPrimitive.ItemIndicator>
      </span>
      {children}
    </ContextMenuPrimitive.RadioItem>
  )
}

Create new file name types/menu.d.ts.

types/menu.d.ts
type menuOptionT = allowedPrimitiveT | optionT | (optionT & {
  variant?: "default" | "destructive"
  shortcut?: string
  disabled?: boolean
})

type menuGroupT = {
  group: string
  options: menuOptionT[]
  className?: string
  groupLabelCls?: string
}

type subMenuT = {
  submenu: string
  options: (menuOptionT | menuGroupT)[]
  triggerCls?: string
  contentCls?: string
}

type menuOptionsT = (menuOptionT | menuGroupT | subMenuT)[]

type menuInputOptionT = allowedPrimitiveT | optionT | (optionT & {
  disabled?: boolean
})

type menuInputGroupT = {
  group: string
  options: menuInputOptionT[]
  className?: string
  groupLabelCls?: string
}

type inputSubMenuT = {
  submenu: string
  options: (menuInputOptionT | menuInputGroupT)[]
  triggerCls?: string
  contentCls?: string
}

type menuInputOptionsT = (menuInputOptionT | menuInputGroupT | inputSubMenuT)[]

Add following lines to lib/utils.ts

export const isGroupMenu = optionTypeChecker<menuGroupT>("group")
export const isSubMenu = optionTypeChecker<subMenuT>("submenu")
export const isInputGroupMenu = optionTypeChecker<menuInputGroupT>("group")
export const isInputSubMenu = optionTypeChecker<inputSubMenuT>("submenu")

Create new file name ui/context-menu-wrapper.tsx.

ui/context-menu-wrapper.tsx
"use client"

import { useState } from "react"
import {
  cn, getKey, getLabel, getValue,
  isSeparator,
  isSubMenu,
  isGroupMenu,
  isInputSubMenu,
  isInputGroupMenu,
  parseAllowedPrimitive,
} from "@/lib/utils"

import {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuGroup,
  ContextMenuLabel,
  ContextMenuItem,
  ContextMenuCheckboxItem,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuSub,
  ContextMenuSubTrigger,
  ContextMenuSubContent,
} from "@/components/ui/context-menu"

type commonCheckboxProps = {
  checked?: allowedPrimitiveT[]
  indicatorAt?: indicatorAtT
  onCheckedChange?: (value: allowedPrimitiveT, checked: boolean) => void
}

type commonRadioProps = {
  value?: allowedPrimitiveT
  indicatorAt?: indicatorAtT
  onValueChange?: (value: allowedPrimitiveT) => void
}

type commonSubMenuT = {
  itemCls?: string
  groupCls?: string
  groupLabelCls?: string
}

type commonPropsT = {
  children: React.ReactNode
  itemCls?: string
  groupCls?: string
  groupLabelCls?: string
  contentProps?: React.ComponentProps<typeof ContextMenuContent>
  onSelect?: (value: allowedPrimitiveT) => void
} & React.ComponentProps<typeof ContextMenu>

// -------

type itemProps = {
  option: menuOptionT
  className?: string
  onSelect?: () => void
}
function Item({
  option,
  className,
  onSelect
}: itemProps) {
  const value = getValue(option)

  if (isSeparator(value)) return <ContextMenuSeparator className={className} />

  const label = getLabel(option)
  const opt: any = typeof option === "object" ? option : {}
  const shortcut = opt?.shortcut

  return (
    <ContextMenuItem
      {...opt}
      onSelect={onSelect}
      className={cn(className, opt?.className)}
    >
      {label}
      {shortcut && <ContextMenuShortcut>{shortcut}</ContextMenuShortcut>}
    </ContextMenuItem>
  )
}

type checkboxItemProps = {
  option: menuInputOptionT
  className?: string
  checked?: boolean
  onCheckedChange?: (checked: boolean) => void
  indicatorAt?: indicatorAtT
}
function CheckboxItem({
  option,
  className,
  checked = false,
  indicatorAt,
  onCheckedChange = () => { }
}: checkboxItemProps) {
  const value = getValue(option)

  if (isSeparator(value)) return <ContextMenuSeparator className={className} />

  const label = getLabel(option)
  const disabled = (option as any)?.disabled

  return (
    <ContextMenuCheckboxItem
      checked={checked}
      disabled={disabled}
      className={className}
      indicatorAt={indicatorAt}
      onCheckedChange={onCheckedChange}
    >
      {label}
    </ContextMenuCheckboxItem>
  )
}

type radioItemProps = {
  option: menuInputOptionT
  className?: string
  indicatorAt?: indicatorAtT
}
function RadioItem({
  option,
  className,
  indicatorAt
}: radioItemProps) {
  const value = getValue(option)

  if (isSeparator(value)) return <ContextMenuSeparator className={className} />

  const label = getLabel(option)
  const disabled = (option as any)?.disabled

  return (
    <ContextMenuRadioItem
      value={`${value}`}
      disabled={disabled}
      className={className}
      indicatorAt={indicatorAt}
    >
      {label}
    </ContextMenuRadioItem>
  )
}

type SubMenuProps = commonSubMenuT & {
  submenu: subMenuT
  onSelect?: (value: allowedPrimitiveT) => void
}
function SubMenu({
  submenu,
  itemCls,
  groupCls,
  groupLabelCls,
  onSelect
}: SubMenuProps) {
  return (
    <ContextMenuSub>
      <ContextMenuSubTrigger className={submenu.triggerCls}>
        {submenu.submenu}
      </ContextMenuSubTrigger>

      <ContextMenuSubContent className={submenu.contentCls}>
        {submenu.options.map((option, i) => {
          if (isGroupMenu(option)) {
            return (
              <ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
                <ContextMenuLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
                  {option.group}
                </ContextMenuLabel>

                {option.options.map((grOpt, j) => (
                  <Item
                    key={getKey(grOpt, j)}
                    option={grOpt}
                    className={itemCls}
                    onSelect={() => onSelect?.(getValue(grOpt))}
                  />
                ))}
              </ContextMenuGroup>
            )
          }

          if (isSubMenu(option)) {
            return (
              <SubMenu
                key={getKey(option, i)}
                submenu={option}
                itemCls={itemCls}
                groupCls={groupCls}
                onSelect={onSelect}
              />
            )
          }

          return (
            <Item
              key={getKey(option, i)}
              option={option}
              className={itemCls}
              onSelect={() => onSelect?.(getValue(option))}
            />
          )
        })}
      </ContextMenuSubContent>
    </ContextMenuSub>
  )
}

type CheckboxSubMenuProps = commonSubMenuT & commonCheckboxProps & {
  submenu: inputSubMenuT
}
function CheckboxSubMenu({
  submenu,
  itemCls,
  groupCls,
  groupLabelCls,
  checked = [],
  indicatorAt,
  onCheckedChange = () => { }
}: CheckboxSubMenuProps) {
  return (
    <ContextMenuSub>
      <ContextMenuSubTrigger className={submenu.triggerCls}>
        {submenu.submenu}
      </ContextMenuSubTrigger>

      <ContextMenuSubContent className={submenu.contentCls}>
        {submenu.options.map((option, i) => {
          if (isInputGroupMenu(option)) {
            return (
              <ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
                <ContextMenuLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
                  {option.group}
                </ContextMenuLabel>

                {option.options.map((grOpt, j) => {
                  const v = getValue(grOpt)
                  return (
                    <CheckboxItem
                      key={getKey(grOpt, j)}
                      option={grOpt}
                      checked={checked.includes(v)}
                      className={itemCls}
                      indicatorAt={indicatorAt}
                      onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
                    />
                  )
                })}
              </ContextMenuGroup>
            )
          }

          if (isInputSubMenu(option)) {
            return (
              <CheckboxSubMenu
                key={option.submenu}
                submenu={option}
                checked={checked}
                itemCls={itemCls}
                groupCls={groupCls}
                indicatorAt={indicatorAt}
                groupLabelCls={groupLabelCls}
                onCheckedChange={onCheckedChange}
              />
            )
          }

          const v = getValue(option)
          return (
            <CheckboxItem
              key={getKey(option, i)}
              option={option}
              checked={checked.includes(v)}
              className={itemCls}
              indicatorAt={indicatorAt}
              onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
            />
          )
        })}
      </ContextMenuSubContent>
    </ContextMenuSub>
  )
}

type RadioSubMenuProps = commonSubMenuT & commonRadioProps & {
  submenu: inputSubMenuT
}
function RadioSubMenu({
  submenu,
  itemCls,
  groupCls,
  groupLabelCls,
  value = "",
  indicatorAt,
  onValueChange = () => { }
}: RadioSubMenuProps) {
  return (
    <ContextMenuSub>
      <ContextMenuSubTrigger className={submenu.triggerCls}>
        {submenu.submenu}
      </ContextMenuSubTrigger>

      <ContextMenuSubContent className={submenu.contentCls}>
        <ContextMenuRadioGroup value={`${value}`} onValueChange={onValueChange}>
          {submenu.options.map((option, i) => {
            if (isInputGroupMenu(option)) {
              return (
                <ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
                  <ContextMenuLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
                    {option.group}
                  </ContextMenuLabel>

                  {option.options.map((grOpt, j) => (
                    <RadioItem
                      key={getKey(grOpt, j)}
                      option={grOpt}
                      className={itemCls}
                      indicatorAt={indicatorAt}
                    />
                  ))}
                </ContextMenuGroup>
              )
            }

            if (isInputSubMenu(option)) {
              return (
                <RadioSubMenu
                  key={option.submenu}
                  value={value}
                  submenu={option}
                  itemCls={itemCls}
                  groupCls={groupCls}
                  indicatorAt={indicatorAt}
                  groupLabelCls={groupLabelCls}
                  onValueChange={onValueChange}
                />
              )
            }

            return (
              <RadioItem
                key={getKey(option, i)}
                option={option}
                className={itemCls}
                indicatorAt={indicatorAt}
              />
            )
          })}
        </ContextMenuRadioGroup>
      </ContextMenuSubContent>
    </ContextMenuSub>
  )
}

type ContextWrapperProps = commonPropsT & {
  options: menuOptionsT
}
function ContextWrapper({
  children,
  options,
  itemCls,
  groupCls,
  groupLabelCls,
  contentProps,
  onSelect,
  ...props
}: ContextWrapperProps) {
  return (
    <ContextMenu {...props}>
      <ContextMenuTrigger asChild>
        {children}
      </ContextMenuTrigger>

      <ContextMenuContent {...contentProps}>
        {options.map((option, i) => {
          if (isGroupMenu(option)) {
            return (
              <ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
                <ContextMenuLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
                  {option.group}
                </ContextMenuLabel>

                {option.options.map((grOpt, j) => (
                  <Item
                    key={getKey(grOpt, j)}
                    option={grOpt}
                    className={itemCls}
                    onSelect={() => onSelect?.(getValue(grOpt))}
                  />
                ))}
              </ContextMenuGroup>
            )
          }

          if (isSubMenu(option)) {
            return (
              <SubMenu
                key={option.submenu}
                submenu={option}
                itemCls={itemCls}
                groupCls={groupCls}
                groupLabelCls={groupLabelCls}
                onSelect={onSelect}
              />
            )
          }

          return (
            <Item
              key={getKey(option, i)}
              option={option}
              className={itemCls}
              onSelect={() => onSelect?.(getValue(option))}
            />
          )
        })}
      </ContextMenuContent>
    </ContextMenu>
  )
}

type ContextCheckboxWrapperProps = commonPropsT & commonCheckboxProps & {
  options: menuInputOptionsT
  label?: string
}
function ContextCheckboxWrapper({
  children,
  options,

  label,
  contentProps,
  itemCls,
  groupCls,
  groupLabelCls,

  checked: o_checked,
  onCheckedChange: o_onCheckedChange,

  indicatorAt,
  ...props
}: ContextCheckboxWrapperProps) {
  const [i_checked, setIChecked] = useState<allowedPrimitiveT[]>([])

  function i_Checked(v: allowedPrimitiveT, c: boolean) {
    setIChecked(prev => !c ? prev.filter(p => p !== v) : [...prev, v])
  }

  const checked = o_checked ?? i_checked
  const onCheckedChange = o_onCheckedChange ?? i_Checked

  return (
    <ContextMenu {...props}>
      <ContextMenuTrigger asChild>
        {children}
      </ContextMenuTrigger>

      <ContextMenuContent {...contentProps}>
        {label && (
          <>
            <ContextMenuLabel>{label}</ContextMenuLabel>
            <ContextMenuSeparator />
          </>
        )}
        {options.map((option, i) => {
          if (isInputGroupMenu(option)) {
            return (
              <ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
                <ContextMenuLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
                  {option.group}
                </ContextMenuLabel>

                {option.options.map((grOpt, j) => {
                  const v = getValue(grOpt)
                  return (
                    <CheckboxItem
                      key={getKey(grOpt, j)}
                      option={grOpt}
                      checked={checked.includes(v)}
                      className={itemCls}
                      indicatorAt={indicatorAt}
                      onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
                    />
                  )
                })}
              </ContextMenuGroup>
            )
          }

          if (isInputSubMenu(option)) {
            return (
              <CheckboxSubMenu
                key={option.submenu}
                submenu={option}
                checked={checked}
                itemCls={itemCls}
                groupCls={groupCls}
                indicatorAt={indicatorAt}
                groupLabelCls={groupLabelCls}
                onCheckedChange={onCheckedChange}
              />
            )
          }

          const v = getValue(option)
          return (
            <CheckboxItem
              key={getKey(option, i)}
              option={option}
              checked={checked.includes(v)}
              className={itemCls}
              indicatorAt={indicatorAt}
              onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
            />
          )
        })}
      </ContextMenuContent>
    </ContextMenu>
  )
}

type ContextRadioWrapperProps = commonPropsT & commonRadioProps & {
  options: menuInputOptionsT
  label?: string
}
function ContextRadioWrapper({
  children,
  options,

  label,
  itemCls,
  groupCls,
  groupLabelCls,
  contentProps,

  value: o_value,
  onValueChange: o_onValueChange,

  indicatorAt,
  ...props
}: ContextRadioWrapperProps) {
  const [i_value, setIValue] = useState<allowedPrimitiveT>("")

  const value = o_value ?? i_value
  const onValueChange = o_onValueChange ?? setIValue

  return (
    <ContextMenu {...props}>
      <ContextMenuTrigger asChild>
        {children}
      </ContextMenuTrigger>

      <ContextMenuContent {...contentProps}>
        {label && (
          <>
            <ContextMenuLabel>{label}</ContextMenuLabel>
            <ContextMenuSeparator />
          </>
        )}
        <ContextMenuRadioGroup value={`${value}`} onValueChange={v => onValueChange(parseAllowedPrimitive(v))}>
          {options.map((option, i) => {
            if (isInputGroupMenu(option)) {
              return (
                <ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
                  <ContextMenuLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
                    {option.group}
                  </ContextMenuLabel>

                  {option.options.map((grOpt, j) => (
                    <RadioItem
                      key={getKey(grOpt, j)}
                      option={grOpt}
                      className={itemCls}
                      indicatorAt={indicatorAt}
                    />
                  ))}
                </ContextMenuGroup>
              )
            }

            if (isInputSubMenu(option)) {
              return (
                <RadioSubMenu
                  key={option.submenu}
                  value={value}
                  submenu={option}
                  itemCls={itemCls}
                  groupCls={groupCls}
                  indicatorAt={indicatorAt}
                  groupLabelCls={groupLabelCls}
                  onValueChange={v => onValueChange(parseAllowedPrimitive(v))}
                />
              )
            }

            return (
              <RadioItem
                key={getKey(option, i)}
                option={option}
                className={itemCls}
                indicatorAt={indicatorAt}
              />
            )
          })}
        </ContextMenuRadioGroup>
      </ContextMenuContent>
    </ContextMenu>
  )
}

export {
  ContextWrapper,
  ContextCheckboxWrapper,
  ContextRadioWrapper,
}

Installation

npm install @radix-ui/react-context-menu

Copy and paste the following code into your project.

general.d.ts

types/general.d.ts
type readOnlyChildren = Readonly<{
  children: React.ReactNode;
}>

type allowedPrimitiveT = string | number | boolean

type optionT = {
  label: React.ReactNode
  value: allowedPrimitiveT
  className?: string
}

type groupT = {
  group: string
  options: (allowedPrimitiveT | optionT)[]
  className?: string
}

type optionsT = (allowedPrimitiveT | optionT | groupT)[]

type indicatorAtT = "right" | "left"
types/menu.d.ts
type menuOptionT = allowedPrimitiveT | optionT | (optionT & {
  variant?: "default" | "destructive"
  shortcut?: string
  disabled?: boolean
})

type menuGroupT = {
  group: string
  options: menuOptionT[]
  className?: string
  groupLabelCls?: string
}

type subMenuT = {
  submenu: string
  options: (menuOptionT | menuGroupT)[]
  triggerCls?: string
  contentCls?: string
}

type menuOptionsT = (menuOptionT | menuGroupT | subMenuT)[]

type menuInputOptionT = allowedPrimitiveT | optionT | (optionT & {
  disabled?: boolean
})

type menuInputGroupT = {
  group: string
  options: menuInputOptionT[]
  className?: string
  groupLabelCls?: string
}

type inputSubMenuT = {
  submenu: string
  options: (menuInputOptionT | menuInputGroupT)[]
  triggerCls?: string
  contentCls?: string
}

type menuInputOptionsT = (menuInputOptionT | menuInputGroupT | inputSubMenuT)[]

utils.ts

lib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

export function isAllowedPrimitive(value: unknown): value is allowedPrimitiveT {
  return ["string", "number", "boolean"].includes(typeof value)
}

export function parseAllowedPrimitive(value: allowedPrimitiveT): allowedPrimitiveT {
  if (typeof value !== "string") return value

  const trimmed = value.trim()

  if (trimmed === "true") return true
  if (trimmed === "false") return false
  if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed)

  return trimmed
}

export function optionTypeChecker<T>(key: keyof T) {
  return (option: any): option is T => !!option && typeof option === "object" && key in option
}

export const isSeparator = (item: any) => item === "---"
export const isOption = optionTypeChecker<optionT>("value")
export const isGroup = optionTypeChecker<groupT>("group")

export const getValue = (item: allowedPrimitiveT | optionT) => typeof item === "object" ? item.value : item
export const getLabel = (item: allowedPrimitiveT | optionT) => typeof item === "object" ? item.label : `${item}`

export function getKey(item: allowedPrimitiveT | optionT, i: number): string {
  const val = getValue(item)
  if (typeof val === "boolean") return `key-${val}`
  if (val === "---") return `---${i}`
  return `${val}`
}

export const isSubMenu = optionTypeChecker<subMenuT>("submenu")
export const isGroupMenu = optionTypeChecker<menuGroupT>("group")
export const isInputSubMenu = optionTypeChecker<inputSubMenuT>("submenu")
export const isInputGroupMenu = optionTypeChecker<menuInputGroupT>("group")

context-menu.tsx

ui/context-menu.tsx
"use client"

import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"

import { cn } from "@/lib/utils"

function ContextMenu(props: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
  return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
}

function ContextMenuTrigger(props: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
  return (
    <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
  )
}

function ContextMenuGroup(props: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
  return (
    <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
  )
}

function ContextMenuPortal(props: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
  return (
    <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
  )
}

function ContextMenuSub(props: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
  return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
}

function ContextMenuRadioGroup(props: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
  return (
    <ContextMenuPrimitive.RadioGroup
      data-slot="context-menu-radio-group"
      {...props}
    />
  )
}

function ContextMenuSubTrigger({
  className,
  inset,
  children,
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
  inset?: boolean
}) {
  return (
    <ContextMenuPrimitive.SubTrigger
      data-slot="context-menu-sub-trigger"
      data-inset={inset}
      className={cn(
        "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    >
      {children}
      <ChevronRightIcon className="ml-auto" />
    </ContextMenuPrimitive.SubTrigger>
  )
}

function ContextMenuSubContent({
  className,
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
  return (
    <ContextMenuPrimitive.SubContent
      data-slot="context-menu-sub-content"
      className={cn(
        "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
        className
      )}
      {...props}
    />
  )
}

function ContextMenuContent({
  className,
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
  return (
    <ContextMenuPrimitive.Portal>
      <ContextMenuPrimitive.Content
        data-slot="context-menu-content"
        className={cn(
          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
          className
        )}
        {...props}
      />
    </ContextMenuPrimitive.Portal>
  )
}

function ContextMenuItem({
  className,
  inset,
  variant = "default",
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
  inset?: boolean
  variant?: "default" | "destructive"
}) {
  return (
    <ContextMenuPrimitive.Item
      data-slot="context-menu-item"
      data-inset={inset}
      data-variant={variant}
      className={cn(
        "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    />
  )
}

function ContextMenuCheckboxItem({
  className,
  children,
  checked,
  indicatorAt = "right",
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem> & { indicatorAt?: indicatorAtT }) {
  return (
    <ContextMenuPrimitive.CheckboxItem
      data-slot="context-menu-checkbox-item"
      className={cn(
        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
        indicatorAt === "right" ? "pr-8 pl-2" : "pr-2 pl-8",
      )}
      checked={checked}
      {...props}
    >
      <span className={cn("pointer-events-none absolute flex size-3.5 items-center justify-center", indicatorAt === "right" ? "right-2" : "left-2")}>
        <ContextMenuPrimitive.ItemIndicator>
          <CheckIcon className="size-4" />
        </ContextMenuPrimitive.ItemIndicator>
      </span>
      {children}
    </ContextMenuPrimitive.CheckboxItem>
  )
}

function ContextMenuRadioItem({
  className,
  children,
  indicatorAt = "right",
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem> & { indicatorAt?: indicatorAtT }) {
  return (
    <ContextMenuPrimitive.RadioItem
      data-slot="context-menu-radio-item"
      className={cn(
        "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className,
        indicatorAt === "right" ? "pr-8 pl-2" : "pr-2 pl-8",
      )}
      {...props}
    >
      <span className={cn("pointer-events-none absolute flex size-3.5 items-center justify-center", indicatorAt === "right" ? "right-2" : "left-2")}>
        <ContextMenuPrimitive.ItemIndicator>
          <CircleIcon className="size-2 fill-current" />
        </ContextMenuPrimitive.ItemIndicator>
      </span>
      {children}
    </ContextMenuPrimitive.RadioItem>
  )
}

function ContextMenuLabel({
  className,
  inset,
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
  inset?: boolean
}) {
  return (
    <ContextMenuPrimitive.Label
      data-slot="context-menu-label"
      data-inset={inset}
      className={cn(
        "text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
        className
      )}
      {...props}
    />
  )
}

function ContextMenuSeparator({
  className,
  ...props
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
  return (
    <ContextMenuPrimitive.Separator
      data-slot="context-menu-separator"
      className={cn("bg-border -mx-1 my-1 h-px", className)}
      {...props}
    />
  )
}

function ContextMenuShortcut({
  className,
  ...props
}: React.ComponentProps<"span">) {
  return (
    <span
      data-slot="context-menu-shortcut"
      className={cn(
        "text-muted-foreground ml-auto text-xs tracking-widest",
        className
      )}
      {...props}
    />
  )
}

export {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuCheckboxItem,
  ContextMenuRadioItem,
  ContextMenuLabel,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuGroup,
  ContextMenuPortal,
  ContextMenuSub,
  ContextMenuSubContent,
  ContextMenuSubTrigger,
  ContextMenuRadioGroup,
}

context-menu-wrapper.tsx

ui/context-menu-wrapper.tsx
"use client"

import { useState } from "react"
import {
  cn, getKey, getLabel, getValue,
  isSeparator,
  isSubMenu,
  isGroupMenu,
  isInputSubMenu,
  isInputGroupMenu,
  parseAllowedPrimitive,
} from "@/lib/utils"

import {
  ContextMenu,
  ContextMenuTrigger,
  ContextMenuContent,
  ContextMenuGroup,
  ContextMenuLabel,
  ContextMenuItem,
  ContextMenuCheckboxItem,
  ContextMenuRadioGroup,
  ContextMenuRadioItem,
  ContextMenuSeparator,
  ContextMenuShortcut,
  ContextMenuSub,
  ContextMenuSubTrigger,
  ContextMenuSubContent,
} from "@/components/ui/context-menu"

type commonCheckboxProps = {
  checked?: allowedPrimitiveT[]
  indicatorAt?: indicatorAtT
  onCheckedChange?: (value: allowedPrimitiveT, checked: boolean) => void
}

type commonRadioProps = {
  value?: allowedPrimitiveT
  indicatorAt?: indicatorAtT
  onValueChange?: (value: allowedPrimitiveT) => void
}

type commonSubMenuT = {
  itemCls?: string
  groupCls?: string
  groupLabelCls?: string
}

type commonPropsT = {
  children: React.ReactNode
  itemCls?: string
  groupCls?: string
  groupLabelCls?: string
  contentProps?: React.ComponentProps<typeof ContextMenuContent>
  onSelect?: (value: allowedPrimitiveT) => void
} & React.ComponentProps<typeof ContextMenu>

// -------

type itemProps = {
  option: menuOptionT
  className?: string
  onSelect?: () => void
}
function Item({
  option,
  className,
  onSelect
}: itemProps) {
  const value = getValue(option)

  if (isSeparator(value)) return <ContextMenuSeparator className={className} />

  const label = getLabel(option)
  const opt: any = typeof option === "object" ? option : {}
  const shortcut = opt?.shortcut

  return (
    <ContextMenuItem
      {...opt}
      onSelect={onSelect}
      className={cn(className, opt?.className)}
    >
      {label}
      {shortcut && <ContextMenuShortcut>{shortcut}</ContextMenuShortcut>}
    </ContextMenuItem>
  )
}

type checkboxItemProps = {
  option: menuInputOptionT
  className?: string
  checked?: boolean
  onCheckedChange?: (checked: boolean) => void
  indicatorAt?: indicatorAtT
}
function CheckboxItem({
  option,
  className,
  checked = false,
  indicatorAt,
  onCheckedChange = () => { }
}: checkboxItemProps) {
  const value = getValue(option)

  if (isSeparator(value)) return <ContextMenuSeparator className={className} />

  const label = getLabel(option)
  const disabled = (option as any)?.disabled

  return (
    <ContextMenuCheckboxItem
      checked={checked}
      disabled={disabled}
      className={className}
      indicatorAt={indicatorAt}
      onCheckedChange={onCheckedChange}
    >
      {label}
    </ContextMenuCheckboxItem>
  )
}

type radioItemProps = {
  option: menuInputOptionT
  className?: string
  indicatorAt?: indicatorAtT
}
function RadioItem({
  option,
  className,
  indicatorAt
}: radioItemProps) {
  const value = getValue(option)

  if (isSeparator(value)) return <ContextMenuSeparator className={className} />

  const label = getLabel(option)
  const disabled = (option as any)?.disabled

  return (
    <ContextMenuRadioItem
      value={`${value}`}
      disabled={disabled}
      className={className}
      indicatorAt={indicatorAt}
    >
      {label}
    </ContextMenuRadioItem>
  )
}

type SubMenuProps = commonSubMenuT & {
  submenu: subMenuT
  onSelect?: (value: allowedPrimitiveT) => void
}
function SubMenu({
  submenu,
  itemCls,
  groupCls,
  groupLabelCls,
  onSelect
}: SubMenuProps) {
  return (
    <ContextMenuSub>
      <ContextMenuSubTrigger className={submenu.triggerCls}>
        {submenu.submenu}
      </ContextMenuSubTrigger>

      <ContextMenuSubContent className={submenu.contentCls}>
        {submenu.options.map((option, i) => {
          if (isGroupMenu(option)) {
            return (
              <ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
                <ContextMenuLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
                  {option.group}
                </ContextMenuLabel>

                {option.options.map((grOpt, j) => (
                  <Item
                    key={getKey(grOpt, j)}
                    option={grOpt}
                    className={itemCls}
                    onSelect={() => onSelect?.(getValue(grOpt))}
                  />
                ))}
              </ContextMenuGroup>
            )
          }

          if (isSubMenu(option)) {
            return (
              <SubMenu
                key={getKey(option, i)}
                submenu={option}
                itemCls={itemCls}
                groupCls={groupCls}
                onSelect={onSelect}
              />
            )
          }

          return (
            <Item
              key={getKey(option, i)}
              option={option}
              className={itemCls}
              onSelect={() => onSelect?.(getValue(option))}
            />
          )
        })}
      </ContextMenuSubContent>
    </ContextMenuSub>
  )
}

type CheckboxSubMenuProps = commonSubMenuT & commonCheckboxProps & {
  submenu: inputSubMenuT
}
function CheckboxSubMenu({
  submenu,
  itemCls,
  groupCls,
  groupLabelCls,
  checked = [],
  indicatorAt,
  onCheckedChange = () => { }
}: CheckboxSubMenuProps) {
  return (
    <ContextMenuSub>
      <ContextMenuSubTrigger className={submenu.triggerCls}>
        {submenu.submenu}
      </ContextMenuSubTrigger>

      <ContextMenuSubContent className={submenu.contentCls}>
        {submenu.options.map((option, i) => {
          if (isInputGroupMenu(option)) {
            return (
              <ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
                <ContextMenuLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
                  {option.group}
                </ContextMenuLabel>

                {option.options.map((grOpt, j) => {
                  const v = getValue(grOpt)
                  return (
                    <CheckboxItem
                      key={getKey(grOpt, j)}
                      option={grOpt}
                      checked={checked.includes(v)}
                      className={itemCls}
                      indicatorAt={indicatorAt}
                      onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
                    />
                  )
                })}
              </ContextMenuGroup>
            )
          }

          if (isInputSubMenu(option)) {
            return (
              <CheckboxSubMenu
                key={option.submenu}
                submenu={option}
                checked={checked}
                itemCls={itemCls}
                groupCls={groupCls}
                indicatorAt={indicatorAt}
                groupLabelCls={groupLabelCls}
                onCheckedChange={onCheckedChange}
              />
            )
          }

          const v = getValue(option)
          return (
            <CheckboxItem
              key={getKey(option, i)}
              option={option}
              checked={checked.includes(v)}
              className={itemCls}
              indicatorAt={indicatorAt}
              onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
            />
          )
        })}
      </ContextMenuSubContent>
    </ContextMenuSub>
  )
}

type RadioSubMenuProps = commonSubMenuT & commonRadioProps & {
  submenu: inputSubMenuT
}
function RadioSubMenu({
  submenu,
  itemCls,
  groupCls,
  groupLabelCls,
  value = "",
  indicatorAt,
  onValueChange = () => { }
}: RadioSubMenuProps) {
  return (
    <ContextMenuSub>
      <ContextMenuSubTrigger className={submenu.triggerCls}>
        {submenu.submenu}
      </ContextMenuSubTrigger>

      <ContextMenuSubContent className={submenu.contentCls}>
        <ContextMenuRadioGroup value={`${value}`} onValueChange={onValueChange}>
          {submenu.options.map((option, i) => {
            if (isInputGroupMenu(option)) {
              return (
                <ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
                  <ContextMenuLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
                    {option.group}
                  </ContextMenuLabel>

                  {option.options.map((grOpt, j) => (
                    <RadioItem
                      key={getKey(grOpt, j)}
                      option={grOpt}
                      className={itemCls}
                      indicatorAt={indicatorAt}
                    />
                  ))}
                </ContextMenuGroup>
              )
            }

            if (isInputSubMenu(option)) {
              return (
                <RadioSubMenu
                  key={option.submenu}
                  value={value}
                  submenu={option}
                  itemCls={itemCls}
                  groupCls={groupCls}
                  indicatorAt={indicatorAt}
                  groupLabelCls={groupLabelCls}
                  onValueChange={onValueChange}
                />
              )
            }

            return (
              <RadioItem
                key={getKey(option, i)}
                option={option}
                className={itemCls}
                indicatorAt={indicatorAt}
              />
            )
          })}
        </ContextMenuRadioGroup>
      </ContextMenuSubContent>
    </ContextMenuSub>
  )
}

type ContextWrapperProps = commonPropsT & {
  options: menuOptionsT
}
function ContextWrapper({
  children,
  options,
  itemCls,
  groupCls,
  groupLabelCls,
  contentProps,
  onSelect,
  ...props
}: ContextWrapperProps) {
  return (
    <ContextMenu {...props}>
      <ContextMenuTrigger asChild>
        {children}
      </ContextMenuTrigger>

      <ContextMenuContent {...contentProps}>
        {options.map((option, i) => {
          if (isGroupMenu(option)) {
            return (
              <ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
                <ContextMenuLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
                  {option.group}
                </ContextMenuLabel>

                {option.options.map((grOpt, j) => (
                  <Item
                    key={getKey(grOpt, j)}
                    option={grOpt}
                    className={itemCls}
                    onSelect={() => onSelect?.(getValue(grOpt))}
                  />
                ))}
              </ContextMenuGroup>
            )
          }

          if (isSubMenu(option)) {
            return (
              <SubMenu
                key={option.submenu}
                submenu={option}
                itemCls={itemCls}
                groupCls={groupCls}
                groupLabelCls={groupLabelCls}
                onSelect={onSelect}
              />
            )
          }

          return (
            <Item
              key={getKey(option, i)}
              option={option}
              className={itemCls}
              onSelect={() => onSelect?.(getValue(option))}
            />
          )
        })}
      </ContextMenuContent>
    </ContextMenu>
  )
}

type ContextCheckboxWrapperProps = commonPropsT & commonCheckboxProps & {
  options: menuInputOptionsT
  label?: string
}
function ContextCheckboxWrapper({
  children,
  options,

  label,
  contentProps,
  itemCls,
  groupCls,
  groupLabelCls,

  checked: o_checked,
  onCheckedChange: o_onCheckedChange,

  indicatorAt,
  ...props
}: ContextCheckboxWrapperProps) {
  const [i_checked, setIChecked] = useState<allowedPrimitiveT[]>([])

  function i_Checked(v: allowedPrimitiveT, c: boolean) {
    setIChecked(prev => !c ? prev.filter(p => p !== v) : [...prev, v])
  }

  const checked = o_checked ?? i_checked
  const onCheckedChange = o_onCheckedChange ?? i_Checked

  return (
    <ContextMenu {...props}>
      <ContextMenuTrigger asChild>
        {children}
      </ContextMenuTrigger>

      <ContextMenuContent {...contentProps}>
        {label && (
          <>
            <ContextMenuLabel>{label}</ContextMenuLabel>
            <ContextMenuSeparator />
          </>
        )}
        {options.map((option, i) => {
          if (isInputGroupMenu(option)) {
            return (
              <ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
                <ContextMenuLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
                  {option.group}
                </ContextMenuLabel>

                {option.options.map((grOpt, j) => {
                  const v = getValue(grOpt)
                  return (
                    <CheckboxItem
                      key={getKey(grOpt, j)}
                      option={grOpt}
                      checked={checked.includes(v)}
                      className={itemCls}
                      indicatorAt={indicatorAt}
                      onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
                    />
                  )
                })}
              </ContextMenuGroup>
            )
          }

          if (isInputSubMenu(option)) {
            return (
              <CheckboxSubMenu
                key={option.submenu}
                submenu={option}
                checked={checked}
                itemCls={itemCls}
                groupCls={groupCls}
                indicatorAt={indicatorAt}
                groupLabelCls={groupLabelCls}
                onCheckedChange={onCheckedChange}
              />
            )
          }

          const v = getValue(option)
          return (
            <CheckboxItem
              key={getKey(option, i)}
              option={option}
              checked={checked.includes(v)}
              className={itemCls}
              indicatorAt={indicatorAt}
              onCheckedChange={(checked) => onCheckedChange?.(v, checked)}
            />
          )
        })}
      </ContextMenuContent>
    </ContextMenu>
  )
}

type ContextRadioWrapperProps = commonPropsT & commonRadioProps & {
  options: menuInputOptionsT
  label?: string
}
function ContextRadioWrapper({
  children,
  options,

  label,
  itemCls,
  groupCls,
  groupLabelCls,
  contentProps,

  value: o_value,
  onValueChange: o_onValueChange,

  indicatorAt,
  ...props
}: ContextRadioWrapperProps) {
  const [i_value, setIValue] = useState<allowedPrimitiveT>("")

  const value = o_value ?? i_value
  const onValueChange = o_onValueChange ?? setIValue

  return (
    <ContextMenu {...props}>
      <ContextMenuTrigger asChild>
        {children}
      </ContextMenuTrigger>

      <ContextMenuContent {...contentProps}>
        {label && (
          <>
            <ContextMenuLabel>{label}</ContextMenuLabel>
            <ContextMenuSeparator />
          </>
        )}
        <ContextMenuRadioGroup value={`${value}`} onValueChange={v => onValueChange(parseAllowedPrimitive(v))}>
          {options.map((option, i) => {
            if (isInputGroupMenu(option)) {
              return (
                <ContextMenuGroup key={option.group} className={cn(groupCls, option.className)}>
                  <ContextMenuLabel className={cn("pb-0.5 text-xs text-muted-foreground font-normal", groupLabelCls, option.groupLabelCls)}>
                    {option.group}
                  </ContextMenuLabel>

                  {option.options.map((grOpt, j) => (
                    <RadioItem
                      key={getKey(grOpt, j)}
                      option={grOpt}
                      className={itemCls}
                      indicatorAt={indicatorAt}
                    />
                  ))}
                </ContextMenuGroup>
              )
            }

            if (isInputSubMenu(option)) {
              return (
                <RadioSubMenu
                  key={option.submenu}
                  value={value}
                  submenu={option}
                  itemCls={itemCls}
                  groupCls={groupCls}
                  indicatorAt={indicatorAt}
                  groupLabelCls={groupLabelCls}
                  onValueChange={v => onValueChange(parseAllowedPrimitive(v))}
                />
              )
            }

            return (
              <RadioItem
                key={getKey(option, i)}
                option={option}
                className={itemCls}
                indicatorAt={indicatorAt}
              />
            )
          })}
        </ContextMenuRadioGroup>
      </ContextMenuContent>
    </ContextMenu>
  )
}

export {
  ContextWrapper,
  ContextCheckboxWrapper,
  ContextRadioWrapper,
}

Done

You can now use ContextCheckboxWrapper, ContextRadioWrapper, and ContextWrapper.

Usage

Basic

import { ContextCheckboxWrapper, ContextRadioWrapper, ContextWrapper } from "@/components/ui/context-menu-wrapper"
import { Button } from "@/components/ui/button"

export function Basic() {
  const dropdownOptions = ["Option 1", "Option 2", "Option 3"]

  return (
    <>
      <ContextWrapper
        options={dropdownOptions}
      >
        <Button variant="outline">
          Options
        </Button>
      </ContextWrapper>

      <ContextCheckboxWrapper options={dropdownOptions}>
        <Button variant="outline">
          Checkbox
        </Button>
      </ContextCheckboxWrapper>

      <ContextRadioWrapper options={dropdownOptions}>
        <Button variant="outline">
          Radio
        </Button>
      </ContextRadioWrapper>
    </>
  )
}

Controlled

import { useState } from "react"
import { ContextCheckboxWrapper, ContextRadioWrapper, ContextWrapper } from "@/components/ui/context-menu-wrapper"
import { Button } from "@/components/ui/button"

export function Controlled() {
  const [checked, setChecked] = useState<allowedPrimitiveT[]>([])
  const [val, setVal] = useState<allowedPrimitiveT>(true)

  const dropdownOptions = ["Option 1", "Option 2", "Option 3"]

  return (
    <>
      <ContextWrapper
        options={dropdownOptions}
        onSelect={v => console.log(v)}
      >
        <Button variant="outline">
          Options
        </Button>
      </ContextWrapper>

      <ContextCheckboxWrapper
        checked={checked}
        options={dropdownOptions}
        onCheckedChange={(value, isChecked) => {
          setChecked((prev) =>
            isChecked
              ? [...prev, value]
              : prev.filter((x) => x !== value)
          )
        }}
      >
        <Button variant="outline">
          Checkbox
        </Button>
      </ContextCheckboxWrapper>

      <ContextRadioWrapper
        value={val}
        options={dropdownOptions}
        onValueChange={setVal}
      >
        <Button variant="outline">
          Radio
        </Button>
      </ContextRadioWrapper>
    </>
  )
}

Nested Complex Data

const dropdownOptions = [
  { label: "New File", value: "new", shortcut: "Ctrl+N" },
  "Save",
  12,
  { label: <><Banana /> Banana</>, value: "banana" },
  "---",
  {
    group: "Settings",
    options: [
      { label: "Appearance", value: "appearance" },
      22,
      true
    ],
  },

  {
    submenu: "More",
    options: [
      { label: <><Apple /> Apple</>, value: "apple" },
      {
        group: "Tools",
        options: [
          { label: "Formatter", value: "formatter" },
          false,
        ],
      },
    ],
  },
]

export function Complex() {
  return (
    <ContextWrapper
      options={dropdownOptions}
    />
  )
}

Reference

Prop

Type

Prop

Type

ContextWrapper

Prop

Type

ContextCheckboxWrapper

Prop

Type

ContextRadioWrapper

Prop

Type