{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bar-picker",
  "type": "registry:component",
  "title": "Bar Picker",
  "description": "A short run of values as columns of lights, one column picked.",
  "dependencies": [
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "cssVars": {
    "light": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    },
    "dark": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    }
  },
  "files": [
    {
      "path": "components/panelui/bar-picker.tsx",
      "type": "registry:component",
      "target": "@components/panelui/bar-picker.tsx",
      "content": "'use client';\n\n/**\n * BarPicker — a short run of values as columns of lights, one column picked.\n *\n * ```tsx\n * <BarPicker data={week} value={day} onValueChange={setDay} />\n * ```\n *\n * ## A column is a stack of lights\n *\n * Each value is drawn as a column of segments, lit from the bottom up to the\n * value, the way a level meter shows loudness. The unlit segments stay as\n * faint outlines of themselves, so every column is the same height and how\n * full it is can be judged against its own top without an axis.\n *\n * ## The picked column is the bright one\n *\n * It is lit at full strength with a glow on its highest segment, and it is\n * wider than the others. The rest are the same colour, held back. Picking\n * another column lights it from the bottom, one segment after the next.\n *\n * ## A choice, not a chart\n *\n * It behaves like a row of radio buttons. Exactly one column is picked, and\n * the arrow keys walk along the row. The figure for the picked column is\n * yours to show beside it, large: a column of eight lights cannot be read to\n * the unit, but one number can.\n */\nimport {\n  useEffect,\n  useRef,\n  useState,\n  type ComponentProps,\n  type KeyboardEvent,\n  type ReactNode,\n} from 'react';\nimport { tv } from 'tailwind-variants';\n\nexport interface BarPickerBar {\n  /** Written under the column: \"Mon\", \"W3\". Keep it to a few characters. */\n  label: ReactNode;\n  value: number;\n  /** The long form, for a screen reader: \"Monday\". Defaults to the label. */\n  name?: string;\n}\n\n/** Which of the theme's roles the lights are drawn in. */\nexport type BarPickerColor = 'primary' | 'info' | 'success' | 'warning' | 'destructive';\n\nconst barPickerVariants = tv({\n  slots: {\n    root: 'flex w-full items-end gap-1.5',\n    column:\n      'group flex min-w-0 cursor-pointer flex-col items-stretch gap-2.5 rounded-lg outline-none transition-[flex-grow] duration-500 ease-[cubic-bezier(0.34,1.4,0.64,1)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-4 focus-visible:ring-offset-background motion-reduce:transition-none',\n    // Stacked from the bottom, so the first segment in the markup is the\n    // lowest light.\n    stack: 'flex h-28 flex-col-reverse gap-[3px]',\n    segment:\n      'min-h-0 flex-1 rounded-[5px] transition-[background-color,box-shadow,opacity] duration-300 motion-reduce:transition-none',\n    label: 'truncate text-center text-xs text-muted-foreground transition-colors',\n  },\n  variants: {\n    color: {\n      primary: { root: '[--pu-color:var(--primary)]' },\n      info: { root: '[--pu-color:var(--info)]' },\n      success: { root: '[--pu-color:var(--success)]' },\n      warning: { root: '[--pu-color:var(--warning)]' },\n      destructive: { root: '[--pu-color:var(--destructive)]' },\n    },\n    selected: {\n      true: { column: 'flex-[1.5]', label: 'font-medium text-foreground' },\n      false: { column: 'flex-1' },\n    },\n    /** What one segment is doing. */\n    light: {\n      off: { segment: 'bg-muted' },\n      // Lit in a column that is not picked: the colour, held back.\n      dim: {\n        segment:\n          'bg-[color-mix(in_oklab,var(--pu-color)_26%,transparent)] group-hover:bg-[color-mix(in_oklab,var(--pu-color)_48%,transparent)]',\n      },\n      on: { segment: 'bg-[var(--pu-color)]' },\n      // The highest light of the picked column.\n      peak: {\n        segment:\n          'bg-[var(--pu-color)] shadow-[0_0_16px_color-mix(in_oklab,var(--pu-color)_65%,transparent)]',\n      },\n    },\n  },\n});\n\nexport interface BarPickerProps\n  extends Omit<ComponentProps<'div'>, 'children' | 'onChange' | 'defaultValue' | 'color'> {\n  /** The columns, left to right. */\n  data: BarPickerBar[];\n  /** The value a fully lit column stands for. Defaults to the largest in `data`. */\n  max?: number;\n  /** The index of the picked column, when the caller holds it. */\n  value?: number;\n  /** The index picked to begin with, when the caller does not hold it. */\n  defaultValue?: number;\n  /** Called with the index of a column when it is picked. */\n  onValueChange?: (index: number) => void;\n  /** How many lights a column is made of. */\n  segments?: number;\n  /** Which of the theme's roles the lights are drawn in. */\n  color?: BarPickerColor;\n  /** Writes a value for a screen reader: \"17 deploys\". */\n  formatValue?: (value: number) => string;\n  /** What a screen reader calls the row. */\n  label?: string;\n}\n\n/** Milliseconds between one light of a column coming on and the next. */\nconst STAGGER = 32;\n\nexport function BarPicker({\n  data,\n  max,\n  value,\n  defaultValue = 0,\n  onValueChange,\n  segments = 8,\n  color = 'primary',\n  formatValue = (bar) => String(bar),\n  label = 'Values',\n  className,\n  ...props\n}: BarPickerProps) {\n  const slots = barPickerVariants({ color });\n  const [internal, setInternal] = useState(defaultValue);\n  const picked = clampIndex(value ?? internal, data.length);\n  const refs = useRef<(HTMLButtonElement | null)[]>([]);\n  // The columns start dark and light up once mounted, so they are seen\n  // arriving.\n  const [lit, setLit] = useState(false);\n  useEffect(() => {\n    const frame = requestAnimationFrame(() => setLit(true));\n    return () => cancelAnimationFrame(frame);\n  }, []);\n\n  const count = Math.max(1, Math.round(segments));\n  const ceiling = max ?? Math.max(1, ...data.map((bar) => bar.value));\n\n  const pick = (index: number) => {\n    if (value === undefined) setInternal(index);\n    onValueChange?.(index);\n  };\n\n  const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>, index: number) => {\n    const rtl = getComputedStyle(event.currentTarget).direction === 'rtl';\n    const forward = rtl ? -1 : 1;\n    const moves: Record<string, number> = {\n      ArrowLeft: index - forward,\n      ArrowRight: index + forward,\n      ArrowDown: index - 1,\n      ArrowUp: index + 1,\n      Home: 0,\n      End: data.length - 1,\n    };\n    const target = moves[event.key];\n    if (target === undefined) return;\n    event.preventDefault();\n    // Round the ends, as a radio group does.\n    const next = (target + data.length) % data.length;\n    refs.current[next]?.focus();\n    pick(next);\n  };\n\n  return (\n    <div role=\"radiogroup\" aria-label={label} className={slots.root({ className })} {...props}>\n      {data.map((bar, index) => {\n        const selected = index === picked;\n        const part = barPickerVariants({ selected });\n        const name = bar.name ?? (typeof bar.label === 'string' ? bar.label : `Bar ${index + 1}`);\n        // How many lights the value reaches. Any value above zero lights at\n        // least one, so a small value is a mark and not an empty column.\n        const share = Math.min(1, Math.max(0, bar.value / ceiling));\n        const reached = !lit || bar.value <= 0 ? 0 : Math.max(1, Math.round(share * count));\n        return (\n          <button\n            key={index}\n            ref={(node) => {\n              refs.current[index] = node;\n            }}\n            type=\"button\"\n            role=\"radio\"\n            aria-checked={selected}\n            aria-label={`${name}: ${formatValue(bar.value)}`}\n            tabIndex={selected ? 0 : -1}\n            onClick={() => pick(index)}\n            onKeyDown={(event) => onKeyDown(event, index)}\n            className={part.column()}\n          >\n            <span className={slots.stack()}>\n              {Array.from({ length: count }, (_, level) => {\n                const on = level < reached;\n                const light = !on ? 'off' : !selected ? 'dim' : level === reached - 1 ? 'peak' : 'on';\n                return (\n                  <span\n                    key={level}\n                    className={barPickerVariants({ light }).segment()}\n                    style={{\n                      // Bottom first, so a column is seen filling upwards.\n                      transitionDelay: on ? `${level * STAGGER}ms` : undefined,\n                      // The picked column brightens towards its top.\n                      opacity:\n                        light === 'on'\n                          ? Number((0.5 + 0.5 * ((level + 1) / reached)).toFixed(3))\n                          : undefined,\n                    }}\n                  />\n                );\n              })}\n            </span>\n            <span aria-hidden=\"true\" className={part.label()}>\n              {bar.label}\n            </span>\n          </button>\n        );\n      })}\n    </div>\n  );\n}\nBarPicker.displayName = 'BarPicker';\n\nfunction clampIndex(index: number, length: number): number {\n  if (length === 0) return 0;\n  return Math.min(length - 1, Math.max(0, index));\n}\n"
    }
  ]
}
