{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dot-bars",
  "title": "Dot Bars",
  "description": "Dot-matrix column series: one column per sample, with dots stacked to the value.",
  "dependencies": [
    "class-variance-authority@^0.7.1"
  ],
  "registryDependencies": [
    "@pestoui/style",
    "@pestoui/utils"
  ],
  "files": [
    {
      "path": "pestoui/components/dot-bars.tsx",
      "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Dot-matrix column series: one column per sample, each a bottom-aligned stack\n   of round dots whose count is the value. A compact trend that sits beside a\n   figure, in the same family as Sparkline, TickStrip and HeatGrid — none of\n   which can draw it. HeatGrid encodes value as opacity and paints its zero cell\n   muted, so the space above a column would fill with grey squares; Sparkline's\n   bars stretch under preserveAspectRatio=\"none\" and would turn dots into\n   ellipses.\n\n   The unlit space above a column is never painted. A column holds only its lit\n   dots and the root aligns columns to the bottom, so whatever surface the strip\n   sits on shows through above each one.\n\n   The tone lands on the root as a text colour and every dot paints from\n   currentColor. Nothing here is text, so that colour carries no contrast\n   requirement of its own; it only names the fill.\n\n   No client directive: it renders from props alone, with no hooks and no\n   function-valued props. */\nconst dotBarsVariants = cva(\n  \"inline-flex max-w-full items-end justify-end gap-(--dot-gap) overflow-hidden\",\n  {\n    variants: {\n      tone: {\n        selected: \"text-selected\",\n        success: \"text-success\",\n        warning: \"text-warning\",\n        destructive: \"text-destructive\",\n        info: \"text-info\",\n        muted: \"text-muted-foreground\",\n        \"chart-1\": \"text-chart-1\",\n        \"chart-2\": \"text-chart-2\",\n        \"chart-3\": \"text-chart-3\",\n        \"chart-4\": \"text-chart-4\",\n        \"chart-5\": \"text-chart-5\",\n      },\n      /* Custom properties rather than per-dot classes, so the dot, the gap\n         between dots and the gap between columns are one number each and the\n         root's height can be derived from them below. */\n      size: {\n        sm: \"[--dot-gap:--spacing(0.5)] [--dot:--spacing(1)]\",\n        default: \"[--dot-gap:--spacing(0.75)] [--dot:--spacing(1.5)]\",\n      },\n    },\n    defaultVariants: {\n      tone: \"selected\",\n      size: \"default\",\n    },\n  },\n)\n\ntype DotBarsTone = NonNullable<VariantProps<typeof dotBarsVariants>[\"tone\"]>\ntype DotBarsSize = NonNullable<VariantProps<typeof dotBarsVariants>[\"size\"]>\n\nconst DEFAULT_ROWS = 6\n\n/* Locale pinned so the server and the client speak the same summary — an\n   unpinned toLocaleString is a hydration mismatch waiting for a non-English\n   browser. */\nfunction formatReading(value: number) {\n  return value.toLocaleString(\"en-US\", { maximumFractionDigits: 2 })\n}\n\ntype DotBarsProps = Omit<React.ComponentProps<\"div\">, \"children\"> &\n  VariantProps<typeof dotBarsVariants> & {\n    /** The series, oldest first. Non-finite and negative entries read as 0. */\n    data: number[]\n    /** Dots in a full column. Default 6. */\n    rows?: number\n    /** Pins the top of the scale. Omit to normalise against the series' peak. */\n    max?: number\n    /** Names the series in the spoken summary — \"Daily runs\". */\n    srLabel?: string\n  }\n\nfunction DotBars({\n  data,\n  rows = DEFAULT_ROWS,\n  max,\n  srLabel,\n  tone = \"selected\",\n  size = \"default\",\n  className,\n  style,\n  \"aria-label\": ariaLabel,\n  ...props\n}: DotBarsProps) {\n  /* A fractional, zero or non-finite row count has no sensible column, so it\n     falls back to the default rather than drawing nothing. */\n  const rowCount = Number.isFinite(rows) && rows >= 1 ? Math.floor(rows) : DEFAULT_ROWS\n\n  /* One NaN would poison the peak and blank every column at once. */\n  const values = data.map((value) => (Number.isFinite(value) ? Math.max(0, value) : 0))\n\n  /* reduce from 0, not Math.max(...values): an empty series spreads to\n     Math.max() and returns -Infinity. */\n  const peak = values.reduce((highest, value) => Math.max(highest, value), 0)\n  const scale = max != null && Number.isFinite(max) && max > 0 ? max : peak\n\n  /* Ceilings, as in HeatGrid: any sample above zero lights at least one dot, so\n     a quiet day still registers. The clamp to rowCount is what stops an\n     explicit `max` below a sample from pushing the column past the top. An\n     all-zero series has a zero scale and lights nothing. */\n  const litOf = (value: number) => {\n    if (value <= 0 || scale <= 0) return 0\n    return Math.min(rowCount, Math.max(1, Math.ceil((value / scale) * rowCount)))\n  }\n\n  const latest = values.length > 0 ? values[values.length - 1] : 0\n  const summary =\n    values.length === 0\n      ? \"No data\"\n      : `latest ${formatReading(latest)}, peak ${formatReading(peak)}`\n\n  return (\n    <div\n      data-slot=\"dot-bars\"\n      data-tone={tone}\n      data-size={size}\n      role=\"img\"\n      aria-label={ariaLabel ?? (srLabel ? `${srLabel}: ${summary}` : summary)}\n      className={cn(dotBarsVariants({ tone, size }), className)}\n      /* The height of a full column, so an empty or short series still holds\n         its place and every column measures from the same floor. */\n      style={{\n        height: `calc(${rowCount} * var(--dot) + ${rowCount - 1} * var(--dot-gap))`,\n        ...style,\n      }}\n      {...props}\n    >\n      {values.map((value, index) => {\n        const lit = litOf(value)\n        return (\n          <span\n            key={index}\n            data-slot=\"dot-bars-column\"\n            data-lit={lit}\n            className=\"flex w-(--dot) shrink-0 flex-col gap-(--dot-gap)\"\n          >\n            {Array.from({ length: lit }, (_, dot) => (\n              <span\n                key={dot}\n                data-slot=\"dot-bars-dot\"\n                className=\"size-(--dot) shrink-0 rounded-full bg-current\"\n              />\n            ))}\n          </span>\n        )\n      })}\n    </div>\n  )\n}\n\nexport { DotBars, dotBarsVariants }\nexport type { DotBarsProps, DotBarsTone, DotBarsSize }\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}