{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "title": "Button",
  "description": "The five Material button variants across five sizes, in round and square shapes, with the press morph and the loading state that keeps the button's size.",
  "dependencies": [
    "@base-ui/react@^1.7.0",
    "class-variance-authority@^0.7.1"
  ],
  "registryDependencies": [
    "https://guillermo-rebolledo.github.io/materialcn/r/button-group-context.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/button-variants.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/button.tsx",
      "content": "import { Button as ButtonPrimitive } from \"@base-ui/react/button\"\nimport { type VariantProps } from \"class-variance-authority\"\nimport { useContext } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { ButtonGroupContext } from \"./button-group-context\"\nimport { buttonVariants } from \"./button-variants\"\n\ntype ButtonProps = ButtonPrimitive.Props &\n  VariantProps<typeof buttonVariants> & {\n    /**\n     * The action is in flight.\n     *\n     * The button keeps its exact width and height: the label stays in the\n     * layout and is only made invisible, with the indicator laid over it. A\n     * button that shrinks to fit a spinner moves everything beside it, and can\n     * slide out from under the pointer between mousedown and mouseup — so the\n     * click that started the work lands somewhere else.\n     */\n    loading?: boolean\n  }\n\n/**\n * The spinner. It borrows the circular-progress utility rather than the\n * component: that class already collapses to a static arc under\n * `prefers-reduced-motion`, so the button inherits the library's handling\n * instead of growing a second version of it.\n */\nfunction ButtonSpinner() {\n  return (\n    <span\n      data-slot=\"button-spinner\"\n      className=\"absolute inset-0 grid place-items-center\"\n    >\n      <svg\n        viewBox=\"0 0 24 24\"\n        className=\"size-[1.25em]\"\n        fill=\"none\"\n        aria-hidden\n      >\n        <circle\n          className=\"m3-circular-progress-indeterminate\"\n          cx=\"12\"\n          cy=\"12\"\n          r=\"10\"\n          pathLength=\"100\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2.5\"\n          strokeLinecap=\"round\"\n        />\n      </svg>\n    </span>\n  )\n}\n\nfunction Button({\n  children,\n  className,\n  loading = false,\n  onClick,\n  onKeyDown,\n  variant: variantProp,\n  size: sizeProp,\n  shape: shapeProp,\n  ...props\n}: ButtonProps) {\n  const groupDefaults = useContext(ButtonGroupContext)\n  const variant = variantProp ?? groupDefaults.variant ?? \"default\"\n  const size = sizeProp ?? groupDefaults.size ?? \"default\"\n  const shape = shapeProp ?? groupDefaults.shape ?? \"round\"\n\n  return (\n    <ButtonPrimitive\n      data-slot=\"button\"\n      data-loading={loading || undefined}\n      data-shape={shape}\n      data-size={size}\n      data-variant={variant}\n      /*\n       * `aria-disabled`, not `disabled`.\n       *\n       * The real attribute would pull in the variant's disabled styling, and\n       * loading has to look different from disabled — one says \"come back in a\n       * moment\", the other says \"not for you\". It also drops the button out of\n       * the tab order mid-interaction, moving focus somewhere the user did not\n       * ask for. So the element stays focusable and announces itself busy, and\n       * activation is blocked below.\n       */\n      aria-busy={loading || undefined}\n      aria-disabled={loading || undefined}\n      className={cn(\n        buttonVariants({ variant, size, shape, className }),\n        loading && \"relative cursor-wait\"\n      )}\n      onClick={(event) => {\n        if (loading) {\n          event.preventDefault()\n          event.stopPropagation()\n          return\n        }\n        onClick?.(event)\n      }}\n      onKeyDown={(event) => {\n        // Space and Enter activate a button; `aria-disabled` describes the\n        // state but does not enforce it.\n        if (loading && (event.key === \"Enter\" || event.key === \" \")) {\n          event.preventDefault()\n          return\n        }\n        onKeyDown?.(event)\n      }}\n      {...props}\n    >\n      {/*\n        While loading, the label is wrapped so it can be hidden as a unit —\n        `[&>*]:invisible` would miss a plain text label, which is the common\n        case. `display: contents` means the wrapper generates no box, so the\n        button's own flex layout and gap still apply to the label and its icons\n        exactly as before, and `visibility` is inherited, so hiding the wrapper\n        hides the text node too.\n\n        It is only there while loading. Some of the library's own geometry\n        (the split button's icon sizing) selects direct children of a button,\n        and an always-present wrapper would break that for every button in\n        order to serve the one state that needs it.\n      */}\n      {loading ? (\n        <span data-slot=\"button-content\" className=\"contents invisible\">\n          {children}\n        </span>\n      ) : (\n        children\n      )}\n      {loading && <ButtonSpinner />}\n    </ButtonPrimitive>\n  )\n}\n\nexport { Button, buttonVariants }\nexport type { ButtonProps }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "actions"
  ],
  "type": "registry:ui"
}