{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "package-badge",
  "title": "Package Badge",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "An npm-style package card or inline pill with version, weekly downloads and sparkline, size, license, TypeScript and module formats, and a one-click install command.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/copy-button.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/package-badge.tsx",
      "content": "// Ballmac UI: Package Badge. https://ui.ballmac.com/components/package-badge\n\"use client\"\n\nimport * as React from \"react\"\nimport { Boxes, ExternalLink, Scale, Terminal } from \"lucide-react\"\n\nimport { CopyButton } from \"@/components/ballmac/copy-button\"\nimport { cn } from \"@/lib/utils\"\nimport { useMessages } from \"@/lib/ballmac/i18n\"\n\ntype PackageManager = \"npm\" | \"pnpm\" | \"yarn\" | \"bun\"\n\nconst INSTALL: Record<PackageManager, string> = {\n  npm: \"npm install\",\n  pnpm: \"pnpm add\",\n  yarn: \"yarn add\",\n  bun: \"bun add\",\n}\n\n/** 1_250_000 → \"1.3M\", 48_200 → \"48.2k\". */\nfunction formatCount(value: number) {\n  if (value >= 1_000_000) return `${+(value / 1_000_000).toFixed(1)}M`\n  if (value >= 1000) return `${+(value / 1000).toFixed(1)}k`\n  return String(value)\n}\n\n/** 18_432 bytes → \"18.0 kB\". */\nfunction formatBytes(bytes: number) {\n  if (bytes < 1024) return `${bytes} B`\n  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`\n  return `${(bytes / 1024 / 1024).toFixed(1)} MB`\n}\n\nfunction Sparkline({ values, label }: { values: number[]; label: string }) {\n  const max = Math.max(...values, 1)\n  const min = Math.min(...values)\n  const w = 96\n  const h = 28\n  const pts = values.map((v, i) => [(i / Math.max(values.length - 1, 1)) * w, h - 3 - ((v - min) / Math.max(max - min, 1)) * (h - 6)] as const)\n  const line = pts.map(([x, y], i) => `${i ? \"L\" : \"M\"}${x.toFixed(1)} ${y.toFixed(1)}`).join(\" \")\n  const area = `${line} L${w} ${h} L0 ${h} Z`\n  const id = React.useId().replace(/:/g, \"\")\n  return (\n    <svg role=\"img\" aria-label={label} viewBox={`0 0 ${w} ${h}`} className=\"h-7 w-24 overflow-visible text-chart-2\">\n      <defs>\n        <linearGradient id={id} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n          <stop offset=\"0\" stopColor=\"currentColor\" stopOpacity=\"0.28\" />\n          <stop offset=\"1\" stopColor=\"currentColor\" stopOpacity=\"0\" />\n        </linearGradient>\n      </defs>\n      <path d={area} fill={`url(#${id})`} />\n      <path d={line} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.75\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n      <circle cx={pts[pts.length - 1]![0]} cy={pts[pts.length - 1]![1]} r=\"2.25\" fill=\"currentColor\" />\n    </svg>\n  )\n}\n\ntype PackageBadgeProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** Package name, such as \"@acme/ui\". */\n  name: string\n  /** Latest version, without a leading \"v\". */\n  version: string\n  /** One-line description. */\n  description?: string\n  /** Weekly downloads. */\n  downloads?: number\n  /** Weekly download counts, oldest first, for the sparkline. */\n  trend?: number[]\n  /** Minified and gzipped size in bytes. */\n  size?: number\n  /** SPDX license id, such as \"MIT\". */\n  license?: string\n  /** The package ships its own TypeScript types. */\n  types?: boolean\n  /** Module formats, such as [\"ESM\", \"CJS\"]. */\n  formats?: string[]\n  /** Link to the package page. The name becomes a link when set. */\n  href?: string\n  /** \"card\" shows everything, \"inline\" is a one-line pill for READMEs and docs. */\n  variant?: \"card\" | \"inline\"\n  /** Package manager shown in the install command. */\n  manager?: PackageManager\n  /** Hides the install command in the card. */\n  hideInstall?: boolean\n}\n\nfunction PackageBadge({\n  name,\n  version,\n  description,\n  downloads,\n  trend,\n  size,\n  license,\n  types,\n  formats,\n  href,\n  variant = \"card\",\n  manager = \"npm\",\n  hideInstall = false,\n  className,\n  ...props\n}: PackageBadgeProps) {\n  const msg = useMessages()\n  const command = `${INSTALL[manager]} ${name}`\n  const title = href ? (\n    <a\n      href={href}\n      target=\"_blank\"\n      rel=\"noreferrer noopener\"\n      className=\"inline-flex items-center gap-1 rounded-sm outline-none hover:underline hover:underline-offset-4 focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n    >\n      {name}\n      <ExternalLink aria-hidden=\"true\" className=\"size-3 text-muted-foreground\" />\n    </a>\n  ) : (\n    name\n  )\n\n  if (variant === \"inline\") {\n    return (\n      <div\n        data-slot=\"package-badge\"\n        data-variant=\"inline\"\n        className={cn(\"inline-flex h-7 max-w-full items-center overflow-hidden rounded-full border bg-card text-xs shadow-xs\", className)}\n        {...props}\n      >\n        <span className=\"flex h-full items-center gap-1.5 truncate bg-muted/60 pe-2 ps-2.5 font-mono font-medium text-foreground\">\n          <Boxes aria-hidden=\"true\" className=\"size-3.5 shrink-0 text-muted-foreground\" />\n          <span className=\"truncate\">{title}</span>\n        </span>\n        <span className=\"flex h-full items-center border-s px-2.5 font-mono text-foreground tabular-nums\">v{version}</span>\n        {downloads !== undefined && (\n          <span className=\"hidden h-full items-center border-s px-2.5 text-muted-foreground tabular-nums sm:flex\">\n            {msg(\"package-badge.perWeek\", \"{count}/wk\", { count: formatCount(downloads) })}\n          </span>\n        )}\n      </div>\n    )\n  }\n\n  return (\n    <div\n      data-slot=\"package-badge\"\n      data-variant=\"card\"\n      className={cn(\"w-full overflow-hidden rounded-xl border bg-card text-card-foreground shadow-xs\", className)}\n      {...props}\n    >\n      <div className=\"flex items-start gap-3 p-4 pb-3\">\n        <span aria-hidden=\"true\" className=\"flex size-10 shrink-0 items-center justify-center rounded-lg border bg-muted\">\n          <Boxes className=\"size-5 text-foreground\" />\n        </span>\n        <div className=\"min-w-0 flex-1\">\n          <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n            <h3 className=\"truncate font-mono text-sm font-semibold text-foreground\">{title}</h3>\n            <span className=\"rounded-full border bg-muted px-2 py-px font-mono text-[11px] text-foreground tabular-nums\">v{version}</span>\n          </div>\n          {description && <p className=\"mt-0.5 line-clamp-2 text-[13px] leading-5 text-muted-foreground\">{description}</p>}\n        </div>\n        {trend && trend.length > 1 && (\n          <div className=\"hidden shrink-0 flex-col items-end gap-0.5 sm:flex\">\n            <Sparkline values={trend} label={`Weekly downloads over the last ${trend.length} weeks`} />\n          </div>\n        )}\n      </div>\n\n      <dl className=\"grid grid-cols-2 gap-px border-y bg-border text-xs sm:grid-cols-4\">\n        {((): { k: string; v: string; icon?: React.ReactNode }[] => {\n          const items: { k: string; v: string; icon?: React.ReactNode }[] = []\n          if (downloads !== undefined) items.push({ k: \"Weekly downloads\", v: formatCount(downloads) })\n          if (size !== undefined) items.push({ k: \"Gzipped size\", v: formatBytes(size) })\n          if (license) items.push({ k: \"License\", v: license, icon: <Scale aria-hidden=\"true\" className=\"size-3\" /> })\n          if (formats) items.push({ k: \"Formats\", v: formats.join(\" · \") })\n          return items\n        })()\n          .map((item) => (\n            <div key={item.k} className=\"bg-card px-4 py-2.5\">\n              <dt className=\"text-muted-foreground\">{item.k}</dt>\n              <dd className=\"mt-0.5 flex items-center gap-1 text-[13px] font-medium text-foreground tabular-nums\">\n                {item.icon}\n                {item.v}\n              </dd>\n            </div>\n          ))}\n      </dl>\n\n      {(types || !hideInstall) && (\n        <div className=\"flex flex-wrap items-center gap-2 p-3\">\n          {!hideInstall && (\n            <div className=\"flex min-w-0 flex-1 items-center gap-2 rounded-lg border bg-muted/40 py-1 pe-1 ps-3\">\n              <Terminal aria-hidden=\"true\" className=\"size-3.5 shrink-0 text-muted-foreground\" />\n              <code className=\"min-w-0 flex-1 truncate font-mono text-xs text-foreground\">{command}</code>\n              <CopyButton size=\"sm\" value={command} ariaLabel={`Copy install command: ${command}`} />\n            </div>\n          )}\n          {types && (\n            <span className=\"inline-flex h-7 items-center gap-1 rounded-full border px-2.5 text-xs font-medium text-foreground\">\n              <span aria-hidden=\"true\" className=\"flex size-4 items-center justify-center rounded-[4px] bg-foreground font-mono text-[9px] font-bold text-background\">\n                {\"TS\"}\n              </span>\n              {msg(\"package-badge.typesIncluded\", \"Types included\")}\n            </span>\n          )}\n        </div>\n      )}\n    </div>\n  )\n}\n\nexport { PackageBadge, formatCount, formatBytes, type PackageBadgeProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/package-badge.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "npm",
      "package",
      "version",
      "install",
      "badge"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "Pass the facts you already have: name, version, description, downloads, trend, size, license, types, formats. variant is card | inline. manager picks npm | pnpm | yarn | bun for the copyable install command. Nothing is fetched.",
      "whenToUse": [
        "Library landing pages and docs",
        "Showing dependencies in a changelog or README-style page"
      ],
      "whenNotToUse": [
        "Live registry lookups (fetch on the server and pass the result)"
      ],
      "composesWith": [
        "install-tabs",
        "copy-button",
        "badge"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "Reaches the package link and the copy button"
        },
        {
          "keys": "Screen readers",
          "action": "The trend chart is an image with a text alternative; stats are a description list"
        }
      ],
      "customization": [
        "variant",
        "manager",
        "hideInstall",
        "trend",
        "href"
      ]
    },
    "examples": [
      "package-badge-demo",
      "package-badge-inline"
    ],
    "url": "https://ui.ballmac.com/components/package-badge"
  },
  "categories": [
    "developer"
  ],
  "type": "registry:ui"
}