{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fab-menu",
  "title": "FAB Menu",
  "description": "A FAB that expands into a column of labelled actions.",
  "registryDependencies": [
    "https://guillermo-rebolledo.github.io/materialcn/r/button.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/fab.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/materialcn-theme.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/ui/fab-menu.tsx",
      "content": "import { useCallback, useLayoutEffect, useRef, useState, type ComponentProps } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"./button\"\nimport { FAB, type FABProps } from \"./fab\"\nimport { FABMenuContext, useFABMenu, type FABMenuColor } from \"./fab-menu-context\"\n\ntype FABMenuPlacement = \"bottom-start\" | \"bottom-end\" | \"top-start\" | \"top-end\"\n\n/**\n * Kit geometry (`FAB menu` on the Buttons page): 56dp pill actions with\n * 24dp horizontal padding, an 8dp icon gap and title-medium labels, stacked\n * 4dp apart and 8dp away from a 56dp *round* FAB. Actions take the\n * `<color>-container` roles while the trigger takes the plain role.\n */\ntype FABMenuProps = ComponentProps<\"div\"> & {\n  color?: FABMenuColor\n  defaultOpen?: boolean\n  onOpenChange?: (open: boolean) => void\n  open?: boolean\n  placement?: FABMenuPlacement\n}\n\nfunction FABMenu({\n  children,\n  className,\n  color = \"primary\",\n  defaultOpen = false,\n  onKeyDown,\n  onOpenChange,\n  open: openProp,\n  placement = \"bottom-end\",\n  ...props\n}: FABMenuProps) {\n  const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen)\n  const open = openProp ?? uncontrolledOpen\n  const rootRef = useRef<HTMLDivElement>(null)\n  const triggerRef = useRef<HTMLButtonElement>(null)\n  const setTriggerNode = useCallback((node: HTMLButtonElement | null) => {\n    triggerRef.current = node\n  }, [])\n  const focusTrigger = useCallback(() => triggerRef.current?.focus(), [])\n\n  const setOpen = useCallback((next: boolean) => {\n    if (openProp === undefined) setUncontrolledOpen(next)\n    onOpenChange?.(next)\n  }, [onOpenChange, openProp])\n  const close = useCallback(() => setOpen(false), [setOpen])\n  const toggle = useCallback(() => setOpen(!open), [open, setOpen])\n\n  useLayoutEffect(() => {\n    if (!open) return\n    rootRef.current?.querySelector<HTMLButtonElement>('[data-slot=\"fab-menu-action\"]:not(:disabled)')?.focus()\n  }, [open])\n\n  return (\n    <FABMenuContext.Provider value={{ close, color, focusTrigger, open, setTriggerNode, toggle }}>\n      <div\n        {...props}\n        ref={rootRef}\n        data-slot=\"fab-menu\"\n        data-color={color}\n        data-open={open || undefined}\n        data-placement={placement}\n        className={cn(\"relative z-(--m3-z-menu) inline-flex\", className)}\n        onKeyDown={(event) => {\n          onKeyDown?.(event)\n          if (event.defaultPrevented || !open) return\n          if (event.key === \"Escape\") {\n            event.preventDefault()\n            close()\n            requestAnimationFrame(focusTrigger)\n            return\n          }\n          const actions = Array.from(\n            rootRef.current?.querySelectorAll<HTMLButtonElement>(\n              '[data-slot=\"fab-menu-action\"]:not(:disabled)',\n            ) ?? [],\n          )\n          if ([\"ArrowDown\", \"ArrowUp\", \"Home\", \"End\"].includes(event.key)) {\n            if (!actions.length) return\n            event.preventDefault()\n            const index = actions.indexOf(document.activeElement as HTMLButtonElement)\n            const next = event.key === \"Home\"\n              ? 0\n              : event.key === \"End\"\n                ? actions.length - 1\n                : event.key === \"ArrowDown\"\n                  ? (index + 1) % actions.length\n                  : (index - 1 + actions.length) % actions.length\n            actions[next]?.focus()\n            return\n          }\n          if (event.key !== \"Tab\") return\n          const focusable = Array.from(\n            rootRef.current?.querySelectorAll<HTMLButtonElement>(\n              '[data-slot=\"fab-menu-action\"]:not(:disabled), [data-slot=\"fab\"]:not(:disabled)',\n            ) ?? [],\n          )\n          if (!focusable.length) return\n          const index = focusable.indexOf(document.activeElement as HTMLButtonElement)\n          const next = event.shiftKey\n            ? (index - 1 + focusable.length) % focusable.length\n            : (index + 1) % focusable.length\n          event.preventDefault()\n          focusable[next]?.focus()\n        }}\n      >\n        {children}\n      </div>\n    </FABMenuContext.Provider>\n  )\n}\n\nfunction FABMenuTrigger({ onClick, ref, ...props }: FABProps) {\n  const context = useFABMenu()\n  return (\n    <FAB\n      color={context.color}\n      shape=\"round\"\n      {...props}\n      ref={(node) => {\n        context.setTriggerNode(node)\n        if (typeof ref === \"function\") ref(node)\n        else if (ref) ref.current = node\n      }}\n      aria-expanded={context.open}\n      aria-haspopup=\"menu\"\n      data-open={context.open || undefined}\n      onClick={(event) => {\n        onClick?.(event)\n        if (!event.defaultPrevented) context.toggle()\n      }}\n    />\n  )\n}\n\nfunction FABMenuContent({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useFABMenu()\n  return (\n    <>\n      <button\n        type=\"button\"\n        aria-label=\"Dismiss actions\"\n        aria-hidden={!context.open || undefined}\n        tabIndex={context.open ? 0 : -1}\n        data-slot=\"fab-menu-scrim\"\n        data-open={context.open}\n        className=\"fixed inset-0 -z-10 cursor-default bg-m3-scrim/32 transition-opacity duration-(--m3-spring-effects-fast-duration) data-[open=false]:pointer-events-none data-[open=false]:opacity-0 motion-reduce:transition-none\"\n        onClick={() => {\n          context.close()\n          requestAnimationFrame(context.focusTrigger)\n        }}\n      />\n      <div\n        {...props}\n        role=\"menu\"\n        aria-hidden={!context.open || undefined}\n        inert={!context.open}\n        data-slot=\"fab-menu-content\"\n        data-open={context.open}\n        className={cn(\n          \"absolute flex min-w-max flex-col gap-1\",\n          \"transition-[transform,opacity] duration-(--m3-spring-effects-fast-duration) ease-(--m3-spring-effects-fast) data-[open=false]:pointer-events-none data-[open=false]:scale-95 data-[open=false]:opacity-0 data-[open=true]:scale-100 data-[open=true]:opacity-100 motion-reduce:transition-none\",\n          \"in-data-[placement=bottom-start]:right-0 in-data-[placement=bottom-start]:bottom-full in-data-[placement=bottom-start]:mb-2\",\n          \"in-data-[placement=bottom-end]:bottom-full in-data-[placement=bottom-end]:left-0 in-data-[placement=bottom-end]:mb-2\",\n          \"in-data-[placement=top-start]:top-full in-data-[placement=top-start]:right-0 in-data-[placement=top-start]:mt-2\",\n          \"in-data-[placement=top-end]:top-full in-data-[placement=top-end]:left-0 in-data-[placement=top-end]:mt-2\",\n          className,\n        )}\n      />\n    </>\n  )\n}\n\ntype FABMenuActionProps = Omit<ComponentProps<typeof Button>, \"children\"> & {\n  children?: React.ReactNode\n  closeOnSelect?: boolean\n  label: string\n}\n\nconst actionColorStyles: Record<FABMenuColor, string> = {\n  primary: \"bg-m3-primary-container text-m3-on-primary-container\",\n  secondary: \"bg-m3-secondary-container text-m3-on-secondary-container\",\n  tertiary: \"bg-m3-tertiary-container text-m3-on-tertiary-container\",\n}\n\nfunction FABMenuAction({\n  children,\n  className,\n  closeOnSelect = true,\n  label,\n  onClick,\n  ...props\n}: FABMenuActionProps) {\n  const context = useFABMenu()\n  return (\n    <Button\n      {...props}\n      role=\"menuitem\"\n      data-slot=\"fab-menu-action\"\n      size=\"lg\"\n      className={cn(\n        \"h-14 justify-start gap-2 rounded-[28px] px-6 has-[>svg]:pl-6 text-m3-title-md active:not-disabled:rounded-[28px]\",\n        actionColorStyles[context.color],\n        className,\n      )}\n      onClick={(event) => {\n        onClick?.(event)\n        if (event.defaultPrevented || !closeOnSelect) return\n        context.close()\n        requestAnimationFrame(context.focusTrigger)\n      }}\n    >\n      {children}\n      <span>{label}</span>\n    </Button>\n  )\n}\n\nexport {\n  FABMenu,\n  FABMenuAction,\n  FABMenuContent,\n  FABMenuTrigger,\n  type FABMenuActionProps,\n  type FABMenuPlacement,\n  type FABMenuProps,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/components/ui/fab-menu-context.ts",
      "content": "import { createContext, useContext } from \"react\"\n\ntype FABMenuColor = \"primary\" | \"secondary\" | \"tertiary\"\n\ntype FABMenuContextValue = {\n  close: () => void\n  color: FABMenuColor\n  focusTrigger: () => void\n  open: boolean\n  setTriggerNode: (node: HTMLButtonElement | null) => void\n  toggle: () => void\n}\n\nconst FABMenuContext = createContext<FABMenuContextValue | null>(null)\n\nfunction useFABMenu() {\n  const context = useContext(FABMenuContext)\n  if (!context) throw new Error(\"FABMenu components must be used within FABMenu\")\n  return context\n}\n\nexport { FABMenuContext, useFABMenu, type FABMenuColor }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "actions"
  ],
  "type": "registry:ui"
}