{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"bar-chart","type":"registry:ui","title":"Bar chart","description":"Compare one measure across days and scrub any bar for its value.","categories":["data"],"dependencies":["motion"],"registryDependencies":["https://uiarc.dev/r/arc-motion-tokens.json"],"files":[{"path":"registry/components/bar-chart/bar-chart.tsx","type":"registry:component","target":"@components/arc/bar-chart/bar-chart.tsx","content":"\"use client\";\n\nimport { useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, type KeyboardEvent, type PointerEvent } from \"react\";\nimport { AnimatePresence, animate, cancelFrame, frame, motion, useInView, useMotionValue, usePresence, useReducedMotion, useTransform, type MotionValue, type Variants } from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport styles from \"./bar-chart.module.css\";\n\nexport interface BarChartDatum {\n /** Stable identity, such as an ISO date. A bar whose key survives a data change keeps its place, so switching ranges morphs instead of redrawing. */\n key: string;\n /** Full label for the headline and screen readers, such as \"Tue, Sep 15\". */\n label: string;\n /** Short label under the bar, such as \"T\" or \"Sep 14\". Leave it out to keep the axis quiet. */\n axisLabel?: string;\n value: number;\n}\n\n/** Use a bar chart for one measure across a run of days, weeks, or months, when people compare each period against the rest and against the average. */\nexport interface BarChartProps {\n data: BarChartDatum[];\n /** What is measured, such as \"Active minutes\". Names the chart for assistive technology. */\n label: string;\n /** The range on show, such as \"Sep 15–21, 2026\". It rests under the headline. */\n period: string;\n /** Unit after each value, such as \"min\". */\n unit?: string;\n /** Headline label at rest, above the average. */\n averageLabel?: string;\n /** Headline label while a bar is scrubbed. */\n valueLabel?: string;\n /** Header of the first column in the data table read by screen readers. */\n categoryLabel?: string;\n /** Draws the average as a reference line that springs to each new range. */\n showAverage?: boolean;\n /** Plot height in pixels. */\n height?: number;\n formatValue?: (value: number) => string;\n}\n\nconst { spring, duration, ease, blur, stagger } = motionTokens;\n/** Motion drops inherited velocity on time-defined springs, so values that retarget mid-flight run the same springs written as stiffness and damping. */\nconst physical = ({ visualDuration, bounce }: { visualDuration: number; bounce: number }, restDelta = .01) => { const root = (2 * Math.PI) / (visualDuration * 1.2); return { type: \"spring\" as const, stiffness: root * root, damping: 2 * (1 - bounce) * root, restDelta, restSpeed: restDelta * 2 }; };\nconst settle = physical(spring.smooth);\nconst grow = physical(spring.morph);\nconst glide = physical(spring.snappy);\n/** The zoom runs in fractions of the plot, so it rests far below a pixel. */\nconst zoom = physical(spring.smooth, .00001);\nconst collapse = physical({ visualDuration: duration.standard, bounce: 0 });\nconst fadeFast = { duration: duration.instant, ease: [...ease.standard] } as const;\n/** Starts animations on the next frame, so the render that caused them never swallows a spring's first frames. Returns a cleanup. */\nfunction soon(start: () => { stop: () => void }) {\n let controls: { stop: () => void } | null = null;\n const run = () => { controls = start(); };\n frame.update(run);\n return () => { cancelFrame(run); controls?.stop(); };\n}\n\n/** Room above the top gridline, the share of each slot a bar fills, its widest size, and the radius of its data end. */\nconst TOP = 12, FILL = .58, MAX_BAR = 28, RADIUS = 4;\nconst grouped = new Intl.NumberFormat(\"en-US\", { maximumFractionDigits: 0 });\nconst clamp = (value: number, low: number, high: number) => Math.min(high, Math.max(low, value));\n\n/** Clean gridlines: the smallest step of 1, 2, 3, or 5 that covers the data in four rows or fewer, so shared steps survive a range change. */\nfunction scaleFor(max: number) {\n const safe = Math.max(max, 1);\n const magnitude = 10 ** Math.floor(Math.log10(safe / 4));\n for (const factor of [1, 2, 3, 5, 10]) {\n const step = factor * magnitude, rows = Math.ceil(safe / step);\n if (rows <= 4) return { top: rows * step, ticks: Array.from({ length: rows + 1 }, (_, row) => row * step) };\n }\n return { top: safe, ticks: [0, safe] };\n}\n\n/** A column that grows from a square baseline to a softly rounded data end. */\nfunction barPath(x: number, w: number, h: number, base: number) {\n if (w <= 0 || h <= .01) return `M${x.toFixed(2)} ${base}H${(x + Math.max(w, 0)).toFixed(2)}Z`;\n const r = Math.min(RADIUS, w / 2, h), top = base - h, f = (value: number) => value.toFixed(2);\n return `M${f(x)} ${base}V${f(top + r)}A${f(r)} ${f(r)} 0 0 1 ${f(x + r)} ${f(top)}H${f(x + w - r)}A${f(r)} ${f(r)} 0 0 1 ${f(x + w)} ${f(top + r)}V${base}Z`;\n}\n\n/** Slots count back from the latest bar, so the newest day stays pinned right while a longer range zooms out to the left. */\nfunction usePlace(place: number, reduced: boolean) {\n const value = useMotionValue(place);\n useEffect(() => {\n if (value.get() === place) return;\n if (reduced) { value.jump(place); return; }\n return soon(() => animate(value, place, settle));\n }, [place, reduced, value]);\n return value;\n}\n\ntype Frame = { width: MotionValue; unit: MotionValue };\nconst centerOf = ({ width, unit }: Frame, place: number) => width.get() - (place + .5) * width.get() * unit.get();\n\nfunction Bar({ id, place, value, delay, shown, active, frame, scale, height, reduced }: { id: string; place: number; value: number; delay: number; shown: boolean; active: boolean; frame: Frame; scale: MotionValue; height: number; reduced: boolean }) {\n const [isPresent, safeToRemove] = usePresence();\n const amount = useMotionValue(0);\n const slot = usePlace(place, reduced);\n const [firstDelay] = useState(delay);\n const grown = useRef(false);\n const remove = useRef(safeToRemove);\n useEffect(() => { remove.current = safeToRemove; }, [safeToRemove]);\n\n // A bar grows once it is seen, each a beat after its neighbour; later values settle without a delay.\n useEffect(() => {\n if (!isPresent) return;\n if (reduced) { amount.jump(value); grown.current = true; return; }\n if (!shown) return;\n const first = !grown.current;\n grown.current = true;\n return soon(() => animate(amount, value, first ? { ...grow, delay: firstDelay } : settle));\n }, [amount, firstDelay, isPresent, reduced, shown, value]);\n\n // A leaving bar sinks into the baseline, faster than a bar grows, while the zoom carries it out of the plot.\n useEffect(() => {\n if (isPresent) return;\n if (reduced) { remove.current?.(); return; }\n return soon(() => animate(amount, 0, { ...collapse, onComplete: () => remove.current?.() }));\n }, [amount, isPresent, reduced]);\n\n const d = useTransform(() => {\n const w = frame.width.get() * frame.unit.get(), size = Math.min(w * FILL, MAX_BAR), center = centerOf(frame, slot.get());\n return barPath(center - size / 2, size, (amount.get() / scale.get()) * (height - TOP), height);\n });\n return ;\n}\n\n/** A value on the axis: its gridline and label ride the scale, so a taller range slides them down while new rows fade in above. */\nfunction useRowY(value: number, scale: MotionValue, height: number) {\n return useTransform(() => Math.round(height - (value / scale.get()) * (height - TOP)) + .5);\n}\n\nfunction Gridline({ value, scale, height }: { value: number; scale: MotionValue; height: number }) {\n const y = useRowY(value, scale, height);\n if (value === 0) return null;\n return ;\n}\n\nfunction TickLabel({ value, scale, height, avoid, format }: { value: number; scale: MotionValue; height: number; avoid: MotionValue | null; format: (value: number) => string }) {\n const y = useRowY(value, scale, height);\n // A tick label makes way where the average label sits beside it.\n const opacity = useTransform(() => avoid ? clamp((Math.abs(y.get() - avoid.get()) - 10) / 6, 0, 1) : 1);\n return \n {format(value)}\n ;\n}\n\nfunction AxisLabel({ text, size, shift, place, frame, reduced }: { text: string; size: number; shift: number; place: number; frame: Frame; reduced: boolean }) {\n const slot = usePlace(place, reduced);\n // Centred under its bar and carried by the zoom. A label at either end keeps the nudge that holds it inside the settled plot.\n const x = useTransform(() => centerOf(frame, slot.get()) - size / 2 + shift);\n return {text};\n}\n\n/** Axis labels that fit once the range has settled: every label, else every second, third, and so on, counted back from the latest so the newest date always shows and spacing stays even. */\nfunction fittingLabels(data: BarChartDatum[], width: number, sizes: Record) {\n const fitting = new Map();\n if (!width) return fitting;\n const count = Math.max(1, data.length);\n const labels = data.flatMap((item, index) => {\n const size = item.axisLabel ? sizes[item.axisLabel] : undefined;\n if (size === undefined) return [];\n const natural = width - (data.length - 1 - index + .5) * (width / count) - size / 2;\n return [{ key: item.key, size, natural, left: clamp(natural, 0, Math.max(0, width - size)) }];\n }).reverse();\n for (let stride = 1; stride <= labels.length; stride++) {\n const picked = labels.filter((_, rank) => rank % stride === 0);\n if (stride === labels.length || picked.every((label, rank) => rank === 0 || label.left + label.size + 10 <= picked[rank - 1]!.left)) {\n picked.forEach(label => fitting.set(label.key, label.left - label.natural));\n break;\n }\n }\n return fitting;\n}\n\n/** Changed text rises from a soft blur in the direction it moved, and leaves a little faster than it arrives. */\nconst rise: Variants = {\n hidden: (direction: number) => ({ opacity: 0, y: `${.3 * direction}em`, filter: `blur(${blur.soft}px)` }),\n shown: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: { duration: duration.standard, ease: [...ease.enter] } },\n gone: (direction: number) => ({ opacity: 0, y: `${-.3 * direction}em`, filter: `blur(${blur.subtle}px)`, transition: { duration: duration.instant, ease: [...ease.standard] } }),\n};\nconst fade: Variants = { hidden: { opacity: 0, y: 0, filter: \"blur(0px)\" }, shown: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: { duration: duration.fast } }, gone: { opacity: 0, y: 0, filter: \"blur(0px)\", transition: { duration: duration.instant } } };\n\nfunction Swap({ text, direction, reduced }: { text: string; direction: number; reduced: boolean }) {\n return \n {text}\n ;\n}\n\n/** A number rolls digit by digit, keyed by place from the right, so unchanged digits hold still. Its width springs to fit, so the unit beside it glides instead of jumping. */\nfunction Roll({ text, direction, reduced }: { text: string; direction: number; reduced: boolean }) {\n const sizer = useRef(null);\n const width = useMotionValue(\"auto\");\n useLayoutEffect(() => {\n const node = sizer.current;\n if (!node || typeof ResizeObserver === \"undefined\") return;\n let measured: string | null = null;\n // Only a new value springs; a late web font or a resize jumps straight to fit.\n const observer = new ResizeObserver(() => {\n const next = node.offsetWidth;\n if (measured !== null && measured !== node.textContent && !reduced) animate(width, next, grow);\n else width.jump(next);\n measured = node.textContent;\n });\n observer.observe(node);\n return () => observer.disconnect();\n }, [reduced, width]);\n const chars = [...text];\n const variants = reduced ? fade : rise;\n return \n {text}\n \n {chars.map((char, index) => \n \n {char}\n \n )}\n \n ;\n}\n\nconst subscribeNothing = () => () => {};\n/** Reduced motion is only known in the browser, so the first client render matches the server before it takes effect. */\nfunction useReducedMotionSafe() {\n const hydrated = useSyncExternalStore(subscribeNothing, () => true, () => false);\n return !!useReducedMotion() && hydrated;\n}\n\nexport function BarChart({ data, label, period, unit = \"\", averageLabel = \"Daily average\", valueLabel = \"Total\", categoryLabel = \"Day\", showAverage = true, height = 176, formatValue = value => grouped.format(value) }: BarChartProps) {\n const reduced = useReducedMotionSafe();\n const figure = useRef(null);\n const plot = useRef(null);\n const inView = useInView(figure, { once: true, amount: .35 });\n const titleId = useId();\n const count = Math.max(data.length, 1), last = data.length - 1;\n const average = data.length ? data.reduce((sum, item) => sum + item.value, 0) / data.length : 0;\n const peak = data.reduce((max, item) => Math.max(max, item.value), 0);\n const { top, ticks } = useMemo(() => scaleFor(peak), [peak]);\n const signature = data.map(item => item.key).join(\"|\");\n const suffix = unit ? ` ${unit}` : \"\";\n\n // Bars that join an existing chart grow outward from the ones that stayed; the first reveal runs left to right.\n const [known, setKnown] = useState(() => ({ signature, keys: new Set(data.map(item => item.key)), fresh: new Map(), changes: 0 }));\n const [active, setActive] = useState(null);\n if (known.signature !== signature) {\n const incoming = data.map((item, index) => ({ key: item.key, place: last - index })).filter(item => !known.keys.has(item.key)).sort((a, b) => a.place - b.place);\n const step = Math.min(stagger.item, .32 / Math.max(1, incoming.length));\n setKnown({ signature, keys: new Set(data.map(item => item.key)), fresh: new Map(incoming.map((item, rank) => [item.key, rank * step])), changes: known.changes + 1 });\n setActive(null);\n }\n const revealStep = Math.min(stagger.item, .36 / count);\n const index = active === null ? null : Math.min(active, last);\n const scrubbing = index !== null && last >= 0;\n const shownValue = scrubbing ? data[index]!.value : average;\n // Headline copy moves the way the data did: a larger value rises from below, a later day arrives from below too.\n const [trend, setTrend] = useState({ value: shownValue, index, valueWay: 1, indexWay: 1 });\n if (trend.value !== shownValue || trend.index !== index) setTrend({ value: shownValue, index, valueWay: shownValue === trend.value ? trend.valueWay : shownValue > trend.value ? 1 : -1, indexWay: index === null || trend.index === null ? 1 : Math.sign(index - trend.index) || 1 });\n\n const width = useMotionValue(0);\n const unitShare = useMotionValue(1 / count);\n const scale = useMotionValue(top);\n const mean = useMotionValue(0);\n const cursor = useMotionValue(0);\n const frame = useMemo(() => ({ width, unit: unitShare }), [width, unitShare]);\n\n // The chart draws in its own pixels; the width is read before paint and followed on resize. Axis labels wait for it, then thin out where they would collide.\n const [plotWidth, setPlotWidth] = useState(0);\n const [labelSizes, setLabelSizes] = useState>({});\n const measure = useRef(null);\n const labelTexts = [...new Set(data.map(item => item.axisLabel).filter((text): text is string => !!text))].join(\"\\n\");\n useLayoutEffect(() => {\n const node = plot.current, ruler = measure.current;\n if (!node || !ruler) return;\n const read = () => {\n width.set(node.clientWidth);\n setPlotWidth(node.clientWidth);\n const sizes: Record = {};\n ruler.querySelectorAll(\"[data-text]\").forEach(item => { sizes[item.dataset.text ?? \"\"] = item.offsetWidth; });\n setLabelSizes(current => Object.keys(sizes).every(text => current[text] === sizes[text]) ? current : sizes);\n };\n read();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(read);\n observer.observe(node);\n observer.observe(ruler);\n return () => observer.disconnect();\n }, [labelTexts, width]);\n const shownLabels = useMemo(() => fittingLabels(data, plotWidth, labelSizes), [data, plotWidth, labelSizes]);\n\n // A new range zooms the time axis around the latest bar, rescales the gridlines, and moves the average, all on springs that can be retargeted mid-flight.\n useEffect(() => {\n if (reduced) { unitShare.jump(1 / count); scale.jump(top); return; }\n return soon(() => { const controls = [animate(unitShare, 1 / count, zoom), animate(scale, top, settle)]; return { stop: () => controls.forEach(control => control.stop()) }; });\n }, [count, reduced, scale, top, unitShare]);\n // The average rises with the first bars, then springs to each new range.\n const meanStarted = useRef(false);\n useEffect(() => {\n if (reduced) { mean.jump(average); return; }\n if (!inView) return;\n const first = !meanStarted.current;\n meanStarted.current = true;\n return soon(() => animate(mean, average, first ? { ...settle, delay: .12 } : settle));\n }, [average, inView, mean, reduced]);\n\n // The cursor appears on the first scrubbed bar and glides between the ones after it.\n const wasScrubbing = useRef(false);\n useEffect(() => {\n if (index === null) { wasScrubbing.current = false; return; }\n const place = last - index;\n if (reduced || !wasScrubbing.current) cursor.jump(place);\n wasScrubbing.current = true;\n if (reduced) return;\n const controls = animate(cursor, place, glide);\n return () => controls.stop();\n }, [cursor, index, last, reduced]);\n\n const cursorX = useTransform(() => Math.round(centerOf(frame, cursor.get())) + .5);\n const meanY = useTransform(() => Math.round(height - (mean.get() / scale.get()) * (height - TOP)) + .5);\n const meanText = useTransform(() => `Avg ${formatValue(Math.round(mean.get()))}`);\n\n const pointAt = (clientX: number) => {\n const rect = plot.current?.getBoundingClientRect();\n if (!rect?.width) return last;\n const place = Math.floor(((rect.right - clientX) / rect.width) * count);\n return clamp(last - place, 0, last);\n };\n const onKeyDown = (event: KeyboardEvent) => {\n if (event.key === \"Escape\" && scrubbing) { event.preventDefault(); setActive(null); return; }\n const from = index ?? last + 1, page = Math.min(7, count);\n const next = ({ ArrowLeft: from - 1, ArrowDown: from - 1, ArrowRight: index === null ? last : from + 1, ArrowUp: index === null ? last : from + 1, PageDown: from - page, PageUp: from + page, Home: 0, End: last } as Record)[event.key];\n if (next === undefined || last < 0) return;\n event.preventDefault();\n setActive(clamp(next, 0, last));\n };\n const onPointerDown = (event: PointerEvent) => {\n if (event.pointerType !== \"mouse\") event.currentTarget.setPointerCapture?.(event.pointerId);\n setActive(pointAt(event.clientX));\n };\n const onPointerMove = (event: PointerEvent) => { if (event.pointerType === \"mouse\" || event.buttons) setActive(pointAt(event.clientX)); };\n const release = (event: PointerEvent) => { if (event.pointerType !== \"mouse\") setActive(null); };\n\n const scrubbed = scrubbing ? data[index] : null;\n const reading = scrubbed ?? data[last];\n const valueText = reading ? `${reading.label}: ${formatValue(reading.value)}${suffix}` : \"No data\";\n const highest = data.reduce((best, item) => !best || item.value > best.value ? item : best, null);\n const lowest = data.reduce((best, item) => !best || item.value < best.value ? item : best, null);\n const summary = `${label}, ${period}. ${averageLabel} ${formatValue(Math.round(average))}${suffix}.${highest ? ` Highest ${highest.label}, ${formatValue(highest.value)}${suffix}.` : \"\"}${lowest && lowest !== highest ? ` Lowest ${lowest.label}, ${formatValue(lowest.value)}${suffix}.` : \"\"}`;\n const shown = inView || reduced;\n const avoid = showAverage ? meanY : null;\n\n return
\n
{label}, {period}
\n
\n \n {unit && {unit}}\n \n
\n
\n
\n \n {ticks.map(value => )}\n \n \n {data.map((item, at) => )}\n \n \n {showAverage && }\n \n
\n
\n {ticks.map(value => )}\n {showAverage && {meanText}}\n
\n
\n \n {data.map((item, at) => item.axisLabel && shownLabels.has(item.key) ? : null)}\n \n {labelTexts.split(\"\\n\").filter(Boolean).map(text => {text})}\n
\n {/* The scrubber lies over the plot: hover or drag across it, or focus it and use the arrow keys. Vertical swipes still scroll the page. */}\n
setActive(null)} onPointerLeave={event => { if (event.pointerType === \"mouse\") setActive(null); }} onKeyDown={onKeyDown} onBlur={() => setActive(null)}\n onFocus={event => { if (event.currentTarget.matches(\":focus-visible\") && last >= 0) setActive(current => current ?? last); }} />\n
\n
\n \n \n {data.map(item => )}\n
{label}, {period}
{categoryLabel}{label}
{item.label}{formatValue(item.value)}{suffix}
\n

{known.changes ? `${period}. ${averageLabel} ${formatValue(Math.round(average))}${suffix}.` : \"\"}

\n
;\n}\n\nexport default BarChart;\n"},{"path":"registry/components/bar-chart/bar-chart.module.css","type":"registry:component","target":"@components/arc/bar-chart/bar-chart.module.css","content":".figure { display: grid; min-width: 0; gap: var(--space-5); margin: 0; color: var(--foreground); }\n\n/* The headline is the readout: the average at rest, the scrubbed bar while exploring. Each line keeps its height, so swaps never move the plot. */\n.header { display: grid; min-width: 0; gap: 2px; }\n.kind, .when { display: block; min-height: calc(1em * var(--leading-body)); font-size: var(--text-sm); line-height: var(--leading-body); }\n.kind { color: var(--text-secondary); }\n.when { color: var(--text-muted); }\n.value { display: flex; min-width: 0; align-items: baseline; gap: 6px; font-family: var(--font-display); font-size: var(--text-3xl); font-weight: 500; letter-spacing: var(--tracking-display); line-height: var(--leading-display); font-variant-numeric: tabular-nums; }\n.unit { color: var(--text-secondary); font-family: var(--font-body); font-size: var(--text-base); font-weight: 400; letter-spacing: var(--tracking-body); }\n.swap { position: relative; display: block; min-width: 0; }\n.swapText { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n/* A rolling number: outgoing digits are popped out of flow and anchored right, and the clip hides a digit still rising into a new place. */\n.roll { position: relative; display: inline-flex; justify-content: flex-end; overflow-x: clip; overflow-y: visible; white-space: pre; }\n.digits { position: relative; display: inline-flex; flex: none; }\n.digit { position: relative; display: inline-block; white-space: pre; }\n.sizer { position: absolute; top: 0; left: 0; visibility: hidden; white-space: pre; pointer-events: none; }\n\n/* Plot, right-hand value axis, and the date axis beneath. */\n.chart { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) 52px; grid-template-rows: auto 26px; min-width: 0; }\n.plot { position: relative; grid-area: 1 / 1; min-width: 0; }\n.svg { display: block; overflow: hidden; }\n.grid { stroke: var(--border); stroke-width: 1; shape-rendering: crispEdges; }\n.baseline { stroke: var(--border-strong); stroke-width: 1; shape-rendering: crispEdges; }\n.cursor { stroke: var(--border-strong); stroke-width: 1; shape-rendering: crispEdges; }\n.mean { stroke: var(--text-secondary); stroke-width: 1; stroke-dasharray: 3 3; shape-rendering: crispEdges; }\n/* The series is drawn in a soft step of the accent; the scrubbed bar takes the full accent while its neighbours step back. */\n.bar { fill: color-mix(in oklab, var(--accent) 62%, var(--surface)); transition: fill var(--duration-fast) var(--ease-standard); }\n.chart[data-scrubbing] .bar { fill: color-mix(in oklab, var(--accent) 24%, var(--surface)); }\n/* On a dark surface a lighter mix keeps the hue clear instead of turning muddy. */\n:global([data-theme=\"dark\"]) .bar { fill: color-mix(in oklab, var(--accent) 76%, var(--surface)); }\n:global([data-theme=\"dark\"]) .chart[data-scrubbing] .bar { fill: color-mix(in oklab, var(--accent) 34%, var(--surface)); }\n:global([data-theme=\"dark\"]) .chart .bar[data-active] { fill: var(--accent); }\n.chart .bar[data-active] { fill: var(--accent); }\n\n.gutter { position: relative; grid-area: 1 / 2; min-width: 0; }\n.tick, .meanLabel { position: absolute; top: 0; left: 10px; display: block; height: 0; white-space: nowrap; pointer-events: none; }\n.tickText { display: block; transform: translateY(-50%); color: var(--text-muted); font-size: var(--text-xs); line-height: 1; font-variant-numeric: tabular-nums; }\n.meanLabel .tickText { color: var(--foreground); font-weight: 500; }\n\n.axis { position: relative; grid-area: 2 / 1; min-width: 0; overflow: hidden; }\n.axisLabel { position: absolute; top: 8px; left: 0; color: var(--text-muted); font-size: var(--text-xs); line-height: 1; white-space: nowrap; font-variant-numeric: tabular-nums; }\n\n/* The scrubber covers the plot and its axes. Horizontal drags scrub, vertical swipes still scroll the page. */\n.scrubber { position: absolute; inset: 0; border-radius: calc(var(--radius-control) / 2); outline: none; touch-action: pan-y; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; }\n\n.srOnly { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }\n@media (prefers-reduced-motion: reduce) { .bar { transition: none; } }\n.measure { position: absolute; top: 0; left: 0; display: flex; visibility: hidden; pointer-events: none; }\n.measure > span { flex: none; font-size: var(--text-xs); line-height: 1; white-space: nowrap; font-variant-numeric: tabular-nums; }\n"}],"docs":"Docs and live preview: https://uiarc.dev/components/bar-chart","meta":{"tier":"free","kind":"component","docs":"https://uiarc.dev/components/bar-chart","markdown":"https://uiarc.dev/components/bar-chart/markdown","tags":["chart","bar","data"]}}