{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bar-race",
  "type": "registry:ui",
  "title": "Bar Race",
  "description": "Horizontal bars that grow, shrink and overtake each other as the data changes.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/bar-race.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, animate, motion, useReducedMotion } from \"motion/react\";\n\nexport interface BarRaceDatum {\n  /** Stable identity — bars are tracked by id as they overtake each other. */\n  id: string;\n  label: string;\n  value: number;\n  /** Bar colour; falls back to the accent / neutral scheme. */\n  color?: string;\n}\n\nexport interface BarRaceProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  data: BarRaceDatum[];\n  /** Accessible name; also the caption of the screen-reader table. */\n  label: string;\n  /** Number of bars shown (the table still lists every row). */\n  maxBars?: number;\n  /** Duration of the reorder, width and number tweens, in ms. */\n  duration?: number;\n  /** Default number formatting when `format` is not given. */\n  notation?: \"standard\" | \"compact\";\n  /** Custom value formatter. */\n  format?: (value: number) => string;\n  /** Leader in the accent colour, others neutral (bars without their own colour). */\n  highlightLeader?: boolean;\n  /** Show the rank number before each label. */\n  showRank?: boolean;\n  /** Row height. */\n  size?: \"sm\" | \"md\" | \"lg\";\n  accent?: string;\n  /** Announce leader changes through a polite live region. */\n  announce?: boolean;\n}\n\n// Label columns narrow inside small containers (container queries on the root).\nconst ROW: Record<NonNullable<BarRaceProps[\"size\"]>, { row: string; text: string; cols: string }> = {\n  sm: {\n    row: \"h-6\",\n    text: \"text-xs\",\n    cols: \"grid-cols-[auto_minmax(0,4.5rem)_minmax(0,1fr)] @sm:grid-cols-[auto_minmax(0,6rem)_minmax(0,1fr)]\",\n  },\n  md: {\n    row: \"h-8\",\n    text: \"text-sm\",\n    cols: \"grid-cols-[auto_minmax(0,5rem)_minmax(0,1fr)] @md:grid-cols-[auto_minmax(0,7.5rem)_minmax(0,1fr)]\",\n  },\n  lg: {\n    row: \"h-11\",\n    text: \"text-base\",\n    cols: \"grid-cols-[auto_minmax(0,5.5rem)_minmax(0,1fr)] @md:grid-cols-[auto_minmax(0,8.5rem)_minmax(0,1fr)]\",\n  },\n};\n\nconst EASE = [0.22, 1, 0.36, 1] as const;\n\n/** The value next to a bar: tweens from the number on screen to the new one (never from zero). */\nfunction RaceValue({ value, format, duration }: { value: number; format: (v: number) => string; duration: number }) {\n  const nodeRef = React.useRef<HTMLSpanElement>(null);\n  const shownRef = React.useRef(value);\n  const [initialValue] = React.useState(value);\n\n  React.useEffect(() => {\n    const node = nodeRef.current;\n    if (!node) return;\n    const from = shownRef.current;\n    if (duration <= 0 || from === value) {\n      shownRef.current = value;\n      node.textContent = format(value);\n      return;\n    }\n    const controls = animate(from, value, {\n      duration: duration / 1000,\n      ease: EASE,\n      onUpdate: (v) => {\n        shownRef.current = v;\n        node.textContent = format(v);\n      },\n    });\n    return () => controls.stop();\n  }, [value, duration, format]);\n\n  return (\n    <span ref={nodeRef} className=\"tabular-nums\">\n      {format(initialValue)}\n    </span>\n  );\n}\n\n/**\n * A horizontal bar chart where bars grow, shrink and overtake each other: rows reorder with a\n * shared layout animation, widths tween, and value labels count from their previous value.\n * A screen-reader table mirrors the data and leader changes are announced politely.\n */\nexport function BarRace({\n  data,\n  label,\n  maxBars = 8,\n  duration = 700,\n  notation = \"standard\",\n  format,\n  highlightLeader = true,\n  showRank = true,\n  size = \"md\",\n  accent = \"#FF7448\",\n  announce = true,\n  className = \"\",\n  style,\n  ...props\n}: BarRaceProps) {\n  const reduceMotion = useReducedMotion() ?? false;\n  const ms = reduceMotion ? 0 : duration;\n\n  const defaultFormat = React.useMemo(() => {\n    const nf = new Intl.NumberFormat(\"en-US\", {\n      notation,\n      maximumFractionDigits: notation === \"compact\" ? 1 : 0,\n    });\n    return (v: number) => nf.format(v);\n  }, [notation]);\n  const fmt = format ?? defaultFormat;\n\n  const sorted = [...data].sort((a, b) => b.value - a.value || a.label.localeCompare(b.label));\n  const visible = sorted.slice(0, Math.max(1, Math.round(maxBars)));\n  const top = Math.max(...visible.map((d) => d.value), 1e-9);\n  const leader = sorted[0];\n\n  // Announce a new leader (not on first render) — tracked while rendering, no effect round-trip.\n  const [lead, setLead] = React.useState({ id: leader?.id, message: \"\" });\n  if (leader && lead.id !== leader.id) {\n    setLead({ id: leader.id, message: `${leader.label} takes the lead with ${fmt(leader.value)}.` });\n  }\n\n  const sizes = ROW[size] ?? ROW.md;\n  const transition = { duration: ms / 1000, ease: EASE };\n\n  return (\n    <div\n      role=\"group\"\n      aria-label={label}\n      className={`@container w-full text-zinc-950 dark:text-zinc-50 ${className}`}\n      style={{ [\"--br-accent\" as string]: accent, ...style }}\n      {...props}\n    >\n      <ol aria-hidden=\"true\" className=\"relative flex flex-col gap-1.5\">\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          {visible.map((item, rank) => {\n            const pct = Math.max(0, (item.value / top) * 100);\n            const own = item.color;\n            const leaderTone = highlightLeader ? rank === 0 : true;\n            return (\n              <motion.li\n                key={item.id}\n                layout=\"position\"\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                exit={{ opacity: 0 }}\n                transition={{ layout: transition, opacity: { duration: Math.min(0.3, ms / 1000) } }}\n                className={`grid items-center gap-2 @md:gap-3 ${sizes.cols} ${sizes.row} ${sizes.text}`}\n              >\n                <span\n                  className={`w-5 font-mono text-[10px] tabular-nums text-zinc-400 dark:text-zinc-500 ${showRank ? \"\" : \"hidden\"}`}\n                >\n                  {String(rank + 1).padStart(2, \"0\")}\n                </span>\n                <span className={`truncate tracking-[-0.01em] ${showRank ? \"\" : \"col-span-2\"}`}>{item.label}</span>\n                <span className=\"relative h-[72%] pr-12 @md:pr-16\">\n                  <motion.span\n                    className={`relative block h-full min-w-[2px] rounded-[3px] transition-colors duration-500 ${\n                      own ? \"\" : leaderTone ? \"bg-[var(--br-accent)]\" : \"bg-zinc-300 dark:bg-zinc-700\"\n                    }`}\n                    style={own ? { backgroundColor: own } : undefined}\n                    initial={false}\n                    animate={{ width: `${pct}%` }}\n                    transition={transition}\n                  >\n                    <span className=\"absolute top-1/2 left-full -translate-y-1/2 pl-2 font-mono text-[11px] whitespace-nowrap text-zinc-600 dark:text-zinc-300\">\n                      <RaceValue value={item.value} format={fmt} duration={ms} />\n                    </span>\n                  </motion.span>\n                </span>\n              </motion.li>\n            );\n          })}\n        </AnimatePresence>\n      </ol>\n\n      <table className=\"sr-only\">\n        <caption>{label}</caption>\n        <thead>\n          <tr>\n            <th scope=\"col\">Rank</th>\n            <th scope=\"col\">Name</th>\n            <th scope=\"col\">Value</th>\n          </tr>\n        </thead>\n        <tbody>\n          {sorted.map((item, rank) => (\n            <tr key={item.id}>\n              <td>{rank + 1}</td>\n              <th scope=\"row\">{item.label}</th>\n              <td>{fmt(item.value)}</td>\n            </tr>\n          ))}\n        </tbody>\n      </table>\n\n      {announce ? (\n        <p className=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n          {lead.message}\n        </p>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "data"
  ],
  "docs": "Documentation: https://wancraft.my/components/bar-race"
}
