{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bullet-chart",
  "title": "Bullet Chart",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "Stephen Few's bullet chart: a bar for the value, a mark for the target, three shades behind for poor, fair and good, and a sentence saying whether the goal was met. Many measures in a small space, server-rendered.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/bullet-chart.tsx",
      "content": "// Ballmac UI: Bullet Chart. https://ui.ballmac.com/components/bullet-chart\n\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Minus, TriangleAlert } from \"lucide-react\"\n\nimport { useMessages } from \"@/lib/ballmac/i18n\"\nimport { cn } from \"@/lib/utils\"\n\ntype BulletItem = {\n  id: string\n  label: string\n  /** Where it is now. */\n  value: number\n  /** The goal. Drawn as a vertical mark. */\n  target?: number\n  /** The end of the scale. Defaults to a little past the largest of value, target and the ranges. */\n  max?: number\n  /** Where \"poor\" ends and where \"fair\" ends; beyond that is \"good\". Drawn as three shades behind the bar. */\n  ranges?: [number, number]\n  /** A line of context under the label. */\n  note?: string\n  /** When lower is better (response time, cost), the target is a ceiling. */\n  lowerIsBetter?: boolean\n}\n\ntype BulletChartProps = React.ComponentProps<\"ul\"> & {\n  items: BulletItem[]\n  /** Turns a value into text. */\n  format?: (value: number) => string\n}\n\n/** How an item stands against its target, in words. */\nfunction standing(i: BulletItem): \"met\" | \"close\" | \"behind\" | \"none\" {\n  if (i.target === undefined) return \"none\"\n  const ratio = i.lowerIsBetter ? i.target / Math.max(i.value, 1e-9) : i.value / Math.max(i.target, 1e-9)\n  return ratio >= 1 ? \"met\" : ratio >= 0.85 ? \"close\" : \"behind\"\n}\n\nconst WORDS = { met: \"Target met\", close: \"Close to target\", behind: \"Behind target\", none: \"\" } as const\n\n/**\n * A bullet chart (Stephen Few's replacement for gauges): a bar for the value, a mark for the target and three shades behind for poor, fair and good. Many\n * measures fit in a small space, and every one says in words whether it met its goal. Runs on the server.\n */\nfunction BulletChart({ items, format = (v) => v.toLocaleString(\"en-US\"), className, ...props }: BulletChartProps) {\n  const msg = useMessages()\n  return (\n    <ul data-slot=\"bullet-chart\" className={cn(\"grid w-full gap-5\", className)} {...props}>\n      {items.map((i) => {\n        const top = (i.max ?? Math.max(i.value, i.target ?? 0, i.ranges?.[1] ?? 0) * 1.15) || 1\n        const pct = (v: number) => `${Math.min(100, Math.max(0, (v / top) * 100))}%`\n        const st = standing(i)\n        const StIcon = st === \"met\" ? Check : st === \"none\" ? Minus : TriangleAlert\n        return (\n          <li key={i.id} data-standing={st} className=\"grid gap-1.5\">\n            <div className=\"flex flex-wrap items-baseline justify-between gap-x-4 gap-y-0.5\">\n              <p className=\"text-sm font-medium text-foreground\">{i.label}</p>\n              <p className=\"flex items-center gap-2 text-sm tabular-nums\">\n                <span className=\"font-semibold text-foreground\">{format(i.value)}</span>\n                {i.target !== undefined && <span className=\"text-muted-foreground\">{msg(\"bullet-chart.of\", \"of {target}\", { target: format(i.target) })}</span>}\n              </p>\n            </div>\n            <div\n              role=\"meter\"\n              aria-label={i.label}\n              aria-valuemin={0}\n              aria-valuemax={top}\n              aria-valuenow={Math.min(i.value, top)}\n              aria-valuetext={`${format(i.value)}${i.target !== undefined ? ` against a target of ${format(i.target)}. ${WORDS[st]}` : \"\"}`}\n              className=\"relative h-6 overflow-hidden rounded-md bg-muted\"\n            >\n              {i.ranges && (\n                <>\n                  <span aria-hidden=\"true\" className=\"absolute inset-y-0 start-0 bg-foreground/[0.07]\" style={{ width: pct(i.ranges[0]) }} />\n                  <span aria-hidden=\"true\" className=\"absolute inset-y-0 bg-foreground/[0.12]\" style={{ insetInlineStart: pct(i.ranges[0]), width: `calc(${pct(i.ranges[1])} - ${pct(i.ranges[0])})` }} />\n                  <span aria-hidden=\"true\" className=\"absolute inset-y-0 bg-foreground/[0.19]\" style={{ insetInlineStart: pct(i.ranges[1]), insetInlineEnd: 0 }} />\n                </>\n              )}\n              <span aria-hidden=\"true\" className=\"absolute inset-y-[7px] start-0 rounded-e-[3px] bg-foreground\" style={{ width: pct(i.value) }} />\n              {i.target !== undefined && <span aria-hidden=\"true\" className=\"absolute inset-y-[3px] w-[3px] -translate-x-1/2 rounded-full bg-chart-3 shadow-[0_0_0_1.5px_var(--card)] rtl:translate-x-1/2\" style={{ insetInlineStart: pct(i.target) }} />}\n            </div>\n            <div className=\"flex flex-wrap items-center justify-between gap-x-4 text-xs text-muted-foreground\">\n              <span>{i.note}</span>\n              {st !== \"none\" && (\n                <span className={cn(\"flex items-center gap-1.5\", st !== \"met\" && \"font-medium text-foreground\")}>\n                  <StIcon aria-hidden=\"true\" className=\"size-3.5\" />\n                  {WORDS[st]}\n                </span>\n              )}\n            </div>\n          </li>\n        )\n      })}\n    </ul>\n  )\n}\n\nexport { BulletChart, standing, type BulletChartProps, type BulletItem }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/bullet-chart.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "bullet chart",
      "kpi",
      "target",
      "progress",
      "gauge",
      "chart",
      "goals",
      "okr"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "items is [{ id, label, value, target?, max?, ranges?: [poorEnd, fairEnd], note?, lowerIsBetter? }]. format(value). Each item reads as a meter and says Target met, Close to target (within 85%) or Behind target. No client code: it renders on the server.",
      "whenToUse": [
        "Comparing several measures to their goals in a small space",
        "KPIs, SLAs and budgets where the target matters more than the trend"
      ],
      "whenNotToUse": [
        "A single value with no goal; use progress",
        "A trend; use sparkline or chart-line",
        "Proportions of a whole; use treemap or chart-pie"
      ],
      "composesWith": [
        "stat-card",
        "kpi-row",
        "slo-1",
        "chart-frame"
      ],
      "a11y": [
        {
          "keys": "Screen readers",
          "action": "Each row is a meter reading '84 against a target of 90. Close to target'"
        },
        {
          "keys": "Words",
          "action": "The standing is written ('Behind target') with an icon; the shades and the mark are decoration"
        },
        {
          "keys": "Right-to-left",
          "action": "Bars and marks start from the right edge"
        }
      ],
      "customization": [
        "ranges for the three shades",
        "target and max",
        "lowerIsBetter for times and costs",
        "format"
      ]
    },
    "statesNone": "A presentational chart: you pass the numbers in, so it has nothing of its own to load or fail.",
    "examples": [
      "bullet-chart-demo",
      "bullet-chart-lower"
    ],
    "url": "https://ui.ballmac.com/components/bullet-chart"
  },
  "categories": [
    "data-display"
  ],
  "type": "registry:ui"
}