{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"badge","type":"registry:component","title":"Badge","description":"A short status, category or count label: six tones, an icon or a status dot, two sizes.","categories":["display"],"dependencies":["@radix-ui/react-direction@1.1.4","clsx@^2.1.1","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/components/badge/badge.module.css","type":"registry:file","target":"src/ui/beamline/components/badge/badge.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* Badge: a tinted label. The tone sets three local colours; the tag role lets a look outline it and ink its text\n     (pop turns badges into stickers), every other look leaves it a quiet tinted pill for dense tables. */\n  .root {\n    --pui-badge-bg: var(--pui-hover);\n    --pui-badge-fg: var(--pui-ink-2);\n    --pui-badge-dot: var(--pui-ink-3);\n    display: inline-flex;\n    flex-shrink: 0;\n    max-width: 100%;\n    align-items: center;\n    gap: var(--pui-space-1);\n    height: 22px;\n    padding-inline: var(--pui-space-2);\n    /* nesting input (O1 §14.1, as Button): a parent that holds this near its rounded corner sets --pui-nest-radius and\n       --pui-nest-inset; the corner becomes radius − inset, clamped between a floor (chip) and the part's own role radius (or --pui-nest-ceiling: a pill container sets it to the pill radius). Unset, the declaration is invalid and the own corner\n       applies. --pui-badge-radius wins over both. */\n    --_badge-nest: clamp(\n      var(--pui-radius-chip),\n      calc(var(--pui-nest-radius) - var(--pui-nest-inset, 0px)),\n      var(--pui-nest-ceiling, var(--pui-radius-pill))\n    );\n    border-radius: var(--pui-badge-radius, var(--_badge-nest, var(--pui-radius-pill)));\n    background: var(--pui-badge-bg);\n    color: var(--m-tag-ink, var(--pui-badge-fg));\n    font-size: var(--pui-text-xs);\n    font-weight: var(--pui-weight-medium);\n    font-variant-numeric: tabular-nums;\n    line-height: 1;\n    white-space: nowrap;\n    /* a tone that changes (Pending → Paid) eases its fill and ink in place; the word itself changes at once */\n    transition:\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default),\n      var(--m-transition);\n  }\n  /* in a cell too narrow for it the label ends in \"…\" (the whole label shows on hover); icon and dot stay */\n  .label {\n    min-width: 0;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    /* the clip keeps descenders: the label is one line tall at the badge's line height */\n    padding-block: 0.1em;\n  }\n  .root :where(svg) {\n    width: 14px;\n    height: 14px;\n    flex-shrink: 0;\n  }\n  .sm {\n    height: 20px;\n    padding-inline: var(--pui-space-1-5);\n    font-size: var(--pui-text-2xs);\n  }\n  .sm :where(svg) {\n    width: 12px;\n    height: 12px;\n  }\n  .accent {\n    --pui-badge-bg: var(--pui-accent-tint);\n    --pui-badge-fg: var(--pui-accent-ink);\n    --pui-badge-dot: var(--pui-accent);\n  }\n  .success {\n    --pui-badge-bg: var(--pui-green-tint);\n    --pui-badge-fg: var(--pui-green);\n    --pui-badge-dot: var(--pui-green);\n  }\n  .warning {\n    --pui-badge-bg: var(--pui-orange-tint);\n    --pui-badge-fg: var(--pui-orange);\n    --pui-badge-dot: var(--pui-orange);\n  }\n  .danger {\n    --pui-badge-bg: var(--pui-red-tint);\n    --pui-badge-fg: var(--pui-red);\n    --pui-badge-dot: var(--pui-red);\n  }\n  .info {\n    --pui-badge-bg: var(--pui-blue-tint);\n    --pui-badge-fg: var(--pui-blue);\n    --pui-badge-dot: var(--pui-blue);\n  }\n  /* a category's own colour (identity): its dot, larger than a status dot so the colour reads, ringed in the badge's\n     own ink so a pale or a dark stop shows on any fill; an icon takes the colour instead; with tint the pill takes a light\n     wash of it and the word stays in ink */\n  .root[data-color] {\n    --pui-badge-dot: var(--_label-color);\n  }\n  .root[data-color] .dot {\n    width: 8px;\n    height: 8px;\n    box-shadow: 0 0 0 1px color-mix(in oklab, currentColor 30%, transparent);\n  }\n  .root[data-color] > :where(svg) {\n    color: var(--_label-color);\n  }\n  .root[data-color=\"tint\"] {\n    --pui-badge-bg: color-mix(in oklab, var(--_label-color) 18%, var(--pui-surface));\n    --pui-badge-fg: var(--pui-ink);\n  }\n  .dot {\n    width: 6px;\n    height: 6px;\n    flex-shrink: 0;\n    border-radius: var(--pui-radius-pill);\n    background: var(--pui-badge-dot);\n    transition: background-color var(--pui-motion-out) var(--pui-ease-default);\n  }\n}\n/* CSS-module composition only; visual rules stay in the component layer. */\n.root {\n  composes: m-tag from global;\n}\n"},{"path":"beamline/components/badge/badge.tsx","type":"registry:file","target":"src/ui/beamline/components/badge/badge.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { BadgeDefaults } from \"./defaults\";\nexport type { BadgeDefaults } from \"./defaults\";\n\nimport type { CSSProperties, HTMLAttributes, PointerEvent, ReactNode, Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./badge.module.css\";\n\nexport type BadgeTone = \"neutral\" | \"accent\" | \"success\" | \"warning\" | \"danger\" | \"info\";\nexport type BadgeSize = \"sm\" | \"md\";\n\nexport interface BadgeClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the dot part; no override by default. */\n  dot?: string;\n  /** Classes for the label part; no override by default. */\n  label?: string;\n}\n\nexport interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {\n  /** Semantic colour; pair it with a word or icon (colour is never the only signal). */\n  tone?: BadgeTone;\n  /** Height 20 / 22 px. */\n  size?: BadgeSize;\n  /** A leading icon element. */\n  icon?: ReactNode;\n  /** A leading status dot in the tone colour, instead of an icon. */\n  dot?: boolean;\n  /** A category's own colour, for a label the user matches across places (a card's \"Billing\" and the filter that\n   *  shows it): a dot in it before the word, ringed in the badge's ink so a pale or dark colour reads on any surface.\n   *  Take it from `identity(i, n)` in `@/lib/tone`, never a status tone. Omitted by default: no dot. */\n  color?: string;\n  /** With `color`, the pill takes a light tint of it as well (the word stays in ink). Default false: a neutral pill. */\n  tint?: boolean;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLSpanElement>;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: BadgeClassNames;\n}\n\n/** A short label for status, category or count; a category the user matches across places takes `color` (a dot,\n *  optionally a tint, from the identity ramp). A tone that changes (Pending → Paid) eases its colours in place;\n *  nothing else moves, so it costs nothing in a dense table. In a cell too narrow for it, the label ends in \"…\" and the\n *  whole label shows on hover. Reads scoped presentation defaults. A look may outline it (pop makes it a sticker). */\nexport function Badge({\n  tone: toneProp,\n  size: sizeProp,\n  icon,\n  dot = false,\n  color,\n  tint = false,\n  className,\n  classNames,\n  children,\n  ref,\n  onPointerEnter,\n  ...props\n}: BadgeProps) {\n  const scopeDefaults = useComponentDefaults<BadgeDefaults>(\"badge\", [\"tone\", \"size\"]);\n  const tone = toneProp ?? scopeDefaults.tone ?? \"neutral\";\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n\n  // a label cut by a narrow cell names itself on hover; nothing is measured until someone points at it\n  const nameIfCut = (e: PointerEvent<HTMLSpanElement>) => {\n    onPointerEnter?.(e);\n    const label = e.currentTarget.querySelector<HTMLElement>(\"[data-slot=badge-label]\");\n    if (!label || props.title !== undefined) return;\n    e.currentTarget.title = label.scrollWidth > label.clientWidth + 1 ? (label.textContent ?? \"\") : \"\";\n  };\n  return (\n    <span\n      ref={ref}\n      data-slot=\"badge\"\n      data-tone={tone}\n      data-color={color ? (tint ? \"tint\" : \"dot\") : undefined}\n      onPointerEnter={nameIfCut}\n      {...props}\n      style={color ? ({ \"--_label-color\": color, ...props.style } as CSSProperties) : props.style}\n      className={cn(\n        styles.root,\n        size === \"sm\" && styles.sm,\n        tone !== \"neutral\" && styles[tone],\n        classNames?.root,\n        className,\n      )}\n    >\n      {(dot || (color && !icon)) && (\n        <span data-slot=\"badge-dot\" aria-hidden className={cn(styles.dot, classNames?.dot)} />\n      )}\n      {icon}\n      {children != null && children !== false && (\n        <span data-slot=\"badge-label\" className={cn(styles.label, classNames?.label)}>\n          {children}\n        </span>\n      )}\n    </span>\n  );\n}\n\nexport default Badge;\n"},{"path":"beamline/components/badge/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/badge/defaults.ts","content":"/** Pure allowed presentation defaults for Badge; no component or runtime imports. */\nexport interface BadgeDefaults {\n  /** Shared tone setting; omit to inherit the surrounding scope or the component default. */\n  tone?: \"danger\" | \"accent\" | \"success\" | \"warning\" | \"info\" | \"neutral\";\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\";\n}\n"},{"path":"beamline/lib/utils.ts","type":"registry:file","target":"src/ui/beamline/lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n/* The type roles (utilities.css) and the 2xs step are sizes, not text colours: without this, tailwind-merge reads\n   `text-overline` as a colour and drops it next to `text-ink-3`. A role and a size step still replace each other.\n   Likewise `font-title` is the look's title weight, not a family: read as a family it dropped `font-display` beside it,\n   and every merged page title lost the look's display face. */\nconst twMerge = extendTailwindMerge({\n  extend: {\n    classGroups: {\n      \"font-weight\": [{ font: [\"title\"] }],\n      \"font-family\": [{ font: [\"display\", \"numeric\", \"figure\"] }],\n      \"font-size\": [\n        {\n          text: [\n            \"2xs\",\n            \"display\",\n            \"title\",\n            \"heading\",\n            \"subheading\",\n            \"body\",\n            \"body-sm\",\n            \"reading\",\n            \"label\",\n            \"caption\",\n            \"overline\",\n            \"numeric\",\n            \"code\",\n          ],\n        },\n      ],\n    },\n  },\n});\n\n/** Merge class names, letting later Tailwind utilities win over earlier ones. */\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\n/** A small seeded random generator (LCG): the same seed gives the same sequence, for stable sample data and jitter. */\nexport function rng(seed = 1) {\n  let s = seed >>> 0 || 1;\n  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 2 ** 32;\n}\n\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n"}],"docs":"Badge is in src/ui/beamline/components/badge/badge.tsx. The source is in src/ui/beamline (ui/beamline in a project without src/). After Tailwind, import its styles/beamline.css once, then styles/look-prism.css in your app stylesheet (use relative paths). Keep the shared base before all chosen looks. Wrap your app in <UIRoot preset={prism}> with UIRoot from its runtime/appearance and prism from its looks/prism.  shadcn replaces files when asked; use the Beamline managed-source CLI from the start for three-way upgrades that preserve edits.","meta":{"free":true,"version":"0.2.0-rc.42","library":"https://beamline.io/components/badge"}}