{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "progress-bar",
  "type": "registry:ui",
  "title": "ProgressBar",
  "description": "The accent progress primitive: determinate fill, busy sweep, open-ended indicator, or segment accumulation against a tally.",
  "categories": [
    "display"
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "https://whiskeyjack.net/r/use-reduced-motion.json",
    "https://whiskeyjack.net/r/utils.json"
  ],
  "files": [
    {
      "path": "components/ui/progress-bar.tsx",
      "type": "registry:ui",
      "target": "components/ui/progress-bar.tsx",
      "content": "import * as React from 'react'\nimport { cva, type VariantProps } from 'class-variance-authority'\nimport { cn } from '@/lib/utils'\nimport { useReducedMotion } from '@/hooks/use-reduced-motion'\n\nconst progressBarVariants = cva('relative block w-full overflow-hidden rounded-full', {\n  variants: {\n    size: {\n      xs: 'h-[3px]',\n      sm: 'h-1',\n      md: 'h-2',\n      lg: 'h-3',\n    },\n  },\n  defaultVariants: { size: 'md' },\n})\n\n// Dot diameter per size -- the segment geometry (gaps, pill width, the faint\n// filler pattern) derives from it. Keep in sync with the height classes above.\nconst DOT_PX = { xs: 3, sm: 4, md: 8, lg: 12 } as const\n\n// Segment accumulation: every SEGMENT_SPAN filled dots collapse into one solid\n// pill of PILL_DOT_FACTOR dot-widths, so an open-ended tally stays compact.\nconst SEGMENT_SPAN = 10\nconst PILL_DOT_FACTOR = 3\n\nexport interface ProgressBarProps extends VariantProps<typeof progressBarVariants> {\n  /** Progress as a fraction from 0 to 1 (clamped). Ignored when `indeterminate`. */\n  value?: number\n  /**\n   * Open-ended / no-target progress. With `count` it renders segment\n   * accumulation (see `count`); without one it renders the plain dotted accent\n   * line. The static counterpart to a native <progress> with no value -- it\n   * means \"unbounded\", not \"loading\", so it does not animate. Used for\n   * Chip Away's free-streak / count goals.\n   */\n  indeterminate?: boolean\n  /**\n   * The tally behind an `indeterminate` bar (check-ins, streak days, ...).\n   * Renders segment accumulation: each unit fills one solid accent dot on a row\n   * of faint track-colored dots, and every 10 filled dots collapse into a solid\n   * pill a few dot-widths wide -- so the tally can grow past the track width.\n   * 0 shows the all-faint default state. Ignored without `indeterminate`.\n   */\n  count?: number\n  /**\n   * Work is in flight and its next step cannot be counted yet.\n   *\n   * Distinct from `indeterminate`, which means *unbounded* and renders a static\n   * dotted line: `busy` is *loading*, and lays a sweeping accent band over the\n   * track. Combine it with a `value` for a run whose stages are only partly\n   * countable -- the bar holds its fill and keeps a sign of life through the\n   * stage that cannot report a count. Reduced motion holds the band still, so\n   * the surrounding text has to carry the meaning either way.\n   */\n  busy?: boolean\n  /**\n   * Accessible label. With a value the bar exposes role=\"progressbar\" +\n   * aria-valuenow (indeterminate omits aria-valuenow, per ARIA). Omit, or pass\n   * `decorative`, when adjacent text already conveys the value (e.g. the TabBar\n   * underline beneath a \"Day 6 of 10\" line).\n   */\n  label?: string\n  /** Render purely decorative (aria-hidden, no role). */\n  decorative?: boolean\n  className?: string\n  /** Inline styles for the root element. */\n  style?: React.CSSProperties\n}\n\nconst trackColor =\n  'var(--wj-progress-track, color-mix(in srgb, var(--color-accent-500) 35%, transparent))'\n\n/**\n * Progress bar / indicator. Rendered with <span>s (display fixed via classes)\n * so it can sit inside a <button> (e.g. a sidebar tab item). Uses, mirroring a\n * tab underline's states: a 0..1 `value` is a determinate fill (accent fill\n * over a low-opacity accent track); `indeterminate` + `count` is segment\n * accumulation (open-ended tally); `indeterminate` alone is a dotted accent\n * line; neither is just an empty track.\n */\nexport const ProgressBar = React.forwardRef<HTMLSpanElement, ProgressBarProps>(function ProgressBar({\n  value = 0,\n  indeterminate,\n  count,\n  busy,\n  size,\n  label,\n  decorative,\n  className,\n  style,\n}, ref) {\n  const reduced = useReducedMotion()\n  const pct = Math.round(Math.max(0, Math.min(1, value)) * 100)\n\n  const a11y = decorative\n    ? ({ 'aria-hidden': true } as const)\n    : {\n        role: 'progressbar' as const,\n        'aria-valuemin': 0,\n        'aria-valuemax': 100,\n        // Indeterminate progress omits aria-valuenow.\n        ...(indeterminate ? {} : { 'aria-valuenow': pct }),\n        ...(label ? { 'aria-label': label } : {}),\n        // A bar that is working but cannot say how far along it is says so,\n        // rather than reporting a number that will not move.\n        ...(busy ? { 'aria-busy': true } : {}),\n      }\n\n  if (indeterminate && count != null) {\n    const dot = DOT_PX[size ?? 'md']\n    const tally = Math.max(0, Math.floor(count))\n    const pills = Math.floor(tally / SEGMENT_SPAN)\n    const dots = tally % SEGMENT_SPAN\n    return (\n      <span ref={ref} className={cn(progressBarVariants({ size }), 'bg-transparent', className)} style={style} {...a11y}>\n        {/* Gap equals the dot size (the dotted line's rhythm: dot-sized gaps). */}\n        <span className=\"flex h-full w-full items-center\" style={{ gap: `${dot}px` }}>\n          {Array.from({ length: pills }, (_, i) => (\n            // Pills may shrink (down to a dot) when a large tally outgrows the\n            // width -- the newest pills and the current batch stay visible.\n            <span\n              key={`pill-${i}`}\n              className=\"block h-full rounded-full bg-[var(--color-accent-500)]\"\n              style={{ width: dot * PILL_DOT_FACTOR, minWidth: dot }}\n            />\n          ))}\n          {Array.from({ length: dots }, (_, i) => (\n            <span\n              key={`dot-${i}`}\n              className=\"block h-full shrink-0 rounded-full bg-[var(--color-accent-500)]\"\n              style={{ width: dot }}\n            />\n          ))}\n          {/* Faint dots fill whatever width remains -- a repeating pattern in\n              the track color, phase-aligned with the flex gap so the rhythm is\n              unbroken. The right edge ends wherever the pattern lands. */}\n          <span\n            className=\"block h-full min-w-0 flex-1\"\n            style={{\n              backgroundImage: `radial-gradient(circle at ${dot / 2}px center, ${trackColor} ${dot / 2}px, transparent ${dot / 2}px)`,\n              backgroundSize: `${dot * 2}px 100%`,\n              backgroundPosition: 'left center',\n              backgroundRepeat: 'repeat-x',\n            }}\n          />\n        </span>\n      </span>\n    )\n  }\n\n  if (indeterminate) {\n    return (\n      <span ref={ref} className={cn(progressBarVariants({ size }), 'bg-transparent', className)} style={style} {...a11y}>\n        <span\n          className=\"block h-full w-full\"\n          // Round dots repeating from the left, evenly spaced with the gap equal to\n          // the dot size (3px dot, 3px gap; 6px period). The right edge ends wherever\n          // the pattern lands. Tunable via the 1.5px radius + the 6px period.\n          style={{\n            backgroundImage:\n              'radial-gradient(circle at 1.5px center, var(--color-accent-500) 1.5px, transparent 1.5px)',\n            backgroundSize: '6px 100%',\n            backgroundPosition: 'left center',\n            backgroundRepeat: 'repeat-x',\n          }}\n        />\n      </span>\n    )\n  }\n\n  return (\n    <span\n      ref={ref}\n      className={cn(progressBarVariants({ size }), className)}\n      // Track: accent at low opacity. Override --wj-progress-track to tune / theme.\n      // Caller style merges last so it can override the track color.\n      style={{ backgroundColor: trackColor, ...style }}\n      {...a11y}\n    >\n      <span\n        className={cn(\n          'block h-full rounded-full bg-[var(--color-accent-500)]',\n          !reduced && 'transition-[width] duration-300 ease-out'\n        )}\n        style={{ width: `${pct}%` }}\n      />\n      {/* The busy sweep rides the whole track, over the fill, so a bar that is\n          holding at 15% through an uncountable stage still reads as working.\n          The utility carries its own reduced-motion backstop. */}\n      {busy && (\n        <span aria-hidden className=\"wj-busy-sweep absolute inset-0 rounded-full\" />\n      )}\n    </span>\n  )\n})\n\nProgressBar.displayName = 'ProgressBar'\n"
    }
  ],
  "docs": "Pass `value` for a determinate fill, `busy` for work in flight, `indeterminate` for open-ended progress, or `count` for segment accumulation. `busy` and `indeterminate` are different claims: `indeterminate` means UNBOUNDED and draws a static dotted line, while `busy` means LOADING and sweeps an accent band over the track – pair `busy` with a `value` for a run whose stages are only partly countable, so the bar holds its fill through the stage that cannot report a count. Use `label` to expose role=\"progressbar\", or `decorative` when adjacent text already conveys the value. It renders as spans, so it is valid inside a button, which is how the TabBar underline works.",
  "meta": {
    "group": "display",
    "related": [
      "tab-bar",
      "calendar-heatmap"
    ],
    "exports": [
      "ProgressBar"
    ],
    "siteSlug": "progress"
  }
}
