{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"button","type":"registry:ui","title":"Button","description":"Triggers an action. Sixteen variants — solid, soft, outline, ghost and link — four sizes, each with a square one for an icon alone, a loading state, and can render as a link.","dependencies":["class-variance-authority","lucide-react","radix-ui"],"registryDependencies":["https://dashral.com/r/utils.json"],"files":[{"path":"registry/dashral/button.tsx","type":"registry:file","target":"components/dashral/button.tsx","content":"import * as React from \"react\"\nimport { Slot } from \"radix-ui\"\nimport { cva } from \"class-variance-authority\"\nimport { Loader2 } from \"lucide-react\"\n\nimport { cn } from \"./lib/utils\"\n\ntype ButtonVariant = \"solid\" | \"soft\" | \"outline\" | \"ghost\" | \"link\"\ntype ButtonColor = \"primary\" | \"neutral\" | \"info\" | \"success\" | \"warning\" | \"destructive\" | \"highlight\"\n\n/** The color a variant takes when none is given: quiet variants are neutral, the rest primary. */\nconst defaultColors: Record<ButtonVariant, ButtonColor> = {\n  solid: \"primary\",\n  soft: \"primary\",\n  outline: \"neutral\",\n  ghost: \"neutral\",\n  link: \"primary\",\n}\n\n// Every class is written out in full so Tailwind finds it.\nconst tones: Record<ButtonVariant, Record<ButtonColor, string>> = {\n  solid: {\n    primary: \"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90\",\n    neutral: \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n    info: \"bg-info text-info-foreground shadow-xs hover:bg-info/90\",\n    success: \"bg-success text-success-foreground shadow-xs hover:bg-success/90\",\n    warning: \"bg-warning text-warning-foreground shadow-xs hover:bg-warning/90\",\n    destructive: \"bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/30\",\n    highlight: \"bg-highlight text-highlight-foreground shadow-xs hover:bg-highlight/90\",\n  },\n  soft: {\n    primary: \"bg-primary/12 text-primary-text hover:bg-primary/18\",\n    neutral: \"bg-foreground/6 text-foreground hover:bg-foreground/10\",\n    info: \"bg-info/12 text-info-text hover:bg-info/18\",\n    success: \"bg-success/12 text-success-text hover:bg-success/18\",\n    warning: \"bg-warning/18 text-warning-text hover:bg-warning/25\",\n    destructive: \"bg-destructive/10 text-destructive-text hover:bg-destructive/15\",\n    highlight: \"bg-highlight/12 text-highlight-text hover:bg-highlight/18\",\n  },\n  outline: {\n    primary: \"border border-primary/40 bg-card text-primary-text shadow-xs hover:bg-primary/8\",\n    neutral: \"border bg-card shadow-xs hover:bg-accent hover:text-accent-foreground\",\n    info: \"border border-info/40 bg-card text-info-text shadow-xs hover:bg-info/8\",\n    success: \"border border-success/40 bg-card text-success-text shadow-xs hover:bg-success/8\",\n    warning: \"border border-warning/50 bg-card text-warning-text shadow-xs hover:bg-warning/10\",\n    destructive: \"border border-destructive/40 bg-card text-destructive-text shadow-xs hover:bg-destructive/8\",\n    highlight: \"border border-highlight/40 bg-card text-highlight-text shadow-xs hover:bg-highlight/8\",\n  },\n  ghost: {\n    primary: \"text-primary-text hover:bg-primary/10\",\n    neutral: \"hover:bg-accent hover:text-accent-foreground\",\n    info: \"text-info-text hover:bg-info/10\",\n    success: \"text-success-text hover:bg-success/10\",\n    warning: \"text-warning-text hover:bg-warning/12\",\n    destructive: \"text-destructive-text hover:bg-destructive/10\",\n    highlight: \"text-highlight-text hover:bg-highlight/10\",\n  },\n  link: {\n    primary: \"text-primary-text underline-offset-4 hover:underline\",\n    neutral: \"text-foreground underline-offset-4 hover:underline\",\n    info: \"text-info-text underline-offset-4 hover:underline\",\n    success: \"text-success-text underline-offset-4 hover:underline\",\n    warning: \"text-warning-text underline-offset-4 hover:underline\",\n    destructive: \"text-destructive-text underline-offset-4 hover:underline\",\n    highlight: \"text-highlight-text underline-offset-4 hover:underline\",\n  },\n}\n\n/** A soft colored glow under a solid button, for the one action that matters most on a page. */\nconst glows: Record<ButtonColor, string> = {\n  primary: \"shadow-lg shadow-primary/30 hover:shadow-primary/40\",\n  neutral: \"shadow-lg shadow-foreground/10\",\n  info: \"shadow-lg shadow-info/30 hover:shadow-info/40\",\n  success: \"shadow-lg shadow-success/30 hover:shadow-success/40\",\n  warning: \"shadow-lg shadow-warning/30 hover:shadow-warning/40\",\n  destructive: \"shadow-lg shadow-destructive/30 hover:shadow-destructive/40\",\n  highlight: \"shadow-lg shadow-highlight/30 hover:shadow-highlight/40\",\n}\n\n// It presses in a little, except where it anchors a menu, popover, tooltip or listbox (Radix marks those with data-state\n// or aria-haspopup): Radix measures the trigger as it opens, and a shrinking one would place the layer a hair off. A\n// Motion button (@dashral/motion, data-press=\"motion\") presses on its own spring instead.\nconst buttonBase = cva(\n  \"inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all outline-none motion-safe:enabled:not-aria-busy:not-data-[state]:not-data-[press=motion]:not-[[aria-haspopup]]:not-[[role=combobox]]:active:scale-[0.97] aria-busy:cursor-progress focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:ring-destructive/15 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      size: {\n        // Each size has a square one for an icon alone, at the same height: xs and icon-xs, sm and icon-sm, and so on.\n        // With an icon beside the label (however deep it sits) the sides tighten; the loading spinner doesn't count, so the\n        // width holds while it spins.\n        xs: \"h-7 gap-1 rounded-md px-2.5 text-xs has-[svg:not([data-spinner])]:px-2 [&_svg:not([class*='size-'])]:size-3.5\",\n        sm: \"h-8 gap-1.5 rounded-md px-3 has-[svg:not([data-spinner])]:px-2.5\",\n        default: \"h-9 px-4 py-2 has-[svg:not([data-spinner])]:px-3\",\n        lg: \"h-10 rounded-md px-6 has-[svg:not([data-spinner])]:px-4\",\n        \"icon-xs\": \"size-7 [&_svg:not([class*='size-'])]:size-3.5\",\n        \"icon-sm\": \"size-8\",\n        icon: \"size-9\",\n        \"icon-lg\": \"size-10\",\n        // Text-sized, for a link-style button inside a sentence or a row of small text.\n        inline: \"h-auto gap-1 rounded-sm p-0 text-[length:inherit]\",\n      },\n    },\n    defaultVariants: { size: \"default\" },\n  }\n)\n\ntype ButtonStyleProps = {\n  /** How much the button stands out: solid, soft, outline, ghost or link. */\n  variant?: ButtonVariant | null\n  /** Its color. Solid, soft and link default to primary; outline and ghost to neutral. */\n  color?: ButtonColor | null\n  size?: \"xs\" | \"sm\" | \"default\" | \"lg\" | \"icon-xs\" | \"icon-sm\" | \"icon\" | \"icon-lg\" | \"inline\" | null\n  /** A soft colored glow under a solid button. */\n  glow?: boolean\n  className?: string\n}\n\n/** The classes for a button, for an element that should look like one (a Link, a menu trigger). */\nfunction buttonVariants({ variant, color, size, glow, className }: ButtonStyleProps = {}) {\n  const kind = variant ?? \"solid\"\n  const tone = color ?? defaultColors[kind]\n  return cn(buttonBase({ size }), tones[kind][tone], glow && kind === \"solid\" && glows[tone], className)\n}\n\nconst spin = \"animate-spin motion-reduce:animate-[spin_1.5s_linear_infinite]\"\n/**\n * A layer of the label's grid cell: laid out like the button itself, its gap and justify included, and as wide as the\n * button, so a className's justify-between, a child's ms-auto or flex-1, and truncate all work as on a plain flex row.\n */\nconst layer = \"col-start-1 row-start-1 inline-flex min-w-0 items-center [justify-content:inherit] gap-[inherit]\"\n\n/**\n * A button: variant says how much it stands out, color what it means. Presses in slightly when clicked (not with\n * reduced motion). `loading` shows a spinner (and `loadingText`) in place of the label, the button keeping its width,\n * and ignores clicks, but keeps the button focusable (aria-busy and aria-disabled instead of disabled), so keyboard\n * users don't lose their place.\n */\nfunction Button({\n  className,\n  variant,\n  color,\n  size,\n  glow,\n  asChild = false,\n  loading = false,\n  loadingText,\n  children,\n  onClick,\n  ...props\n}: Omit<React.ComponentProps<\"button\">, \"color\"> &\n  Omit<ButtonStyleProps, \"className\"> & {\n    asChild?: boolean\n    /** Shows a spinner in place of the label and ignores clicks, without moving focus. */\n    loading?: boolean\n    /** Beside the spinner while loading, e.g. \"Saving…\"; without it the spinner shows alone. Not with asChild. */\n    loadingText?: React.ReactNode\n  }) {\n  const Comp = asChild ? Slot.Root : \"button\"\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      {...props}\n      className={buttonVariants({ variant, color, size, glow, className })}\n      aria-busy={loading || undefined}\n      aria-disabled={loading || props[\"aria-disabled\"]}\n      // preventDefault also stops a submit button from submitting its form.\n      onClick={loading ? (event: React.MouseEvent<HTMLButtonElement>) => event.preventDefault() : onClick}\n    >\n      {/* asChild: Slot looks for Slottable among the direct children (not in a fragment), and can't wrap its child, so the\n          spinner sits before it. */}\n      {asChild && loading && <Loader2 data-spinner=\"\" className={spin} aria-hidden />}\n      {asChild ? (\n        <Slot.Slottable>{children}</Slot.Slottable>\n      ) : (\n        // The label and the loading state share one grid cell, so the button is as wide as the wider of the two and never\n        // jumps as it starts or stops loading; the label stays in place, hidden, while the spinner (and loadingText) show.\n        // The cell fills the button (one 1fr column) and passes its justify on to the layers.\n        <span className=\"grid w-full min-w-0 grid-cols-1 [justify-content:inherit] gap-[inherit]\">\n          <span className={cn(layer, loading && \"invisible\")}>{children}</span>\n          {loading && (\n            <span className={layer}>\n              <Loader2 data-spinner=\"\" className={spin} aria-hidden />\n              {loadingText}\n            </span>\n          )}\n        </span>\n      )}\n    </Comp>\n  )\n}\n\nexport { Button, buttonVariants, type ButtonVariant, type ButtonColor }\n"}],"cssVars":{"theme":{"color-primary-text":"var(--primary-text, var(--primary))","color-destructive-foreground":"var(--destructive-foreground)","color-destructive-text":"var(--destructive-text)","color-info":"var(--info)","color-info-foreground":"var(--info-foreground)","color-info-text":"var(--info-text)","color-success":"var(--success)","color-success-foreground":"var(--success-foreground)","color-success-text":"var(--success-text)","color-warning":"var(--warning)","color-warning-foreground":"var(--warning-foreground)","color-warning-text":"var(--warning-text)","color-highlight":"var(--highlight)","color-highlight-foreground":"var(--highlight-foreground)","color-highlight-text":"var(--highlight-text)"},"light":{"destructive-foreground":"oklch(0.985 0 0)","destructive-text":"#bd2800","info":"oklch(0.5 0.134 242.749)","info-foreground":"oklch(0.985 0 0)","info-text":"oklch(0.443 0.11 240.79)","success":"oklch(0.508 0.118 165.612)","success-foreground":"oklch(0.985 0 0)","success-text":"oklch(0.432 0.095 166.913)","warning":"#f59200","warning-foreground":"oklch(0.3 0.07 60)","warning-text":"#964e00","highlight":"#b8008c","highlight-foreground":"oklch(0.985 0 0)","highlight-text":"#a3007b"},"dark":{"destructive-foreground":"oklch(0.2 0 0)","destructive-text":"#ff8a5c","info":"oklch(0.685 0.169 237.323)","info-foreground":"oklch(0.2 0 0)","info-text":"oklch(0.746 0.16 232.661)","success":"oklch(0.696 0.17 162.48)","success-foreground":"oklch(0.2 0.04 160)","success-text":"oklch(0.765 0.177 163.223)","warning":"#f59200","warning-foreground":"oklch(0.25 0.06 60)","warning-text":"#ffba42","highlight":"#ff2ecf","highlight-foreground":"oklch(0.2 0 0)","highlight-text":"#ff75df"}}}