{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "title": "Button",
  "author": "AniBT <https://anibt.net>",
  "description": "AniBT 「贴纸手账」 button: candy primary with gloss and a ✦ on hover, raised secondary, quiet ghost, red danger, link and field paints on shadcn's variant and size names, with a non-resizing loading state.",
  "dependencies": [
    "class-variance-authority",
    "radix-ui"
  ],
  "registryDependencies": [
    "@anibt/theme",
    "@anibt/spinner"
  ],
  "files": [
    {
      "path": "registry/anibt/ui/button.tsx",
      "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { Slot } from \"radix-ui\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Spinner } from \"@/registry/anibt/ui/spinner\"\n\n/**\n * Button — AniBT 「贴纸手账」.\n *\n * The variant and size names are shadcn's, so this file can replace\n * `components/ui/button.tsx` and every shadcn component that imports\n * `buttonVariants` keeps compiling. Each variant is one A paint, owned by an\n * `anibt-btn-*` recipe from `@anibt/theme`:\n *\n * - `default` — candy: the one filled action on a view. Pink gradient, white\n *   ink, a gloss on the top half, a deeper edge; hover lifts it and pops a ✦,\n *   press pushes it onto its edge.\n * - `secondary` / `outline` — raised: paper with an input-coloured ring and a\n *   1.5px bottom edge; hover lifts it and turns label and ring pink.\n * - `ghost` — quiet: nothing until the pointer is on it, then the pink wash.\n * - `destructive` — danger: red tint, red ink, red edge, the raised lift.\n * - `link` — the pink word, underlined on hover.\n * - `field` — a picker trigger (combobox) that reads as a field, not a key.\n *\n * Sizes are A's control heights: 32 (`default`), 28 (`sm`), 36 (`lg`), and\n * the matching icon squares; `xs` / `icon-xs` (24) exist for shadcn parity.\n *\n * States, the same in 白樱 and 夜樱 because every colour is a token:\n * focus-visible is a 2px ring-colour outline 2px outside the box (it never\n * replaces the paint's own edge); disabled is flat muted; `loading` sets\n * `aria-busy`, blocks interaction and overlays a spinner without resizing\n * the button — every disabled paint is gated on `not-aria-busy`, so work in\n * flight keeps its colour.\n */\n\nconst LINK = \"bg-transparent text-accent-foreground hover:underline hover:underline-offset-4\"\n\nconst DISABLED_FLAT =\n  \"disabled:not-aria-busy:bg-muted aria-disabled:not-aria-busy:bg-muted\"\n\nconst FIELD =\n  \"bg-card font-normal text-foreground inset-ring-1 inset-ring-input hover:inset-ring-anibt-wash-line aria-expanded:inset-ring-[1.5px] aria-expanded:inset-ring-ring disabled:not-aria-busy:bg-muted\"\n\nconst buttonVariants = cva(\n  \"relative inline-flex shrink-0 cursor-pointer items-center justify-center rounded-[10px] text-[13px] leading-none font-semibold whitespace-nowrap outline-hidden select-none aria-busy:cursor-wait disabled:pointer-events-none disabled:not-aria-busy:text-muted-foreground disabled:not-aria-busy:shadow-none aria-disabled:pointer-events-none aria-disabled:not-aria-busy:text-muted-foreground aria-disabled:not-aria-busy:shadow-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid aria-invalid:outline-2 aria-invalid:outline-anibt-danger-fill aria-invalid:outline-solid [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      variant: {\n        default: \"anibt-btn-primary\",\n        secondary: `anibt-btn-raised ${DISABLED_FLAT}`,\n        outline: `anibt-btn-raised ${DISABLED_FLAT}`,\n        ghost: \"anibt-btn-ghost\",\n        destructive: `anibt-btn-danger ${DISABLED_FLAT}`,\n        link: LINK,\n        field: FIELD,\n      },\n      size: {\n        default: \"h-8 gap-2 px-4 has-[>svg]:px-3.5\",\n        xs: \"h-6 gap-1 px-2 text-[12px] has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n        sm: \"h-7 gap-1.5 px-3 text-[12px] has-[>svg]:px-2.5\",\n        lg: \"h-9 gap-2 px-5 text-[14px] has-[>svg]:px-4\",\n        icon: \"size-8\",\n        \"icon-xs\": \"size-6 [&_svg:not([class*='size-'])]:size-3\",\n        \"icon-sm\": \"size-7\",\n        \"icon-lg\": \"size-9\",\n      },\n    },\n    compoundVariants: [\n      // A link is a word, whatever size it was given: no box, no padding.\n      // Compound classes come after the size's, so they win the merge.\n      { variant: \"link\", class: \"h-auto px-0 has-[>svg]:px-0\" },\n    ],\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  }\n)\n\ntype ButtonProps = React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    /** Render the single child element (a link, say) with the button's paint. */\n    asChild?: boolean\n    /**\n     * Base UI's form of `asChild`, so the components of shadcn's `base-*`\n     * styles that render a button as another element keep working:\n     * `render={<a href=\"…\" />}`. The element takes the button's paint and\n     * props; `children` go inside it.\n     */\n    render?: React.ReactElement\n    /**\n     * Base UI compatibility: `false` when `render` is not a `<button>`. It only\n     * decides whether `type` and `disabled` are passed to the rendered element.\n     */\n    nativeButton?: boolean\n    /** Busy: shows the spinner, sets `aria-busy` and blocks interaction. */\n    loading?: boolean\n    /**\n     * Replaces the label while busy. Without it the label keeps its box and\n     * the spinner is painted over it, so the button never changes width.\n     */\n    loadingText?: React.ReactNode\n  }\n\nfunction Button({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  asChild = false,\n  render,\n  nativeButton = true,\n  type = \"button\",\n  loading = false,\n  loadingText,\n  disabled,\n  children,\n  ...props\n}: ButtonProps) {\n  const disabledNow = Boolean(disabled || loading)\n  const overlaySpinner = loading && loadingText === undefined\n  const classes = cn(buttonVariants({ variant, size, className }))\n\n  if (asChild || render) {\n    // A rendered <button> still gets its type and real `disabled`; anything\n    // else (a link) is marked `aria-disabled` instead.\n    const native = Boolean(render) && nativeButton\n    const child = render && children !== undefined ? React.cloneElement(render, undefined, children) : (render ?? children)\n    return (\n      <Slot.Root\n        data-slot=\"button\"\n        data-variant={variant}\n        data-size={size}\n        aria-busy={loading || undefined}\n        aria-disabled={!native && disabledNow ? true : undefined}\n        className={classes}\n        {...(native ? { type, disabled: disabledNow } : {})}\n        {...props}\n      >\n        {child}\n      </Slot.Root>\n    )\n  }\n\n  return (\n    <button\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      aria-busy={loading || undefined}\n      type={type}\n      disabled={disabledNow}\n      className={classes}\n      {...props}\n    >\n      {overlaySpinner ? (\n        <>\n          <Spinner className=\"absolute inset-0 m-auto size-4\" />\n          {/* `opacity-0`, not `invisible`: the label stays in the\n              accessibility tree, so the button keeps its name. */}\n          <span className=\"inline-flex items-center gap-[inherit] opacity-0\">{children}</span>\n        </>\n      ) : (\n        <>\n          {loading ? <Spinner className=\"size-4\" /> : null}\n          {loading ? loadingText : children}\n        </>\n      )}\n    </button>\n  )\n}\n\nexport { Button, buttonVariants }\nexport type { ButtonProps }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "actions"
  ],
  "type": "registry:ui"
}