{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "button",
  "title": "Button",
  "description": "Five variants for every level of emphasis, in four sizes, each with an icon-only square.",
  "dependencies": [
    "@base-ui/react",
    "@stylexjs/stylex"
  ],
  "registryDependencies": [
    "https://constella.aayush.cv/r/base.json"
  ],
  "files": [
    {
      "path": "src/components/primitives/button.tsx",
      "content": "\"use client\";\n\nimport { Button as ButtonPrimitive } from \"@base-ui/react/button\";\nimport * as stylex from \"@stylexjs/stylex\";\n\nimport type { Sx } from \"@/lib/constella/styles\";\nimport { joinStyles, stateStyles, typeStyles } from \"@/lib/constella/styles\";\nimport {\n  colors,\n  durations,\n  easings,\n  fontWeights,\n  media,\n  opacities,\n  presses,\n  sizes,\n  space,\n  strokes,\n} from \"@/lib/constella/tokens.stylex\";\n\nconst buttonVariants = [\n  \"primary\",\n  \"secondary\",\n  \"outline\",\n  \"ghost\",\n  \"link\",\n] as const;\nconst buttonColors = [\"danger\"] as const;\nconst buttonSizes = [\n  \"xs\",\n  \"sm\",\n  \"default\",\n  \"lg\",\n  \"icon-xs\",\n  \"icon-sm\",\n  \"icon\",\n  \"icon-lg\",\n] as const;\nconst buttonCorners = [\"rounded\", \"pill\"] as const;\n\ntype ButtonVariant = (typeof buttonVariants)[number];\ntype ButtonColor = (typeof buttonColors)[number];\ntype ButtonSize = (typeof buttonSizes)[number];\ntype ButtonCorners = (typeof buttonCorners)[number];\n\ninterface ButtonStyleOptions {\n  corners?: ButtonCorners;\n  size?: ButtonSize;\n  color?: ButtonColor;\n  variant?: ButtonVariant;\n}\n\ntype ButtonProps = Omit<ButtonPrimitive.Props, \"className\" | \"style\"> &\n  ButtonStyleOptions & { sx?: Sx };\n\n// A trigger opens something on press, so it doesn't give.\nconst press = \":active:not([aria-haspopup], :disabled)\";\n\n// Working, not unavailable: a busy cursor, and when it can't be pressed, a lighter fade than disabled.\nconst busy = \"[aria-busy='true']\";\nconst loading = \"[aria-busy='true']:disabled\";\n\n// At rest, and while hovered or while its popup is open (Base UI marks the trigger).\nconst interactive = <R, A>(rest: R, active: A) => ({\n  default: rest,\n  \":is([data-popup-open])\": active,\n  [media.hover]: {\n    default: rest,\n    \":is([data-popup-open])\": active,\n    \":hover:not(:disabled)\": active,\n  },\n  \":active:not(:disabled)\": active,\n});\n\nconst kbdInset = (height: string) =>\n  `calc((${height} - ${sizes.kbd}) / 2 - ${strokes.border})`;\n\n// A glyph carries its own whitespace, so the side holding a `data-icon` sits tighter.\n// A Kbd is a box, so it sits as far from the side as from the top and bottom.\nconst inset = (padding: string, tight: string, height: string) => ({\n  paddingInlineEnd: {\n    default: padding,\n    \":has(> [data-icon='inline-end']:not([data-slot='kbd']))\": tight,\n    \":has(> [data-slot='kbd'][data-icon='inline-end'])\": kbdInset(height),\n  },\n  paddingInlineStart: {\n    default: padding,\n    \":has(> [data-icon='inline-start']:not([data-slot='kbd']))\": tight,\n    \":has(> [data-slot='kbd'][data-icon='inline-start'])\": kbdInset(height),\n  },\n});\n\nconst square = (size: string) => ({\n  height: size,\n  paddingInlineEnd: 0,\n  paddingInlineStart: 0,\n  transform: {\n    default: null,\n    [press]: { default: presses.icon, [media.reducedMotion]: null },\n  },\n  width: size,\n});\n\nconst styles = stylex.create({\n  base: {\n    alignItems: \"center\",\n    // Every variant reserves the border, so a fill and an outline are the same size.\n    borderBlockColor: \"transparent\",\n    borderInlineColor: \"transparent\",\n    borderBlockEndWidth: strokes.border,\n    borderBlockStartWidth: strokes.border,\n    borderInlineEndWidth: strokes.border,\n    borderInlineStartWidth: strokes.border,\n    borderBlockStyle: \"solid\",\n    borderInlineStyle: \"solid\",\n    cursor: {\n      default: \"pointer\",\n      \":disabled\": \"not-allowed\",\n      [busy]: \"progress\",\n    },\n    display: \"inline-flex\",\n    flexShrink: 0,\n    fontWeight: fontWeights.semimedium,\n    gap: space.row,\n    justifyContent: \"center\",\n    marginBlock: 0,\n    marginInline: 0,\n    opacity: {\n      default: 1,\n      \":disabled\": opacities.disabled,\n      [loading]: opacities.busy,\n    },\n    textDecorationLine: \"none\",\n    transform: {\n      default: null,\n      [press]: { default: presses.link, [media.reducedMotion]: null },\n    },\n    transitionDuration: `${durations.press}, ${durations.hover}, ${durations.hover}`,\n    transitionProperty: \"transform, background-color, opacity\",\n    touchAction: \"manipulation\",\n    transitionTimingFunction: `${easings.out}, ease, ease`,\n    userSelect: \"none\",\n    WebkitTapHighlightColor: \"transparent\",\n    whiteSpace: \"nowrap\",\n    // In a ButtonGroup, the focused item's ring stays above its neighbors.\n    zIndex: { default: null, \":focus-visible\": 1 },\n  },\n  // Icons shrink with the size: `sizes.iconXs` at xs, `iconSm` at sm, `icon` from default.\n  xs: {\n    ...inset(space.xs, space.row, sizes.controlXs),\n    gap: space.xxs,\n    height: sizes.controlXs,\n  },\n  sm: {\n    ...inset(space.popup, space.row, sizes.controlSm),\n    gap: space.xxs,\n    height: sizes.controlSm,\n  },\n  default: {\n    ...inset(space.popup, space.xs, sizes.controlMd),\n    height: sizes.controlMd,\n  },\n  lg: {\n    ...inset(space.popup, space.xs, sizes.controlLg),\n    height: sizes.controlLg,\n  },\n  iconXs: square(sizes.controlXs),\n  iconSm: square(sizes.controlSm),\n  icon: square(sizes.controlMd),\n  iconLg: square(sizes.controlLg),\n  // The solid fill lifts by thinning, so it needs no hover color of its own.\n  primary: {\n    backgroundColor: colors.accentSolid,\n    color: colors.accentOnSolid,\n    // Disabled and busy repeat inside the hover block, which StyleX ranks above a bare condition.\n    opacity: {\n      default: 1,\n      \":is([data-popup-open])\": opacities.hover,\n      [media.hover]: {\n        default: 1,\n        \":is([data-popup-open])\": opacities.hover,\n        \":hover:not(:disabled)\": opacities.hover,\n        \":disabled\": opacities.disabled,\n        [loading]: opacities.busy,\n      },\n      \":active:not(:disabled)\": opacities.hover,\n      \":disabled\": opacities.disabled,\n      [loading]: opacities.busy,\n    },\n  },\n  secondary: { color: colors.textPrimary },\n  outline: {\n    backgroundClip: \"padding-box\",\n    borderBlockColor: colors.border,\n    borderInlineColor: colors.border,\n    color: colors.textPrimary,\n  },\n  ghost: { color: colors.textPrimary },\n  // A tint, not a solid: destruction is marked, never shouted.\n  danger: {\n    backgroundColor: interactive(colors.dangerSubtle, colors.dangerSubtleHover),\n    color: colors.dangerText,\n  },\n  dangerGhost: {\n    backgroundColor: interactive(\"transparent\", colors.dangerSubtle),\n    color: colors.dangerText,\n  },\n  dangerOutline: {\n    backgroundColor: interactive(colors.background, colors.dangerSubtle),\n    borderBlockColor: colors.dangerBorder,\n    borderInlineColor: colors.dangerBorder,\n    color: colors.dangerText,\n  },\n  dangerLink: { color: colors.dangerText },\n  // The tint answers hover with its own color, so the solid's fade is undone.\n  dangerSolid: {\n    opacity: {\n      default: 1,\n      \":disabled\": opacities.disabled,\n      [loading]: opacities.busy,\n    },\n  },\n  // Reads as text, so it answers with an underline instead of a fill or a press.\n  link: {\n    backgroundColor: \"transparent\",\n    color: colors.textPrimary,\n    textDecorationLine: interactive(\"none\", \"underline\"),\n    textUnderlineOffset: space.xxs,\n    transform: null,\n  },\n});\n\nconst secondary = [styles.secondary, stateStyles.secondary];\n\nconst variantStyles = {\n  ghost: [styles.ghost, stateStyles.ghost],\n  link: styles.link,\n  outline: [styles.outline, stateStyles.outline],\n  primary: styles.primary,\n  secondary,\n} satisfies Record<ButtonVariant, stylex.StyleXStyles>;\n\n// A solid takes the tint too: destruction is marked, never shouted.\nconst colorStyles = {\n  ghost: { danger: styles.dangerGhost },\n  link: { danger: styles.dangerLink },\n  outline: { danger: styles.dangerOutline },\n  primary: { danger: [secondary, styles.danger, styles.dangerSolid] },\n  secondary: { danger: styles.danger },\n} satisfies Record<ButtonVariant, Record<ButtonColor, stylex.StyleXStyles>>;\n\nconst sizeStyles = {\n  \"icon-lg\": styles.iconLg,\n  default: styles.default,\n  icon: styles.icon,\n  \"icon-sm\": styles.iconSm,\n  \"icon-xs\": styles.iconXs,\n  lg: styles.lg,\n  sm: [typeStyles.textSm, styles.sm],\n  xs: [typeStyles.captionLg, styles.xs],\n} satisfies Record<ButtonSize, stylex.StyleXStyles>;\n\nconst cornerStyles = {\n  pill: joinStyles.full,\n  rounded: joinStyles.sm,\n} satisfies Record<ButtonCorners, stylex.StyleXStyles>;\n\n/** A button's styles for another element, like a plain `<a>` that must keep its link role. */\nfunction buttonStyles({\n  corners = \"pill\",\n  size = \"default\",\n  color,\n  variant = \"secondary\",\n}: ButtonStyleOptions = {}) {\n  return [\n    [typeStyles.textSm, styles.base],\n    joinStyles.edges,\n    sizeStyles[size],\n    variantStyles[variant],\n    color && colorStyles[variant][color],\n    cornerStyles[corners],\n  ];\n}\n\nfunction Button({\n  corners = \"pill\",\n  size = \"default\",\n  color,\n  sx,\n  variant = \"secondary\",\n  ...props\n}: ButtonProps) {\n  return (\n    <ButtonPrimitive\n      data-corners={corners}\n      data-size={size}\n      data-slot=\"button\"\n      data-color={color}\n      data-variant={variant}\n      {...props}\n      {...stylex.props(buttonStyles({ corners, size, color, variant }), sx)}\n    />\n  );\n}\n\nexport {\n  Button,\n  buttonCorners,\n  buttonSizes,\n  buttonColors,\n  buttonStyles,\n  buttonVariants,\n};\nexport type {\n  ButtonCorners,\n  ButtonProps,\n  ButtonSize,\n  ButtonColor,\n  ButtonStyleOptions,\n  ButtonVariant,\n};\n",
      "type": "registry:component"
    }
  ],
  "docs": "Usage: https://constella.aayush.cv/docs/components/button/index.md",
  "type": "registry:ui"
}