{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"badge","title":"Badge","description":"Polymorphic badge with semantic variants and responsive sizes.","dependencies":["@base-ui/react@1.7.0","class-variance-authority@0.7.1"],"registryDependencies":["@siteplane/base"],"files":[{"path":"packages/ui/src/components/badge/badge.tsx","content":"\"use client\";\n\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport { useRender } from \"@base-ui/react/use-render\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport {\n  siteplaneUIDefaults,\n  useResolvedControlSize,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport { cn } from \"../../system/utils/cn\";\nimport { baseClass, slotClass } from \"../../system/utils/slot-class\";\nimport { wrapTextRuns } from \"../../system/utils/text-runs\";\nimport \"./badge.css\";\n\n// The badge sets no white-space of its own: its label wraps the way the text\n// around the badge does (design-rework.css, the badge-label rule).\nexport const badgeVariants = cva(\n  \"shrink-0 items-center justify-center disabled:opacity-(--sp-rw-disabled-opacity-action) [&_svg]:pointer-events-none [&_svg]:shrink-0 [button&,a&]:cursor-pointer [button&,a&]:pointer-coarse:after:absolute [button&,a&]:pointer-coarse:after:size-full [button&,a&]:pointer-coarse:after:min-h-11 [button&,a&]:pointer-coarse:after:min-w-11\",\n  {\n    defaultVariants: {\n      size: siteplaneUIDefaults.controlSize,\n      variant: \"default\",\n    },\n    variants: {\n      size: {\n        md: \"\",\n        lg: \"\",\n        sm: \"\",\n        xl: \"\",\n      },\n      // The variant colors, the link and button hover tint and the focus\n      // ring all come from the kit rules in design-rework.css, so no variant\n      // adds a class of its own.\n      variant: {\n        accent: \"\",\n        // Deprecated: the earlier name of accent.\n        brand: \"\",\n        default: \"\",\n        destructive: \"\",\n        error: \"\",\n        info: \"\",\n        inverse: \"\",\n        outline: \"\",\n        secondary: \"\",\n        success: \"\",\n        warning: \"\",\n      },\n    },\n  },\n);\n\nexport type BadgeProfile = \"count\" | \"entitlement\" | \"status\" | \"verified\";\n\n// Counts never truncate (design-rework.css), so their label carries no title.\nconst countProfiles: ReadonlySet<string> = new Set([\n  \"count\",\n  \"group-overflow-count\",\n]);\n\n/**\n * The text of a badge is one label: every run of text children, such as\n * `{count} new`, becomes one label span. The label stays on one line and ends\n * with an ellipsis where the badge meets the edge of its space (rulebook K5);\n * its title carries the full text. Icons and other elements stay children of\n * the badge, which lays them out.\n */\nfunction badgeLabels(\n  children: React.ReactNode,\n  titled: boolean,\n): React.ReactNode[] {\n  return wrapTextRuns(children, (text, index) => (\n    <span\n      className=\"sp-badge-label\"\n      data-slot=\"badge-label\"\n      key={`badge-label-${index}`}\n      title={titled ? text.trim() : undefined}\n    >\n      {text}\n    </span>\n  ));\n}\n\nexport type BadgeVariant = Exclude<\n  NonNullable<VariantProps<typeof badgeVariants>[\"variant\"]>,\n  BadgeDeprecatedVariant\n>;\n/**\n * @deprecated `brand` is the earlier name of `accent`, the kit's brand value;\n * it renders `accent`.\n */\nexport type BadgeDeprecatedVariant = \"brand\";\n\nexport interface BadgeProps extends useRender.ComponentProps<\"span\"> {\n  /**\n   * A form of the badge. `count` is a compact count that grows as a pill and\n   * never truncates; `entitlement` labels a plan with its icon; `status` is a\n   * state in words after a colored dot, without fill or contour, with the dot\n   * in the tone of the variant; `verified` is the check circle on an avatar.\n   */\n  profile?: BadgeProfile;\n  /**\n   * `default` is a white surface with a hairline contour and `outline` keeps\n   * only the contour. `secondary` is a light, translucent neutral fill that\n   * reads on a white card, on a tinted row and in Dark.\n   * `accent`, `inverse` and `destructive` are solid fills,\n   * like the Button variants of those names; `info`, `success`, `warning` and\n   * `error` tint with a status color. A `count` badge is `inverse` and an\n   * `entitlement` badge is `info` unless a variant is set. A `status` badge\n   * keeps no fill: its variant gives the dot's tone, `success`, `warning`,\n   * `error` (or `destructive`) and `info` (or `accent`), and gray for the\n   * others. `brand` is the deprecated earlier name of `accent`.\n   */\n  variant?: BadgeVariant | BadgeDeprecatedVariant | null | undefined;\n  size?: VariantProps<typeof badgeVariants>[\"size\"];\n  /**\n   * `pill` rounds the badge fully at every size; the size keeps its height\n   * and padding, and the variant keeps its styling. `default` takes the\n   * theme's badge radius, `--sp-rw-radius-badge`.\n   */\n  shape?: \"default\" | \"pill\";\n  /**\n   * Runs two soft lights, the brand accent and the amber highlight, around\n   * the badge's edge, for the one announcement on a page. Use it on an opaque\n   * badge such as `default`; reduced motion keeps the lights still.\n   */\n  light?: boolean;\n}\n\nexport function Badge({\n  className,\n  profile,\n  variant,\n  size,\n  shape = \"default\",\n  light = false,\n  render,\n  children,\n  ...props\n}: BadgeProps): React.ReactElement {\n  const resolvedSize = useResolvedControlSize(size);\n  const resolvedVariant: BadgeVariant =\n    variant === \"brand\"\n      ? \"accent\"\n      : (variant ??\n        (profile === \"count\"\n          ? \"inverse\"\n          : profile === \"entitlement\"\n            ? \"info\"\n            : \"default\"));\n  // A badge stays on one line: its text is a label that ends with an ellipsis\n  // when the badge would be wider than the space it sits in (rulebook K5), so\n  // a long value never pushes a row apart. The badge-label rule in\n  // design-rework.css owns that; icons beside the label keep their size.\n  const label = badgeLabels(children, !countProfiles.has(profile ?? \"\"));\n  // The status dot comes from the badge; the words carry the state, so the\n  // dot is hidden from assistive technology.\n  const dot =\n    profile === \"status\" ? (\n      <span aria-hidden=\"true\" className=\"sp-badge-dot\" data-slot=\"badge-dot\" />\n    ) : null;\n  const defaultProps = {\n    // The light lives inside the badge, so className and style place a lit\n    // badge exactly like any other.\n    children: light ? (\n      <>\n        <span\n          aria-hidden=\"true\"\n          className=\"sp-badge-light-halo\"\n          data-slot=\"badge-light-halo\"\n        />\n        <span\n          aria-hidden=\"true\"\n          className=\"sp-badge-light-ring\"\n          data-slot=\"badge-light-ring\"\n        />\n        {dot}\n        {label}\n      </>\n    ) : (\n      <>\n        {dot}\n        {label}\n      </>\n    ),\n    className: cn(\n      slotClass(\"badge\", props, render),\n      baseClass(\"badge\", props, render),\n      badgeVariants({ size: resolvedSize, variant: resolvedVariant }),\n      className,\n    ),\n    \"data-foundation\": \"badge\",\n    \"data-light\": light ? \"\" : undefined,\n    \"data-profile\": profile,\n    \"data-shape\": shape,\n    \"data-size\": resolvedSize,\n    \"data-slot\": \"badge\",\n    \"data-variant\": resolvedVariant,\n  };\n\n  return useRender({\n    defaultTagName: \"span\",\n    props: mergeProps<\"span\">(defaultProps, props),\n    render,\n  });\n}\n","type":"registry:ui","target":"~/src/siteplane/components/badge/badge.tsx"},{"path":"packages/ui/src/components/badge/badge.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  /* The Badge light: two lights, the brand accent and the amber highlight, run\n   * half a turn apart around the badge's edge, and a soft halo follows them\n   * outside. Both are drawn inside the badge, so a lit badge places and sizes\n   * like any other badge. A registered angle turns one conic gradient that\n   * paints the 1 px ring on the edge and the blurred halo around it. */\n  @property --sp-badge-light-angle {\n    syntax: \"<angle>\";\n    inherits: false;\n    initial-value: 0deg;\n  }\n\n  /* A lit badge is its own stacking context, so the halo's negative z-index\n   * stays inside it: above the badge's fill, below its label and its focus\n   * outline, and never under the background of a card or page around it. The\n   * ring and the halo measure the badge's edge from its border, the kit's\n   * hairline width. */\n  :root .sp-base-badge[data-light] {\n    --sp-badge-light-edge: var(--sp-rw-border-default);\n    isolation: isolate;\n  }\n\n  /* Each part shows only outside its content box, so the fill stays clean. */\n  .sp-badge-light-halo,\n  .sp-badge-light-ring {\n    --sp-badge-light-brand: oklch(\n      from var(--sp-rw-bg-accent) calc(l + 0.05) c h\n    );\n    position: absolute;\n    background: conic-gradient(\n      from var(--sp-badge-light-angle),\n      transparent 0deg,\n      oklch(from var(--sp-badge-light-brand) l c h / 0.85) 50deg,\n      oklch(\n          from var(--sp-badge-light-brand) calc(l + 0.2) calc(c * 0.55) h / 0.6\n        )\n        85deg,\n      transparent 130deg,\n      transparent 180deg,\n      oklch(from var(--sp-rw-bg-highlight) l c h / 0.85) 230deg,\n      oklch(from var(--sp-rw-bg-highlight) calc(l + 0.1) calc(c * 0.55) h / 0.6)\n        265deg,\n      transparent 310deg\n    );\n    -webkit-mask:\n      linear-gradient(#000 0 0) content-box,\n      linear-gradient(#000 0 0);\n    -webkit-mask-composite: xor;\n    mask:\n      linear-gradient(#000 0 0) content-box,\n      linear-gradient(#000 0 0);\n    mask-composite: exclude;\n    pointer-events: none;\n    animation: sp-badge-light-turn var(--motion-duration-badge-light)\n      var(--motion-ease-linear) infinite;\n  }\n\n  /* The halo. Insets start at the badge's padding box, one border width inside\n   * its edge. The gradient fills the badge grown by 2 px and blurs outwards;\n   * 26 px of transparent border give the blur room inside the mask, and the\n   * content box, which the mask cuts out, is the badge itself. Its radius is\n   * the badge's own plus those 28 px, so the cut-out keeps the badge's corners\n   * at any radius and the gradient stays concentric with them. */\n  .sp-badge-light-halo {\n    inset: calc((var(--sp-badge-light-edge) + 28px) * -1);\n    z-index: -1;\n    border: 26px solid transparent;\n    border-radius: calc(\n      var(--sp-rw-radius-badge, var(--sp-rw-radius-xs)) + 28px\n    );\n    padding: 2px;\n    background-clip: padding-box;\n    filter: blur(7px);\n    opacity: 0.4;\n    transition: opacity var(--motion-duration-base) var(--motion-ease-standard);\n  }\n\n  /* The light itself: a 1 px ring on the badge's edge, above its fill. */\n  .sp-badge-light-ring {\n    inset: calc(var(--sp-badge-light-edge) * -1);\n    border-radius: inherit;\n    padding: 1px;\n  }\n\n  .sp-base-badge[data-light]:hover > .sp-badge-light-halo {\n    opacity: 0.55;\n  }\n\n  /* A lit badge answers hover with the stronger halo, not the grey link tint. */\n  :root :is(a, button).sp-base-badge[data-light]:hover {\n    background-image: none;\n  }\n\n  @keyframes sp-badge-light-turn {\n    to {\n      --sp-badge-light-angle: 360deg;\n    }\n  }\n\n  /* Reduced motion keeps the lights still and the hover instant (M5): the\n   * document mode, the provider preset and the system setting. */\n  html[data-motion-reduce=\"true\"] .sp-badge-light-halo,\n  html[data-motion-reduce=\"true\"] .sp-badge-light-ring,\n  [data-sp-motion=\"reduced\"] .sp-badge-light-halo,\n  [data-sp-motion=\"reduced\"] .sp-badge-light-ring {\n    animation: none;\n    transition-duration: 1ms;\n  }\n\n  @media (prefers-reduced-motion: reduce) {\n    .sp-badge-light-halo,\n    .sp-badge-light-ring {\n      animation: none;\n      transition-duration: 1ms;\n    }\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/badge/badge.css"},{"path":"packages/ui/src/components/badge/badge-internal.ts","content":"import type * as React from \"react\";\n\nimport { Badge, type BadgeProfile, type BadgeProps } from \"./badge\";\n\n/*\n * Kit-internal Badge. Not public API: the badge index does not export this\n * module, and its names and profiles may change in any release.\n *\n * `group-overflow-count` is the circular count at the end of an avatar stack.\n * AvatarGroupCount renders it; applications use AvatarGroupCount.\n */\nexport type KitBadgeProfile = BadgeProfile | \"group-overflow-count\";\n\nexport type KitBadgeProps = Omit<BadgeProps, \"profile\"> & {\n  profile?: KitBadgeProfile;\n};\n\n/** Badge with the kit's composition profiles; the same component. */\nexport const KitBadge = Badge as (props: KitBadgeProps) => React.ReactElement;\n","type":"registry:ui","target":"~/src/siteplane/components/badge/badge-internal.ts"},{"path":"packages/ui/src/components/badge/index.ts","content":"export * from \"./badge\";\n","type":"registry:ui","target":"~/src/siteplane/components/badge/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/badge.md","categories":["Data display"],"meta":{"avoidWhen":["Avoid for the main action of a view; use Button.","Avoid for feedback that must be announced: a badge is not a live region. Use Alert or Toast."],"description":"Polymorphic badge with semantic variants and responsive sizes.","knownLimitations":["A change of status is not announced; put changing feedback in a live region.","A count needs a contextual accessible name, and decorative icons should be hidden from assistive technology."],"providerNotes":[],"releaseStatus":"stable","searchTerms":["badge"],"useWhen":["Use to label a status, a category or a count next to other content."]}}