{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "px-bars",
  "title": "Bar chart",
  "description": "Pixel bar chart whose bars grow cell by cell and spring to new data; hover or the arrow keys show exact values in a tooltip.",
  "registryDependencies": [
    "https://elements.lumesec.ai/r/lumesec-theme.json",
    "https://elements.lumesec.ai/r/lumesec-math.json",
    "https://elements.lumesec.ai/r/lumesec-motion.json",
    "https://elements.lumesec.ai/r/lumesec-pixel-canvas.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/components/lumesec/px-bars.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { clamp, mixRgb, rgba } from \"@/lib/lumesec/math\";\nimport { prefersReducedMotion, springEuler } from \"@/lib/lumesec/motion\";\nimport { usePixelCanvas } from \"@/lib/lumesec/pixel-canvas\";\nimport { cn } from \"@/lib/utils\";\n\n/** One bar. */\nexport interface PxBarsDatum {\n  /** Label under the bar. */\n  label: string;\n  /** Bar value. Negative and non-finite values draw an empty bar. */\n  value: number;\n}\n\n/** Props of `PxBars`. Other `div` props go to the root element. */\nexport interface PxBarsProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n  /** The bars, left to right. Bars spring to new values when this changes. */\n  data?: readonly PxBarsDatum[];\n  /** Top of the value axis. Without it, or with 0, the axis tops out at 110% of the largest value. */\n  max?: number;\n  /** Start of the accessible description, followed by every label and value. */\n  label?: string;\n  /** Unit shown after each value in the tooltip and the accessible description. */\n  unit?: string;\n  /** Canvas height in pixels. */\n  height?: number;\n}\n\nconst SAMPLE_DATA: PxBarsDatum[] = [\n  { label: \"Mon\", value: 12 },\n  { label: \"Tue\", value: 18 },\n  { label: \"Wed\", value: 9 },\n  { label: \"Thu\", value: 22 },\n  { label: \"Fri\", value: 17 },\n  { label: \"Sat\", value: 6 },\n  { label: \"Sun\", value: 4 },\n];\n\n/** Width of the value axis on the left. */\nconst AXIS = 30;\n/** Height of one pixel cell in a bar. */\nconst CELL = 6;\n\ninterface Bar {\n  position: number;\n  velocity: number;\n}\n\n/**\n * Pixel bar chart whose bars grow cell by cell and spring to new data. Hovering a bar, or the arrow keys\n * while the chart has focus, shows its exact value in a tooltip.\n *\n * @example\n * <PxBars label=\"Runs per day\" unit=\"runs\" data={[{ label: \"Mon\", value: 12 }, { label: \"Tue\", value: 18 }]} />\n */\nexport function PxBars({\n  data = SAMPLE_DATA,\n  max = 0,\n  label = \"Bar chart\",\n  unit,\n  height = 170,\n  className,\n  ...props\n}: PxBarsProps) {\n  const hovered = React.useRef(-1);\n  const bars = React.useRef<Bar[]>([]);\n  /** When the bars started growing, in seconds; each bar waits 50 ms longer than the one before. */\n  const growStart = React.useRef(0);\n  const tipRef = React.useRef<HTMLSpanElement>(null);\n\n  const { ref: canvasRef, redraw } = usePixelCanvas((frame) => {\n    const { palette, context: g, time, delta, canvas } = frame;\n    const w = frame.width;\n    const h = frame.height;\n    const reduced = prefersReducedMotion();\n    const n = data.length;\n    const values = data.map((datum) => drawableValue(datum.value));\n    const columnWidth = n > 0 ? (w - AXIS) / n : 0;\n    const top = 8;\n    const bottom = h - 22;\n    const axisMax = Math.max(1, max || Math.max(0, ...values) * 1.1);\n    const cells = Math.floor((bottom - top) / CELL);\n    const hover = hovered.current;\n    let moving = false;\n    frame.clear();\n\n    g.font = `10px ${getComputedStyle(canvas).fontFamily || \"monospace\"}`;\n    g.textBaseline = \"middle\";\n    g.textAlign = \"right\";\n    for (let q = 0; q <= 4; q++) {\n      const y = bottom - ((bottom - top) * q) / 4;\n      for (let x = AXIS; x < w; x += 6) frame.dot(x, y, palette.foreground, 0.12, 1.6);\n      g.globalAlpha = 0.9;\n      g.fillStyle = rgba(palette.muted);\n      g.fillText(String(Math.round((axisMax * q) / 4)), AXIS - 6, y);\n    }\n\n    // the first bars stagger in; bars added later start at once\n    if (bars.current.length === 0) growStart.current = time;\n    g.textAlign = \"center\";\n    g.textBaseline = \"alphabetic\";\n    data.forEach((datum, i) => {\n      const target = (values[i] / axisMax) * cells;\n      let bar = bars.current[i];\n      if (!bar) {\n        bar = { position: 0, velocity: 0 };\n        bars.current[i] = bar;\n      }\n      if (reduced) bar.position = target;\n      else if (time > growStart.current + i * 0.05) springEuler(bar, target, delta, 160, 0.65);\n      if (Math.abs(target - bar.position) > 0.02 || Math.abs(bar.velocity) > 0.05) moving = true;\n\n      const isHovered = i === hover;\n      const x0 = AXIS + i * columnWidth + columnWidth * 0.18;\n      const columns = Math.max(1, Math.floor((columnWidth * 0.64) / CELL));\n      const filled = Math.max(0, bar.position);\n      const full = Math.floor(filled);\n      for (let c = 0; c < columns; c++) {\n        for (let k = 0; k < Math.ceil(filled); k++) {\n          // the top cell fades in with the fraction of a cell the bar has reached\n          const isTop = k === full;\n          const alpha = isTop ? filled - full : 1;\n          const color =\n            isTop || isHovered ? mixRgb(palette.accent, palette.shine, isHovered ? 0.35 : 0.5) : palette.accent;\n          frame.dot(\n            x0 + c * CELL,\n            bottom - (k + 1) * CELL + 1.5,\n            color,\n            (hover >= 0 && !isHovered ? 0.45 : 0.95) * alpha,\n            4.5,\n          );\n        }\n      }\n      g.globalAlpha = isHovered ? 1 : 0.85;\n      g.fillStyle = rgba(isHovered ? palette.foreground : palette.muted);\n      g.fillText(datum.label, x0 + (columns * CELL) / 2 - 1, h - 6);\n    });\n    g.globalAlpha = 1;\n\n    const tip = tipRef.current;\n    const datum = data[hover];\n    const bar = bars.current[hover];\n    if (tip && datum && bar) {\n      const x = AXIS + (hover + 0.5) * columnWidth;\n      const text = `${datum.label} · ${formatValue(datum.value, unit)}`;\n      tip.hidden = false;\n      if (tip.textContent !== text) tip.textContent = text;\n      tip.style.left = `${clamp(x, 50, w - 50)}px`;\n      tip.style.top = `${bottom - Math.max(0, bar.position) * CELL}px`;\n    }\n    return moving;\n  });\n\n  React.useEffect(redraw, [data, max, unit, redraw]);\n\n  const setHover = (index: number) => {\n    hovered.current = index;\n    redraw();\n  };\n  const hideTip = () => {\n    if (tipRef.current) tipRef.current.hidden = true;\n    setHover(-1);\n  };\n\n  const description = `${label}: ${data.map((datum) => `${datum.label} ${formatValue(datum.value, unit)}`).join(\", \")}`;\n\n  return (\n    <div data-slot=\"px-bars\" className={cn(\"relative block\", className)} {...props}>\n      <canvas\n        ref={canvasRef}\n        tabIndex={0}\n        role=\"img\"\n        aria-label={description}\n        className=\"block w-full cursor-crosshair font-mono outline-none [image-rendering:pixelated] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lumesec focus-visible:outline-solid\"\n        style={{ height }}\n        onPointerMove={(event) => {\n          const rect = event.currentTarget.getBoundingClientRect();\n          const index = Math.floor((event.clientX - rect.left - AXIS) / ((rect.width - AXIS) / data.length));\n          setHover(index >= 0 && index < data.length ? index : -1);\n        }}\n        onPointerLeave={hideTip}\n        onBlur={hideTip}\n        onKeyDown={(event) => {\n          const n = data.length;\n          if (n === 0) return;\n          if (event.key === \"ArrowRight\") {\n            event.preventDefault();\n            setHover((hovered.current + 1) % n);\n          }\n          if (event.key === \"ArrowLeft\") {\n            event.preventDefault();\n            setHover((hovered.current - 1 + n) % n);\n          }\n        }}\n      />\n      {/* positioned from the animation loop, so it rides on the springing bar */}\n      <span\n        ref={tipRef}\n        hidden\n        className=\"pointer-events-none absolute z-[5] [transform:translate(-50%,calc(-100%-6px))] rounded-md bg-foreground px-2 py-1 text-[11.5px] leading-normal whitespace-nowrap text-card\"\n      />\n    </div>\n  );\n}\n\n/** The height a value is drawn at: negative and non-finite values draw an empty bar instead of breaking the scale. */\nfunction drawableValue(value: number) {\n  return Number.isFinite(value) ? Math.max(0, value) : 0;\n}\n\n/** A value as the tooltip and the accessible description write it, with the unit when there is one. */\nfunction formatValue(value: number, unit: string | undefined) {\n  return `${value.toLocaleString(\"en-US\")}${unit ? ` ${unit}` : \"\"}`;\n}\n",
      "type": "registry:component",
      "target": "@components/lumesec/px-bars.tsx"
    }
  ],
  "meta": {
    "tag": "px-bars",
    "collection": "pixel-ui",
    "react": "PxBars",
    "kind": "react",
    "data": "input"
  },
  "docs": "Bar chart docs: https://elements.lumesec.ai/components/pixel-ui/bars",
  "categories": [
    "pixel-ui",
    "navigation-data"
  ],
  "type": "registry:component"
}