{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "title": "Button",
  "description": "Button from the Get IT Done design system.",
  "dependencies": [
    "@radix-ui/react-slot@^1.5.0",
    "class-variance-authority@^0.7.0",
    "lucide-react@^0.379.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/recipes.json",
    "https://zero-design.itdone.de/r/status-dot.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/button.tsx",
      "content": "'use client';\n\nimport { Slot, Slottable } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cloneElement, forwardRef, isValidElement } from 'react';\n\nimport { Loader2 } from 'lucide-react';\n\nimport { focusRing, transitionBase } from '@/lib/recipes';\nimport { cn } from '@/lib/utils';\nimport { useLabels, usePreset } from '@/components/ui/labels-context';\nimport { StatusDot, type StatusDotTone } from '@/components/ui/status-dot';\n\nconst buttonVariants = cva('', {\n  variants: {\n    // State model (all variants): rest, hover, active (pressed), `data-[state=open]` (= hover look,\n    // for menu/popover triggers), focus-visible (shared focus ring recipe) and disabled (set in the\n    // base classes). Filled variants carry a 1px border in their own fill color and no shadow.\n    variant: {\n      default: [\n        'border-button-background-default',\n        'text-button-foreground-default',\n        'bg-button-background-default',\n        'hover:text-button-foreground-hover',\n        'hover:bg-button-background-hover',\n        'hover:border-button-background-hover',\n        'data-[state=open]:text-button-foreground-hover',\n        'data-[state=open]:bg-button-background-hover',\n        'data-[state=open]:border-button-background-hover',\n        'active:bg-button-background-hover',\n        // Disabled keeps the neutral disabled border role, never the brand fill colour or a hover stroke.\n        'disabled:border-border-disabled',\n      ],\n      // Brand-filled button (Figma \"Variant=Filled, Color=Brand\", node 3287:427299), bound to the\n      // themeable `semantic/primary` role. Fill AND border use the primary token; hover darkens\n      // both, active goes one step further (`background-brand-bold-active`).\n      primary: [\n        'border-button-background-primary-default',\n        'text-button-foreground-primary-default',\n        'bg-button-background-primary-default',\n        'hover:bg-button-background-primary-hover',\n        'hover:border-button-background-primary-hover',\n        'data-[state=open]:bg-button-background-primary-hover',\n        'data-[state=open]:border-button-background-primary-hover',\n        'active:bg-background-brand-bold-active',\n        'active:border-background-brand-bold-active',\n        'disabled:border-border-disabled',\n      ],\n      // general-error family: the only destructive fill with a dark-mode-tuned `-on` text and\n      // hover step, so text stays >= 4.5:1 in every theme and mode (rest, hover, open, active).\n      destructive: [\n        'border-general-error-default',\n        'text-general-error-on',\n        'bg-general-error-default',\n        'hover:bg-general-error-hover',\n        'hover:border-general-error-hover',\n        'data-[state=open]:bg-general-error-hover',\n        'data-[state=open]:border-general-error-hover',\n        'active:bg-general-error-hover',\n        'active:border-general-error-hover',\n      ],\n      // Outline: neutral `input` stroke, page background, `shadow-xs`. Hover darkens the BORDER\n      // (no background tint); pressed fills `bg-muted`.\n      outline: [\n        'border-input',\n        'bg-background',\n        'text-foreground',\n        'shadow-xs',\n        'hover:border-border-bold',\n        'data-[state=open]:border-border-bold',\n        'active:bg-muted',\n        // Disabled keeps the neutral disabled border role, never a hover or open stroke.\n        'disabled:border-border-disabled',\n      ],\n      // Outline with destructive intent: error-coloured text and stroke on the page background, error tint with on-container text on hover/press (AA in light and dark).\n      'outline-destructive': [\n        'border-general-error-default',\n        'bg-background',\n        'text-general-error-default',\n        'shadow-xs',\n        'hover:bg-general-error-container',\n        'hover:text-general-error-on-container',\n        'data-[state=open]:bg-general-error-container',\n        'data-[state=open]:text-general-error-on-container',\n        'active:bg-general-error-container',\n        'active:text-general-error-on-container',\n      ],\n      // Secondary: soft neutral fill, distinct from ghost (ghost is transparent at rest).\n      secondary: [\n        'border-transparent',\n        'bg-muted',\n        'text-foreground',\n        'hover:bg-muted-strong',\n        'data-[state=open]:bg-muted-strong',\n        'active:bg-muted-strong',\n        // The border stays transparent when disabled (no brand or focus-like stroke).\n        'disabled:border-transparent',\n      ],\n      ghost: [\n        'border-transparent',\n        'bg-transparent',\n        'text-foreground',\n        'hover:bg-muted',\n        'data-[state=open]:bg-muted',\n        'active:bg-muted-strong',\n      ],\n      link: ['border-transparent', 'underline-offset-4', 'hover:font-semibold'],\n    },\n    size: {\n      // Control heights, paddings and icon sizes bind to the `--size-control-*` and `--size-icon-*` tokens, so one markup is comfortable in every theme and compact in `zero`.\n      // Heights are the control tokens: sm 32 (28 in zero), md and default 40 (32 in zero), lg 42 (36 in zero), xl 48, 2xl 60.\n      // Label type: sm 13/500 (12 via the `button-sm` token variables in zero), md and default `typography-button-md`, lg, xl and 2xl their own `typography-button-*` role.\n      sm: ['leading-normal', 'h-control-sm', 'px-button-px-sm', 'gap-button-gap-sm'],\n      default: ['typography-button-md', 'leading-normal', 'h-control-md', 'px-control-px-md', 'gap-2'],\n      md: ['typography-button-md', 'leading-normal', 'h-control-md', 'px-control-px-md', 'gap-2'],\n      lg: ['typography-button-lg', 'leading-normal', 'h-control-lg', 'px-control-px-lg', 'gap-2'],\n      xl: ['typography-button-xl', 'leading-normal', 'h-control-xl', 'px-control-px-xl', 'gap-2'],\n      '2xl': ['typography-button-2xl', 'leading-normal', 'h-15', 'px-control-px-2xl', 'gap-3'],\n      clamp: ['text-clamp', 'leading-clamp', 'h-10', 'px-8'],\n      'clamp-sm': ['text-clamp-small', 'leading-clamp-small', 'h-8', 'px-6'],\n      link: ['text-base', 'leading-base', 'px-8'],\n      'link-sm': ['text-sm', 'leading-sm', 'px-6'],\n      // Icon-only squares share the height of the text size with the same name, so a row that mixes text and icon buttons lines up.\n      // Heights bind to the control tokens: icon-xs 28 (24 in zero), icon-sm 32 (28), icon 40 (32), icon-lg 42 (36), icon-xl 48 (40), icon-2xl 60 in every preset.\n      // icon-xs is the dense list size (row actions, tags, comments): the hit area is the token (24 at least, WCAG 2.5.8) and the glyph is the `icon-xs` token (14).\n      'icon-xs': ['typography-button-sm', 'h-control-xs', 'w-control-xs', '[&>svg]:h-icon-xs', '[&>svg]:w-icon-xs'],\n      'icon-sm': ['typography-button-sm', 'h-control-sm', 'w-control-sm'],\n      icon: ['typography-button-md', 'h-control-md', 'w-control-md'],\n      'icon-lg': ['typography-button-lg', 'h-control-lg', 'w-control-lg'],\n      'icon-xl': ['typography-button-xl', 'h-control-xl', 'w-control-xl'],\n      'icon-2xl': ['typography-button-2xl', 'h-15', 'w-15'],\n      // Density-aware sizes: height comes from the `--size-control-*` token vars, so the same markup is\n      // comfortable (32/40/42) in every theme and compact (28/32/36) in themes that ship compact density\n      // (e.g. `zero`). Opt in per control (or per table) instead of changing the default sizes.\n      // Alias of `sm` (kept for existing consumers): the same density-aware height, padding and gap tokens.\n      'control-sm': ['leading-normal', 'h-control-sm', 'px-button-px-sm', 'gap-button-gap-sm'],\n      'control-md': ['leading-normal', 'h-control-md', 'px-3', 'gap-2'],\n      'control-lg': ['typography-button-md', 'leading-normal', 'h-control-lg', 'px-4', 'gap-2'],\n      'no-size': [],\n    },\n    // Product preset (see DesignSystemProvider `preset`). `zero` tightens the label type, the gap and the icon-only squares to the workload.zero scale.\n    preset: {\n      default: [],\n      zero: [],\n    },\n  },\n  compoundVariants: [\n    // Label type of the small sizes: 13/500 by default; in zero the `button-sm` token variables (12px). The variables are read directly because the `typography-button-sm` role class is overridden by an `inherit` rule in the tokens preset, which made the label take the surrounding font size.\n    { preset: 'default', size: ['sm', 'control-sm', 'control-md'], className: ['text-ui', 'font-medium', 'leading-normal'] },\n    { preset: 'zero', size: ['sm', 'control-sm'], className: ['text-[length:var(--typo-button-sm-size)]', 'font-[number:var(--typo-button-sm-weight)]', 'leading-[var(--typo-button-sm-lh)]', 'tracking-[var(--typo-button-sm-ls)]'] },\n    { preset: 'zero', size: 'control-md', className: ['text-ui', 'font-medium', 'leading-normal', 'gap-gap-control'] },\n    { preset: 'zero', size: ['default', 'md', 'lg', 'xl', 'control-lg'], className: 'gap-gap-control' },\n    { preset: 'zero', variant: ['default', 'primary', 'destructive'], className: 'shadow-elevation-surface' },\n    // Outline in zero: hover tints the surface and the border keeps the default role (also in dark).\n    {\n      preset: 'zero',\n      variant: 'outline',\n      className: ['border-border-default', 'hover:bg-muted', 'hover:border-border-default', 'data-[state=open]:bg-muted', 'data-[state=open]:border-border-default'],\n    },\n  ],\n  defaultVariants: {\n    variant: 'default',\n    size: 'default',\n    preset: 'default',\n  },\n});\n\n/** Variants whose background is a solid, shadowed fill (Figma \"Variant=Filled\"). */\nconst FILLED_VARIANTS = new Set(['default', 'primary', 'destructive']);\n\n// Leading icon slot: the `--size-icon-md` token (20 in most themes, 16 in zero) through `xl`, 24px at `2xl`.\n// Count pill fill on filled buttons: a 5 percent tint of the on-fill text colour with a 30 percent inset outline, so it stays readable (see button-count-contrast.test.ts) instead of the muted grey fill.\nconst FILLED_COUNT: Record<string, string> = {\n  default: 'bg-button-foreground-default/5 ring-button-foreground-default/30',\n  primary: 'bg-button-foreground-primary-default/5 ring-button-foreground-primary-default/30',\n  destructive: 'bg-general-error-on/5 ring-general-error-on/30',\n};\n\n// Round count pill outside zero: 20px high, at least 20px wide (min-width via theme(), because consumer Tailwind 3.3 has no min-w-5 utility), centred, so one digit is a circle and longer text grows into a pill.\nconst COUNT_PILL = ['inline-flex', 'h-5', '[min-width:theme(spacing.5)]', 'items-center', 'justify-center', 'rounded-full', 'px-1', 'tabular-nums', 'text-xs'];\n\nconst iconSlotClass = (size: ButtonProps['size']) => (size === '2xl' ? ['h-6', 'w-6'] : size === 'icon-xs' ? ['h-icon-xs', 'w-icon-xs'] : ['h-icon-md', 'w-icon-md']);\n\nexport interface ButtonProps\n  extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n    Omit<VariantProps<typeof buttonVariants>, 'preset'> {\n  asChild?: boolean;\n  /** Custom leading icon (Figma \"Icon=Default\"). Ignored when `dotLeading` is set. */\n  icon?: React.ReactNode;\n  /**\n   * Renders a small leading status dot instead of a custom icon (Figma \"Icon=Dot leading\").\n   * Mutually exclusive with `icon` — `dotLeading` wins if both are supplied.\n   */\n  dotLeading?: boolean;\n  underline?: boolean;\n  // Link variant only: keeps the horizontal padding of the size, so a link button in a toolbar or button group has the same hit area as its neighbours. Without it the link has no padding and reads as inline text.\n  padded?: boolean;\n  // Busy state: shows a spinner in the icon slot, keeps the label, blocks activation and sets `aria-busy`.\n  loading?: boolean;\n  // Accessible busy text read by screen readers while `loading`. Wins over the provider label.\n  loadingLabel?: string;\n  // Small count pill after the label (for example a filter button with 3 active filters).\n  count?: number | string;\n  // Where the count sits when the children end with an icon. `end` (default) puts it last, `before-icon` puts it right after the label and before a trailing icon.\n  countPlacement?: 'end' | 'before-icon';\n  // Small status dot in the bottom right corner, mainly for icon-only buttons (sync, connection). Pair it with `statusLabel`, colour alone is not enough.\n  status?: StatusDotTone;\n  // Text for assistive tech. It is appended to `aria-label` when set, otherwise added as hidden text.\n  statusLabel?: string;\n}\n\nconst Button = forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, icon, dotLeading = false, underline = false, padded = false, loading = false, loadingLabel, count, countPlacement = 'end', status, statusLabel, children, disabled, onClick, 'aria-label': ariaLabel, ...props }, ref) => {\n    const l = useLabels({ loading: loadingLabel });\n    const zero = usePreset() === 'zero';\n    // Without an explicit variant the zero preset renders the secondary (outline) button, filled brand stays opt-in.\n    const resolvedVariant = variant ?? (zero ? 'outline' : 'default');\n    const Comp = asChild ? Slot : 'button';\n    // With `asChild`, Slottable marks the child element; icon and dot render inside it.\n    const isFilled = FILLED_VARIANTS.has(resolvedVariant);\n\n    // Figma's \"_Dot\" anatomy is a fixed status indicator, not a color-axis follower:\n    // on solid/Filled backgrounds it renders as the on-color (matches the label via\n    // `currentColor`); on every other variant it renders as the fixed success token.\n    const dot = dotLeading ? (\n      <span\n        aria-hidden=\"true\"\n        className={cn('inline-block', 'shrink-0', 'h-[10px]', 'w-[10px]', 'rounded-full', {\n          'bg-current': isFilled,\n          'bg-button-background-success-default': !isFilled,\n        })}\n      />\n    ) : null;\n\n    // Match the icon's own className (like the passed-in icon's markup does for size\n    // overrides elsewhere) rather than clipping it inside a fixed-size wrapper, so\n    // consumers can still override sizing per-instance.\n    const spinner = loading ? (\n      <Loader2 aria-hidden=\"true\" data-button-spinner=\"\" className={cn('order-first shrink-0', 'animate-spin', iconSlotClass(size))} />\n    ) : null;\n\n    const leadingIcon =\n      !loading && !dotLeading && icon\n        ? isValidElement<{ className?: string }>(icon)\n          ? cloneElement(icon, { className: cn('order-first shrink-0', iconSlotClass(size), icon.props.className) })\n          : icon\n        : null;\n\n    return (\n      <Comp\n        className={cn(\n          'inline-flex',\n          { relative: Boolean(status) },\n          'items-center',\n          'justify-center',\n          'rounded-control',\n          'border',\n          'border-solid',\n          transitionBase,\n          focusRing,\n          // Count before a trailing icon: icons that are not the leading one move behind the DOM-last count.\n          countPlacement === 'before-icon' && '[&>svg:not(.order-first)]:order-last',\n          // A disabled button that still holds focus (clicked, then disabled) must not keep the focus ring: it reads as an enabled, focused control.\n          'disabled:focus-visible:ring-0',\n          'disabled:focus-visible:ring-offset-0',\n          'disabled:pointer-events-none',\n          'disabled:opacity-disabled',\n          'disabled:data-[loading=true]:opacity-100',\n          { 'underline-offset-4': underline },\n          { 'hover:underline': underline },\n          { 'hover:decoration-2': underline },\n          { underline: underline && resolvedVariant === 'link' },\n          buttonVariants({ variant: resolvedVariant, size, preset: zero ? 'zero' : 'default', className: [resolvedVariant === 'link' && !padded && 'p-0', className] }),\n        )}\n        ref={ref}\n        data-slot=\"button\"\n        disabled={disabled || loading}\n        aria-busy={loading || undefined}\n        data-loading={loading || undefined}\n        data-status={status}\n        aria-label={ariaLabel && status && statusLabel ? `${ariaLabel}, ${statusLabel}` : ariaLabel}\n        onClick={loading ? undefined : onClick}\n        {...props}\n      >\n        {loading ? null : dot}\n        {spinner}\n        {leadingIcon}\n        <Slottable>{children}</Slottable>\n        {count != null && (\n          <span\n            data-button-count=\"\"\n            className={cn(\n              isFilled\n                ? zero\n                  ? ['tabular-nums', 'text-inherit', 'typography-kpi-label-strong', 'rounded-full px-1 ring-1 ring-inset', FILLED_COUNT[resolvedVariant]]\n                  : [...COUNT_PILL, 'text-inherit', 'ring-1 ring-inset', FILLED_COUNT[resolvedVariant]]\n                : zero\n                ? ['typography-kpi-label-strong', 'text-text-subtle', 'tabular-nums']\n                : [...COUNT_PILL, 'bg-muted', 'text-text-muted'],\n            )}\n          >\n            {count}\n          </span>\n        )}\n        {loading ? <span className=\"sr-only\">{l.loading}</span> : null}\n        {status ? <StatusDot tone={status} size=\"md\" className=\"absolute bottom-0 right-0 ring-2 ring-background\" /> : null}\n        {status && statusLabel && !ariaLabel ? <span className=\"sr-only\">{statusLabel}</span> : null}\n      </Comp>\n    );\n  },\n);\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}