{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "badge",
  "title": "Badge",
  "description": "A small label in the button's five variants, with icons, a spinner or a link.",
  "dependencies": [
    "@base-ui/react",
    "@stylexjs/stylex"
  ],
  "registryDependencies": [
    "https://constella.aayush.cv/r/base.json"
  ],
  "files": [
    {
      "path": "src/components/primitives/badge.tsx",
      "content": "\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport type { ComponentProps } from \"react\";\n\nimport type { Sx } from \"@/lib/constella/styles\";\nimport { typeStyles } from \"@/lib/constella/styles\";\nimport {\n  badgeVars,\n  colors,\n  durations,\n  fontWeights,\n  media,\n  opacities,\n  radii,\n  sizes,\n  space,\n  strokes,\n} from \"@/lib/constella/tokens.stylex\";\n\nconst badgeVariants = [\n  \"primary\",\n  \"secondary\",\n  \"outline\",\n  \"ghost\",\n  \"link\",\n] as const;\n\ntype BadgeVariant = (typeof badgeVariants)[number];\n\nconst badgeColors = [\"success\", \"info\", \"warning\", \"danger\"] as const;\n\ntype BadgeColor = (typeof badgeColors)[number];\n\nconst badgeSizes = [\"sm\", \"default\", \"lg\"] as const;\n\ntype BadgeSize = (typeof badgeSizes)[number];\n\ntype BadgeProps = Omit<\n  useRender.ComponentProps<\"span\">,\n  \"className\" | \"style\"\n> & {\n  size?: BadgeSize;\n  color?: BadgeColor;\n  sx?: Sx;\n  variant?: BadgeVariant;\n};\n\n// A badge only answers when it's rendered as a link.\nconst link = \":is(a)\";\n\nconst linkHover = <R, A>(rest: R, active: A) => ({\n  default: rest,\n  [media.hover]: { default: rest, [`${link}:hover`]: active },\n  [`${link}:active`]: active,\n});\n\nconst dotIn = (color: BadgeColor) => `:is([data-color='${color}'] > *)`;\n\nconst withAvatar = \":has(> [data-slot='avatar']:first-child)\";\n\n// A glyph carries its own whitespace, so the side holding a `data-icon` sits tighter. A\n// leading avatar fills the height less this inset and the border on every side, and the\n// ends round, so the two stay concentric.\n// Positional: StyleX can't evaluate a destructured parameter.\nconst sizing = (side: string, tight: string, height: string) => ({\n  height,\n  paddingInlineEnd: {\n    default: side,\n    \":has(> [data-icon='inline-end'])\": tight,\n  },\n  paddingInlineStart: {\n    default: side,\n    \":has(> [data-icon='inline-start'])\": tight,\n    [withAvatar]: space.xxxs,\n  },\n  [badgeVars.avatar]: `calc(${height} - 2 * (${space.xxxs} + ${strokes.border}))`,\n});\n\n// A rounded avatar (a logo) keeps the chip corner, a ladder step around its own.\nconst corner = {\n  default: radii.chip,\n  \":has(> [data-slot='avatar'][data-corners='pill']:first-child)\": radii.full,\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    borderStartStartRadius: corner,\n    borderStartEndRadius: corner,\n    borderEndStartRadius: corner,\n    borderEndEndRadius: corner,\n    borderBlockStyle: \"solid\",\n    borderInlineStyle: \"solid\",\n    borderBlockWidth: strokes.border,\n    borderInlineWidth: strokes.border,\n    display: \"inline-flex\",\n    flexShrink: 0,\n    fontWeight: fontWeights.medium,\n    // Safe, so a label wider than the cap clips at its end, not both.\n    justifyContent: \"safe center\",\n    // Capped at its container. Clipped only sideways: the text's line box is taller than the\n    // badge, so a clip on both axes would cut tall accents.\n    maxWidth: \"100%\",\n    minWidth: 0,\n    overflowX: \"clip\",\n    overflowY: \"visible\",\n    textDecorationLine: \"none\",\n    transitionDuration: durations.hover,\n    transitionProperty: \"background-color, border-color, color, opacity\",\n    transitionTimingFunction: \"ease\",\n    // In a sentence it centers on the text; a badge opening on an icon or avatar would\n    // otherwise take its baseline from that child's bottom edge. In a table cell it sits on\n    // the line's top, so it fills the line instead of growing the row.\n    verticalAlign: { default: \"middle\", \":is(td > *)\": \"top\" },\n    whiteSpace: \"nowrap\",\n    width: \"fit-content\",\n  },\n  // Icons are `sizes.iconXs` up to default and `iconSm` at lg.\n  sm: {\n    ...sizing(space.row, space.xxs, sizes.controlXxxs),\n    gap: space.xxs,\n  },\n  default: {\n    ...sizing(space.xs, space.row, sizes.controlXxs),\n    gap: space.xxs,\n  },\n  lg: {\n    ...sizing(space.popup, space.xs, sizes.controlXs),\n    gap: space.row,\n  },\n  primary: {\n    backgroundColor: colors.accentSolid,\n    color: colors.accentOnSolid,\n    opacity: linkHover(1, opacities.hover),\n  },\n  secondary: {\n    backgroundColor: linkHover(colors.fill, colors.fillStrong),\n    color: colors.textPrimary,\n  },\n  outline: {\n    backgroundColor: linkHover(\"transparent\", colors.fillSubtle),\n    borderBlockColor: colors.border,\n    borderInlineColor: colors.border,\n    color: colors.textPrimary,\n  },\n  ghost: {\n    backgroundColor: linkHover(\"transparent\", colors.fillSubtle),\n    color: colors.textPrimary,\n  },\n  dot: {\n    backgroundColor: {\n      default: colors.textMuted,\n      // With no color, the dot on a solid follows its text.\n      \":is([data-variant='primary']:not([data-color]) > *)\": \"currentColor\",\n      [dotIn(\"success\")]: colors.successSolid,\n      [dotIn(\"info\")]: colors.infoSolid,\n      [dotIn(\"warning\")]: colors.warningSolid,\n      [dotIn(\"danger\")]: colors.dangerSolid,\n    },\n    borderStartStartRadius: radii.full,\n    borderStartEndRadius: radii.full,\n    borderEndStartRadius: radii.full,\n    borderEndEndRadius: radii.full,\n    flexShrink: 0,\n    transitionDuration: durations.hover,\n    transitionProperty: \"background-color\",\n    transitionTimingFunction: \"ease\",\n    height: { default: space.row, \":is([data-size='lg'] > *)\": space.xs },\n    width: { default: space.row, \":is([data-size='lg'] > *)\": space.xs },\n  },\n  link: {\n    color: colors.textPrimary,\n    textDecorationLine: linkHover(\"none\", \"underline\"),\n    textUnderlineOffset: space.xxs,\n  },\n  successTint: {\n    backgroundColor: linkHover(colors.successSubtle, colors.successSubtleHover),\n    color: colors.successText,\n  },\n  infoTint: {\n    backgroundColor: linkHover(colors.infoSubtle, colors.infoSubtleHover),\n    color: colors.infoText,\n  },\n  warningTint: {\n    backgroundColor: linkHover(colors.warningSubtle, colors.warningSubtleHover),\n    color: colors.warningText,\n  },\n  dangerTint: {\n    backgroundColor: linkHover(colors.dangerSubtle, colors.dangerSubtleHover),\n    color: colors.dangerText,\n  },\n  successText: { color: colors.successText },\n  infoText: { color: colors.infoText },\n  warningText: { color: colors.warningText },\n  dangerText: { color: colors.dangerText },\n  // The tint answers hover with its own color, so the solid's fade is undone.\n  solidTint: { opacity: 1 },\n});\n\n// A tint on secondary and primary, colored text on ghost and link. Outline keeps its text\n// neutral and colors only its dot, so a row of them stays quiet.\nconst colorStyles = {\n  ghost: {\n    danger: styles.dangerText,\n    info: styles.infoText,\n    success: styles.successText,\n    warning: styles.warningText,\n  },\n  link: {\n    danger: styles.dangerText,\n    info: styles.infoText,\n    success: styles.successText,\n    warning: styles.warningText,\n  },\n  outline: null,\n  primary: {\n    danger: [styles.dangerTint, styles.solidTint],\n    info: [styles.infoTint, styles.solidTint],\n    success: [styles.successTint, styles.solidTint],\n    warning: [styles.warningTint, styles.solidTint],\n  },\n  secondary: {\n    danger: styles.dangerTint,\n    info: styles.infoTint,\n    success: styles.successTint,\n    warning: styles.warningTint,\n  },\n} satisfies Record<\n  BadgeVariant,\n  Record<BadgeColor, stylex.StyleXStyles> | null\n>;\n\nconst sizeStyles = {\n  default: [typeStyles.captionLg, styles.default],\n  lg: [typeStyles.textSm, styles.lg],\n  sm: [typeStyles.captionLg, styles.sm],\n} satisfies Record<BadgeSize, stylex.StyleXStyles>;\n\nconst variantStyles = {\n  ghost: styles.ghost,\n  link: styles.link,\n  outline: styles.outline,\n  primary: styles.primary,\n  secondary: styles.secondary,\n} satisfies Record<BadgeVariant, stylex.StyleXStyles>;\n\n/** A badge's styles for another element. */\nfunction badgeStyles({\n  size = \"default\",\n  color,\n  variant = \"secondary\",\n}: Pick<BadgeProps, \"size\" | \"color\" | \"variant\"> = {}) {\n  return [\n    styles.base,\n    sizeStyles[size],\n    variantStyles[variant],\n    color && colorStyles[variant]?.[color],\n  ];\n}\n\nfunction Badge({\n  render,\n  size = \"default\",\n  color,\n  sx,\n  variant = \"secondary\",\n  ...props\n}: BadgeProps) {\n  return useRender({\n    defaultTagName: \"span\",\n    props: mergeProps<\"span\">(\n      props,\n      stylex.props(badgeStyles({ size, color, variant }), sx)\n    ),\n    render,\n    // Base UI writes state as data attributes: `data-slot`, `data-size`, `data-variant`, `data-color`.\n    state: { size, slot: \"badge\", color, variant },\n  });\n}\n\nfunction BadgeDot({\n  sx,\n  ...props\n}: Omit<ComponentProps<\"span\">, \"className\" | \"style\"> & {\n  sx?: Sx;\n}) {\n  return (\n    <span\n      aria-hidden\n      data-icon=\"inline-start\"\n      data-slot=\"badge-dot\"\n      {...props}\n      {...stylex.props(styles.dot, sx)}\n    />\n  );\n}\n\nexport { Badge, BadgeDot, badgeSizes, badgeColors, badgeStyles, badgeVariants };\nexport type { BadgeProps, BadgeSize, BadgeColor, BadgeVariant };\n",
      "type": "registry:component"
    }
  ],
  "docs": "Usage: https://constella.aayush.cv/docs/components/badge/index.md",
  "type": "registry:ui"
}