{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "arc-menu",
  "title": "Arc Menu",
  "description": "A bottom-anchored shortcut menu that fans actions along a configurable arc while its trigger shrinks and swaps from a shortcut grid to a close icon.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/base/ui/arc-menu.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\ntype ButtonProps = React.ComponentProps<typeof Button>;\ntype ButtonClickEvent = Parameters<NonNullable<ButtonProps[\"onClick\"]>>[0];\ntype ButtonKeyDownEvent = Parameters<\n  NonNullable<ButtonProps[\"onKeyDown\"]>\n>[0];\n\nconst EASE_OUT = [0.23, 1, 0.32, 1] as const;\nconst EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nconst ITEM_DURATION_SECONDS = 0.22;\nconst TRIGGER_DURATION_SECONDS = 0.17;\nconst ICON_SWAP_DELAY_SECONDS = TRIGGER_DURATION_SECONDS / 2;\nconst REDUCED_DURATION_SECONDS = 0.12;\n\nexport type ArcMenuClassNames = {\n  root?: string;\n  menu?: string;\n  actionSlot?: string;\n  actionButton?: string;\n  actionIcon?: string;\n  trigger?: string;\n  triggerSurface?: string;\n  triggerFace?: string;\n  triggerIcon?: string;\n  caption?: string;\n  closeIcon?: string;\n};\n\nexport type ArcMenuTriggerProps = Omit<\n  ButtonProps,\n  | \"aria-controls\"\n  | \"aria-expanded\"\n  | \"aria-haspopup\"\n  | \"aria-label\"\n  | \"children\"\n  | \"ref\"\n  | \"role\"\n  | \"size\"\n  | \"style\"\n  | \"type\"\n  | \"variant\"\n>;\n\ntype ArcMenuContextValue = {\n  actionSize: number;\n  actionIconSize: number;\n  classNames?: ArcMenuClassNames;\n  close: (skipMotion?: boolean) => void;\n  closeOnAction: boolean;\n};\n\nconst ArcMenuContext = React.createContext<ArcMenuContextValue | undefined>(\n  undefined,\n);\n\nfunction useArcMenuContext() {\n  const context = React.useContext(ArcMenuContext);\n\n  if (!context) {\n    throw new Error(\"ArcMenuAction must be used inside ArcMenu.\");\n  }\n\n  return context;\n}\n\nfunction ShortcutGridIcon() {\n  return (\n    <svg aria-hidden=\"true\" viewBox=\"0 0 24 24\" fill=\"none\">\n      <g transform=\"translate(2 2)\">\n        <path\n          fill=\"currentColor\"\n          fillRule=\"evenodd\"\n          clipRule=\"evenodd\"\n          d=\"M6 11a3 3 0 0 1 3 3v3a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-3a3 3 0 0 1 3-3zm-3 2a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1zm14-2a3 3 0 0 1 3 3v3a3 3 0 0 1-3 3h-3a3 3 0 0 1-3-3v-3a3 3 0 0 1 3-3zm-3 2a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1zM6.154.004A3 3 0 0 1 9 3v3a3 3 0 0 1-3 3H3A3 3 0 0 1 .004 6.154L0 6V3a3 3 0 0 1 3-3h3zM3 2a1 1 0 0 0-1 1v3l.005.103A1 1 0 0 0 3 7h3a1 1 0 0 0 1-1V3a1 1 0 0 0-.897-.995L6 2zM17.154.004A3 3 0 0 1 20 3v3a3 3 0 0 1-3 3h-3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h3zM14 2a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-.898-.995L17 2z\"\n        />\n      </g>\n    </svg>\n  );\n}\n\nfunction CloseIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n    >\n      <path d=\"m5.5 5.5 13 13m0-13-13 13\" />\n    </svg>\n  );\n}\n\nfunction resolvePositive(value: number, fallback: number) {\n  return Number.isFinite(value) && value > 0 ? value : fallback;\n}\n\nfunction resolveAngle(value: number, fallback: number) {\n  return Number.isFinite(value) ? value : fallback;\n}\n\nconst FOCUSABLE_SELECTOR = [\n  \"a[href]\",\n  \"button:not(:disabled)\",\n  \"input:not(:disabled)\",\n  \"select:not(:disabled)\",\n  \"textarea:not(:disabled)\",\n  '[tabindex]:not([tabindex=\"-1\"])',\n].join(\",\");\n\nfunction focusAdjacentToTrigger(\n  trigger: HTMLButtonElement | null,\n  direction: -1 | 1,\n) {\n  if (!trigger) return;\n\n  const focusableElements = Array.from(\n    document.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),\n  ).filter(\n    (element) =>\n      element.tabIndex >= 0 &&\n      !element.hasAttribute(\"hidden\") &&\n      !element.closest(\"[inert]\") &&\n      !element.closest('[aria-hidden=\"true\"]'),\n  );\n  const triggerIndex = focusableElements.indexOf(trigger);\n\n  if (triggerIndex === -1) {\n    trigger.focus();\n    return;\n  }\n\n  const target = focusableElements[triggerIndex + direction];\n\n  (target ?? trigger).focus();\n}\n\nfunction getArcPosition({\n  count,\n  endAngle,\n  index,\n  radius,\n  startAngle,\n}: {\n  count: number;\n  endAngle: number;\n  index: number;\n  radius: number;\n  startAngle: number;\n}) {\n  const progress = count <= 1 ? 0.5 : index / (count - 1);\n  const angle = startAngle + (endAngle - startAngle) * progress;\n  const radians = (angle * Math.PI) / 180;\n\n  return {\n    x: Math.cos(radians) * radius,\n    y: Math.sin(radians) * radius,\n  };\n}\n\nexport type ArcMenuProps = Omit<React.ComponentProps<\"div\">, \"onChange\"> & {\n  /** ArcMenuAction elements distributed along the arc. */\n  children: React.ReactNode;\n  /** Controlled open state. */\n  open?: boolean;\n  /** Initial open state for uncontrolled usage. */\n  defaultOpen?: boolean;\n  /** Called whenever the menu requests an open-state change. */\n  onOpenChange?: (open: boolean) => void;\n  /** Radius from the trigger center to each action center, in pixels. */\n  radius?: number;\n  /** First action angle in degrees. -90 points straight up. */\n  startAngle?: number;\n  /** Last action angle in degrees. -90 points straight up. */\n  endAngle?: number;\n  /** Circular trigger size in pixels. */\n  triggerSize?: number;\n  /** Visual trigger size when its caption is hidden. The hit area stays unchanged. */\n  iconOnlyTriggerSize?: number;\n  /** Visual trigger size while the menu is open. The hit area stays unchanged. */\n  openTriggerSize?: number;\n  /** Circular action size in pixels. */\n  actionSize?: number;\n  /** Trigger icon size in pixels. */\n  triggerIconSize?: number;\n  /** Close icon size in pixels. */\n  closeIconSize?: number;\n  /** Action icon size in pixels. */\n  actionIconSize?: number;\n  /** Icon shown while the menu is closed. */\n  triggerIcon?: React.ReactNode;\n  /** Short caption shown under the trigger icon. */\n  triggerCaption?: React.ReactNode;\n  /** Whether to render the trigger caption. Its text still labels the button. */\n  showTriggerCaption?: boolean;\n  /** Icon shown while the menu is open. */\n  closeIcon?: React.ReactNode;\n  /** Accessible name for the closed trigger. */\n  triggerLabel?: string;\n  /** Accessible name for the open trigger. */\n  closeLabel?: string;\n  /** Accessible name for the menu. */\n  menuLabel?: string;\n  /** Close the menu after an action runs. @default true */\n  closeOnAction?: boolean;\n  /** Props forwarded to the trigger button. */\n  triggerProps?: ArcMenuTriggerProps;\n  /** Classes for styling individual component slots. */\n  classNames?: ArcMenuClassNames;\n};\n\nexport type ArcMenuActionProps = Omit<\n  ButtonProps,\n  \"aria-label\" | \"children\" | \"role\" | \"size\" | \"style\" | \"type\" | \"variant\"\n> & {\n  /** Visible tooltip text and accessible name. */\n  label: string;\n  /** Icon rendered inside the circular action button. */\n  icon: React.ReactNode;\n  /** Override the root menu's close-on-action behavior. */\n  closeOnAction?: boolean;\n  /** Classes applied to this action's icon wrapper. */\n  iconClassName?: string;\n  /** Inline styles that do not replace action geometry. */\n  style?: React.CSSProperties;\n};\n\nexport function ArcMenu({\n  children,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  radius = 72,\n  startAngle = -150,\n  endAngle = -30,\n  triggerSize = 56,\n  iconOnlyTriggerSize = 48,\n  openTriggerSize = 40,\n  actionSize = 40,\n  triggerIconSize = 20,\n  closeIconSize = 28,\n  actionIconSize = 16,\n  triggerIcon,\n  triggerCaption = \"Quick\",\n  showTriggerCaption = true,\n  closeIcon,\n  triggerLabel,\n  closeLabel = \"Close and retract shortcuts\",\n  menuLabel = \"Shortcuts\",\n  closeOnAction = true,\n  triggerProps,\n  classNames,\n  className,\n  style,\n  onKeyDown,\n  ref,\n  ...props\n}: ArcMenuProps) {\n  const [internalOpen, setInternalOpen] = React.useState(defaultOpen);\n  const [skipMotion, setSkipMotion] = React.useState(false);\n  const rootRef = React.useRef<HTMLDivElement | null>(null);\n  const triggerRef = React.useRef<HTMLButtonElement | null>(null);\n  const pendingFocusRef = React.useRef<\"first\" | \"last\" | null>(null);\n  const menuId = React.useId();\n  const shouldReduceMotion = useReducedMotion() ?? false;\n  const controlled = openProp !== undefined;\n  const open = controlled ? openProp : internalOpen;\n  const menuItems = React.Children.toArray(children);\n  const resolvedRadius = resolvePositive(radius, 72);\n  const resolvedTriggerSize = resolvePositive(triggerSize, 56);\n  const resolvedIconOnlyTriggerSize = resolvePositive(\n    iconOnlyTriggerSize,\n    48,\n  );\n  const resolvedOpenTriggerSize = resolvePositive(openTriggerSize, 40);\n  const resolvedActionSize = resolvePositive(actionSize, 40);\n  const resolvedTriggerIconSize = resolvePositive(triggerIconSize, 20);\n  const resolvedCloseIconSize = resolvePositive(closeIconSize, 28);\n  const resolvedActionIconSize = resolvePositive(actionIconSize, 16);\n  const hasVisibleTriggerCaption =\n    showTriggerCaption && Boolean(triggerCaption);\n  const closedTriggerScale = hasVisibleTriggerCaption\n    ? 1\n    : Math.min(resolvedIconOnlyTriggerSize, resolvedTriggerSize) /\n      resolvedTriggerSize;\n  const openTriggerScale = resolvedOpenTriggerSize / resolvedTriggerSize;\n  const captionLabel =\n    typeof triggerCaption === \"string\" ? triggerCaption.trim() : \"\";\n  const resolvedTriggerLabel =\n    triggerLabel?.trim() || captionLabel || \"Open shortcuts\";\n  const resolvedCloseLabel =\n    closeLabel.trim() || \"Close and retract shortcuts\";\n  const resolvedMenuLabel = menuLabel.trim() || \"Shortcuts\";\n  const resolvedStartAngle = resolveAngle(startAngle, -150);\n  const resolvedEndAngle = resolveAngle(endAngle, -30);\n  const {\n    className: triggerClassName,\n    disabled: triggerDisabled,\n    onClick: onTriggerClick,\n    onKeyDown: onTriggerKeyDown,\n    ...restTriggerProps\n  } = triggerProps ?? {};\n\n  const setOpen = React.useCallback(\n    (nextOpen: boolean, shouldSkipMotion = false) => {\n      setSkipMotion(shouldSkipMotion);\n\n      if (!controlled) {\n        setInternalOpen(nextOpen);\n      }\n\n      onOpenChange?.(nextOpen);\n    },\n    [controlled, onOpenChange],\n  );\n\n  React.useEffect(() => {\n    if (!skipMotion) return;\n\n    const frame = window.requestAnimationFrame(() => setSkipMotion(false));\n\n    return () => window.cancelAnimationFrame(frame);\n  }, [open, skipMotion]);\n\n  const setRootRef = React.useCallback(\n    (node: HTMLDivElement | null) => {\n      rootRef.current = node;\n\n      if (typeof ref === \"function\") {\n        ref(node);\n      } else if (ref) {\n        ref.current = node;\n      }\n    },\n    [ref],\n  );\n\n  const getEnabledItems = React.useCallback(\n    () =>\n      Array.from(\n        rootRef.current?.querySelectorAll<HTMLButtonElement>(\n          \"[role='menuitem']:not(:disabled)\",\n        ) ?? [],\n      ),\n    [],\n  );\n\n  const close = React.useCallback(\n    (shouldSkipMotion = false) => {\n      setOpen(false, shouldSkipMotion);\n      triggerRef.current?.focus();\n    },\n    [setOpen],\n  );\n\n  React.useEffect(() => {\n    if (!open) return;\n\n    const handlePointerDown = (event: PointerEvent) => {\n      const root = rootRef.current;\n\n      if (!root || !(event.target instanceof Node)) return;\n      if (root.contains(event.target)) return;\n\n      setOpen(false);\n    };\n\n    document.addEventListener(\"pointerdown\", handlePointerDown);\n\n    return () => document.removeEventListener(\"pointerdown\", handlePointerDown);\n  }, [open, setOpen]);\n\n  React.useEffect(() => {\n    if (!open || !pendingFocusRef.current) return;\n\n    const items = getEnabledItems();\n    const target =\n      pendingFocusRef.current === \"first\" ? items[0] : items.at(-1);\n\n    pendingFocusRef.current = null;\n    target?.focus();\n  }, [getEnabledItems, open]);\n\n  const handleTriggerClick = (event: ButtonClickEvent) => {\n    onTriggerClick?.(event);\n\n    if (event.defaultPrevented || triggerDisabled) return;\n\n    if (!open) {\n      pendingFocusRef.current = \"first\";\n    }\n\n    setOpen(!open, event.detail === 0);\n  };\n\n  const handleTriggerKeyDown = (event: ButtonKeyDownEvent) => {\n    onTriggerKeyDown?.(event);\n\n    if (event.defaultPrevented || triggerDisabled) return;\n    if (event.key !== \"ArrowDown\" && event.key !== \"ArrowUp\") return;\n\n    event.preventDefault();\n    pendingFocusRef.current = event.key === \"ArrowDown\" ? \"first\" : \"last\";\n\n    if (!open) {\n      setOpen(true, true);\n      return;\n    }\n\n    const items = getEnabledItems();\n    const target = event.key === \"ArrowDown\" ? items[0] : items.at(-1);\n\n    pendingFocusRef.current = null;\n    target?.focus();\n  };\n\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {\n    onKeyDown?.(event);\n\n    if (event.defaultPrevented || !open) return;\n\n    if (event.key === \"Escape\") {\n      event.preventDefault();\n      close(true);\n      return;\n    }\n\n    if (event.key === \"Tab\") {\n      event.preventDefault();\n      setOpen(false, true);\n      focusAdjacentToTrigger(triggerRef.current, event.shiftKey ? -1 : 1);\n      return;\n    }\n\n    const items = getEnabledItems();\n    const activeIndex = items.indexOf(\n      document.activeElement as HTMLButtonElement,\n    );\n\n    if (activeIndex === -1 || items.length === 0) return;\n\n    let nextIndex: number | undefined;\n\n    if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n      nextIndex = (activeIndex + 1) % items.length;\n    } else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n      nextIndex = (activeIndex - 1 + items.length) % items.length;\n    } else if (event.key === \"Home\") {\n      nextIndex = 0;\n    } else if (event.key === \"End\") {\n      nextIndex = items.length - 1;\n    } else if (\n      event.key.length === 1 &&\n      !event.altKey &&\n      !event.ctrlKey &&\n      !event.metaKey\n    ) {\n      const query = event.key.toLocaleLowerCase();\n\n      for (let offset = 1; offset <= items.length; offset += 1) {\n        const candidateIndex = (activeIndex + offset) % items.length;\n        const candidateLabel =\n          items[candidateIndex]\n            ?.getAttribute(\"aria-label\")\n            ?.toLocaleLowerCase() ?? \"\";\n\n        if (candidateLabel.startsWith(query)) {\n          nextIndex = candidateIndex;\n          break;\n        }\n      }\n    }\n\n    if (nextIndex !== undefined) {\n      event.preventDefault();\n      items[nextIndex]?.focus();\n    }\n  };\n\n  const contextValue = React.useMemo<ArcMenuContextValue>(\n    () => ({\n      actionSize: resolvedActionSize,\n      actionIconSize: resolvedActionIconSize,\n      classNames,\n      close,\n      closeOnAction,\n    }),\n    [\n      classNames,\n      close,\n      closeOnAction,\n      resolvedActionIconSize,\n      resolvedActionSize,\n    ],\n  );\n\n  return (\n    <ArcMenuContext.Provider value={contextValue}>\n      <div\n        {...props}\n        ref={setRootRef}\n        data-slot=\"arc-menu\"\n        data-state={open ? \"open\" : \"closed\"}\n        className={cn(\n          \"relative isolate inline-grid place-items-center\",\n          classNames?.root,\n          className,\n        )}\n        style={{\n          width: resolvedTriggerSize,\n          height: resolvedTriggerSize,\n          ...style,\n        }}\n        onKeyDown={handleKeyDown}\n      >\n        <ul\n          id={menuId}\n          role=\"menu\"\n          aria-label={resolvedMenuLabel}\n          aria-hidden={!open}\n          inert={!open || undefined}\n          data-slot=\"arc-menu-list\"\n          className={cn(\"absolute inset-0 m-0 list-none p-0\", classNames?.menu)}\n        >\n          {menuItems.map((child, index) => {\n            const position = getArcPosition({\n              count: menuItems.length,\n              endAngle: resolvedEndAngle,\n              index,\n              radius: resolvedRadius,\n              startAngle: resolvedStartAngle,\n            });\n            const openTransform = `translate3d(${position.x}px, ${position.y}px, 0) scale(1)`;\n            const closedTransform = shouldReduceMotion\n              ? openTransform\n              : \"translate3d(0px, 0px, 0) scale(0.94)\";\n\n            return (\n              <motion.li\n                key={React.isValidElement(child) ? child.key ?? index : index}\n                role=\"none\"\n                data-slot=\"arc-menu-action-slot\"\n                className={cn(\n                  \"absolute top-1/2 left-1/2 grid place-items-center\",\n                  classNames?.actionSlot,\n                )}\n                style={{\n                  width: resolvedActionSize,\n                  height: resolvedActionSize,\n                  marginLeft: -resolvedActionSize / 2,\n                  marginTop: -resolvedActionSize / 2,\n                  pointerEvents: open ? \"auto\" : \"none\",\n                  willChange: open ? \"transform, opacity\" : \"auto\",\n                }}\n                initial={false}\n                animate={{\n                  opacity: open ? 1 : 0,\n                  transform: open ? openTransform : closedTransform,\n                }}\n                transition={{\n                  duration: skipMotion\n                    ? 0\n                    : shouldReduceMotion\n                      ? REDUCED_DURATION_SECONDS\n                      : ITEM_DURATION_SECONDS,\n                  ease: EASE_OUT,\n                }}\n              >\n                {child}\n              </motion.li>\n            );\n          })}\n        </ul>\n\n        <Button\n          {...restTriggerProps}\n          ref={triggerRef}\n          type=\"button\"\n          variant=\"link\"\n          size=\"icon-lg\"\n          disabled={triggerDisabled}\n          aria-label={open ? resolvedCloseLabel : resolvedTriggerLabel}\n          aria-expanded={open}\n          aria-haspopup=\"menu\"\n          aria-controls={menuId}\n          data-state={open ? \"open\" : \"closed\"}\n          data-slot=\"arc-menu-trigger\"\n          onClick={handleTriggerClick}\n          onKeyDown={handleTriggerKeyDown}\n          className={cn(\n            \"relative origin-center touch-manipulation rounded-full p-0 no-underline\",\n            classNames?.trigger,\n            triggerClassName,\n          )}\n          style={{ width: resolvedTriggerSize, height: resolvedTriggerSize }}\n        >\n          <motion.span\n            aria-hidden=\"true\"\n            data-slot=\"arc-menu-trigger-surface\"\n            className={cn(\n              \"pointer-events-none absolute inset-0 rounded-full bg-primary shadow-sm transition-colors duration-150 group-hover/button:bg-primary/90 group-active/button:bg-primary/90 motion-reduce:transition-none\",\n              classNames?.triggerSurface,\n            )}\n            initial={false}\n            animate={{\n              transform: `scale(${open ? openTriggerScale : closedTriggerScale})`,\n            }}\n            transition={{\n              duration:\n                shouldReduceMotion || skipMotion\n                  ? 0\n                  : TRIGGER_DURATION_SECONDS,\n              ease: EASE_IN_OUT,\n            }}\n          />\n        </Button>\n\n        <div\n          aria-hidden=\"true\"\n          className={cn(\n            \"pointer-events-none absolute inset-0 grid place-items-center text-primary-foreground\",\n            triggerDisabled && \"opacity-50\",\n          )}\n        >\n          <motion.span\n            data-slot=\"arc-menu-trigger-face\"\n            className={cn(\n              \"col-start-1 row-start-1 flex flex-col items-center\",\n              classNames?.triggerFace,\n            )}\n            style={{ gap: 2 }}\n            initial={false}\n            animate={{\n              opacity: open ? 0 : 1,\n              filter:\n                shouldReduceMotion || !open ? \"blur(0px)\" : \"blur(2px)\",\n            }}\n            transition={{\n              duration: skipMotion\n                ? 0\n                : shouldReduceMotion\n                  ? REDUCED_DURATION_SECONDS\n                  : TRIGGER_DURATION_SECONDS,\n              ease: EASE_OUT,\n              delay:\n                shouldReduceMotion || skipMotion || open\n                  ? 0\n                  : ICON_SWAP_DELAY_SECONDS,\n            }}\n          >\n            <span\n              data-icon\n              data-slot=\"arc-menu-trigger-icon\"\n              className={cn(\n                \"grid place-items-center [&_svg]:size-full\",\n                classNames?.triggerIcon,\n              )}\n              style={{\n                width: resolvedTriggerIconSize,\n                height: resolvedTriggerIconSize,\n              }}\n            >\n              {triggerIcon ?? <ShortcutGridIcon />}\n            </span>\n            {showTriggerCaption && triggerCaption ? (\n              <span\n                data-slot=\"arc-menu-caption\"\n                className={cn(\n                  \"text-xs font-medium leading-none\",\n                  classNames?.caption,\n                )}\n              >\n                {triggerCaption}\n              </span>\n            ) : null}\n          </motion.span>\n\n          <motion.span\n            data-icon\n            data-slot=\"arc-menu-close-icon\"\n            className={cn(\n              \"col-start-1 row-start-1 grid place-items-center [&_svg]:size-full\",\n              classNames?.closeIcon,\n            )}\n            style={{\n              width: resolvedCloseIconSize,\n              height: resolvedCloseIconSize,\n            }}\n            initial={false}\n            animate={{\n              opacity: open ? 1 : 0,\n              filter:\n                shouldReduceMotion || open ? \"blur(0px)\" : \"blur(2px)\",\n            }}\n            transition={{\n              duration: skipMotion\n                ? 0\n                : shouldReduceMotion\n                  ? REDUCED_DURATION_SECONDS\n                  : TRIGGER_DURATION_SECONDS,\n              ease: EASE_OUT,\n              delay:\n                shouldReduceMotion || skipMotion || !open\n                  ? 0\n                  : ICON_SWAP_DELAY_SECONDS,\n            }}\n          >\n            {closeIcon ?? <CloseIcon />}\n          </motion.span>\n        </div>\n      </div>\n    </ArcMenuContext.Provider>\n  );\n}\n\nexport function ArcMenuAction({\n  label,\n  icon,\n  closeOnAction: closeOnActionProp,\n  iconClassName,\n  className,\n  style,\n  onClick,\n  disabled,\n  ...props\n}: ArcMenuActionProps) {\n  const context = useArcMenuContext();\n\n  const handleClick = (event: ButtonClickEvent) => {\n    onClick?.(event);\n\n    if (event.defaultPrevented || disabled) return;\n\n    if (closeOnActionProp ?? context.closeOnAction) {\n      context.close(event.detail === 0);\n    }\n  };\n\n  return (\n    <Button\n      {...props}\n      type=\"button\"\n      role=\"menuitem\"\n      variant=\"outline\"\n      size=\"icon-lg\"\n      disabled={disabled}\n      aria-label={label}\n      tabIndex={-1}\n      title={label}\n      data-slot=\"arc-menu-action\"\n      onClick={handleClick}\n      className={cn(\n        \"touch-manipulation rounded-full bg-background/90 p-0 shadow-sm backdrop-blur-md transition-[transform,background-color,color,border-color,box-shadow] duration-150 ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.97] motion-reduce:transform-none motion-reduce:transition-colors\",\n        context.classNames?.actionButton,\n        className,\n      )}\n      style={{\n        ...style,\n        width: context.actionSize,\n        height: context.actionSize,\n      }}\n    >\n      <span\n        aria-hidden=\"true\"\n        data-icon\n        data-slot=\"arc-menu-action-icon\"\n        className={cn(\n          \"grid place-items-center [&_svg]:size-full\",\n          context.classNames?.actionIcon,\n          iconClassName,\n        )}\n        style={{\n          width: context.actionIconSize,\n          height: context.actionIconSize,\n        }}\n      >\n        {icon}\n      </span>\n    </Button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/arc-menu.tsx"
    }
  ],
  "meta": {
    "tags": [
      "floating-action-button",
      "shortcut-menu",
      "quick-actions",
      "radial-menu",
      "optional-caption",
      "controlled",
      "keyboard-navigation",
      "typeahead",
      "custom-icons",
      "per-action-close",
      "menu"
    ],
    "effects": [
      "arc-motion",
      "trigger-scale",
      "icon-swap",
      "simultaneous-actions",
      "fade",
      "reduced-motion"
    ]
  },
  "categories": [
    "overlay"
  ],
  "type": "registry:ui"
}