{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ds-boxplot-chart",
  "title": "Boxplot Chart",
  "description": "A fully custom SVG box-and-whisker chart with ResizeObserver for responsive layout, notch support, and outlier plotting.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "skeleton",
    "ui-i18n",
    "format-utils"
  ],
  "files": [
    {
      "path": "components/ds/boxplot-chart.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva } from \"class-variance-authority\"\nimport { cn } from \"@/lib/utils\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport { BarChart2 } from \"lucide-react\"\nimport { UI_I18N, type UILocale } from \"@/lib/ui-i18n\"\nimport { formatChartValue, type FormatPreset } from \"@/lib/format-utils\"\n\n// ── Types ──────────────────────────────────────────────────────────────────\n\nexport type BoxPlotOrientation = \"vertical\" | \"horizontal\"\n\nexport interface BoxPlotItem {\n  name: string\n  min: number\n  q1: number\n  median: number\n  /** Optional mean marker (diamond) */\n  mean?: number\n  q3: number\n  max: number\n  /** Individual outlier values outside min/max */\n  outliers?: number[]\n  /** Defaults to next --chart-N token */\n  color?: string\n}\n\nexport interface BoxPlotChartProps extends React.HTMLAttributes<HTMLDivElement> {\n  data: BoxPlotItem[]\n  title?: string\n  subtitle?: string\n  footer?: React.ReactNode\n  /** \"vertical\" = boxes grow upward (default); \"horizontal\" = boxes grow rightward */\n  orientation?: BoxPlotOrientation\n  height?: number\n  /** Draw background grid lines on the value axis */\n  showGrid?: boolean\n  /** Draw a diamond at the mean value */\n  showMean?: boolean\n  /** Draw dots for outlier values */\n  showOutliers?: boolean\n  /** Notch the box at the median — visually encodes median confidence */\n  notched?: boolean\n  /** Format value-axis tick labels and tooltip values */\n  valueFormatter?: (value: number) => string\n  format?: FormatPreset\n  decimals?: number\n  currency?: string\n  abbreviate?: boolean\n  /** Show animated skeleton in place of the chart while data loads */\n  loading?: boolean\n  locale?: UILocale\n}\n\n// ── Constants ──────────────────────────────────────────────────────────────\n\nconst CHART_COLORS = [\n  \"var(--chart-1)\",\n  \"var(--chart-2)\",\n  \"var(--chart-3)\",\n  \"var(--chart-4)\",\n  \"var(--chart-5)\",\n]\n\nconst MARGIN_V = { top: 16, right: 20, bottom: 36, left: 48 }\nconst MARGIN_H = { top: 8, right: 24, bottom: 8, left: 96 }\n\n// ── Skeleton ───────────────────────────────────────────────────────────────\n\nconst BOXPLOT_SKELETON_BOXES = [\n  { cx: 12, whiskerLo: 78, q1: 65, median: 50, q3: 32, whiskerHi: 18 },\n  { cx: 35, whiskerLo: 82, q1: 72, median: 55, q3: 38, whiskerHi: 22 },\n  { cx: 58, whiskerLo: 70, q1: 58, median: 40, q3: 25, whiskerHi: 15 },\n  { cx: 81, whiskerLo: 88, q1: 76, median: 62, q3: 45, whiskerHi: 28 },\n]\nconst YAXIS_TICK_WIDTHS = [28, 20, 24, 18, 22]\n\ninterface BoxPlotChartSkeletonProps {\n  height?: number\n  hasTitle?: boolean\n  hasSubtitle?: boolean\n  hasFooter?: boolean\n  className?: string\n}\n\nfunction BoxPlotChartSkeleton({\n  height = 320,\n  hasTitle = false,\n  hasSubtitle = false,\n  hasFooter = false,\n  className,\n}: BoxPlotChartSkeletonProps) {\n  const XAXIS_WIDTHS = [28, 36, 24, 32]\n\n  return (\n    <div\n      className={cn(\"flex w-full flex-col\", className)}\n      data-slot=\"boxplot-chart-skeleton\"\n    >\n      {(hasTitle || hasSubtitle) && (\n        <div className=\"flex flex-col gap-1.5 px-1 pb-4\">\n          {hasTitle && (\n            <Skeleton\n              className=\"h-3.5 w-44 rounded-md\"\n              style={{ animationDelay: \"0s\" }}\n            />\n          )}\n          {hasSubtitle && (\n            <Skeleton\n              className=\"mt-0.5 h-2.5 w-28 rounded-md\"\n              style={{ animationDelay: \"0.1s\" }}\n            />\n          )}\n        </div>\n      )}\n\n      <div className=\"relative overflow-hidden\" style={{ height }}>\n        {/* Y-axis tick labels */}\n        <div className=\"absolute top-2 bottom-8 left-0 flex w-9 flex-col items-end justify-between pr-1\">\n          {YAXIS_TICK_WIDTHS.map((w, i) => (\n            <Skeleton\n              key={i}\n              className=\"h-2.5 rounded-sm\"\n              style={{ width: w, animationDelay: `${0.5 + i * 0.12}s` }}\n            />\n          ))}\n        </div>\n\n        {/* Chart area with SVG box plots */}\n        <div className=\"absolute top-2 right-1 bottom-8 left-11\">\n          <svg\n            viewBox=\"0 0 100 100\"\n            preserveAspectRatio=\"none\"\n            className=\"h-full w-full\"\n          >\n            {/* Horizontal grid lines */}\n            {[25, 50, 75].map((y) => (\n              <line\n                key={y}\n                x1=\"0\"\n                y1={y}\n                x2=\"100\"\n                y2={y}\n                stroke=\"var(--muted)\"\n                strokeWidth=\"0.6\"\n                opacity={0.5}\n                strokeDasharray=\"3 3\"\n              />\n            ))}\n            {/* Box plots */}\n            {BOXPLOT_SKELETON_BOXES.map((box, i) => (\n              <g\n                key={i}\n                className=\"motion-safe:animate-pulse\"\n                style={{ animationDelay: `${i * 0.15}s` }}\n              >\n                {/* Whisker lines */}\n                <line\n                  x1={box.cx}\n                  y1={box.whiskerLo}\n                  x2={box.cx}\n                  y2={box.q1}\n                  stroke=\"var(--muted)\"\n                  strokeWidth=\"1.5\"\n                  strokeDasharray=\"3 2\"\n                />\n                <line\n                  x1={box.cx}\n                  y1={box.q3}\n                  x2={box.cx}\n                  y2={box.whiskerHi}\n                  stroke=\"var(--muted)\"\n                  strokeWidth=\"1.5\"\n                  strokeDasharray=\"3 2\"\n                />\n                {/* Whisker caps */}\n                <line\n                  x1={box.cx - 4}\n                  y1={box.whiskerLo}\n                  x2={box.cx + 4}\n                  y2={box.whiskerLo}\n                  stroke=\"var(--muted)\"\n                  strokeWidth=\"1.5\"\n                />\n                <line\n                  x1={box.cx - 4}\n                  y1={box.whiskerHi}\n                  x2={box.cx + 4}\n                  y2={box.whiskerHi}\n                  stroke=\"var(--muted)\"\n                  strokeWidth=\"1.5\"\n                />\n                {/* IQR box */}\n                <rect\n                  x={box.cx - 7}\n                  y={box.q3}\n                  width={14}\n                  height={box.q1 - box.q3}\n                  fill=\"var(--muted)\"\n                  fillOpacity={0.3}\n                  stroke=\"var(--muted)\"\n                  strokeWidth=\"1.5\"\n                  rx={1}\n                />\n                {/* Median line */}\n                <line\n                  x1={box.cx - 7}\n                  y1={box.median}\n                  x2={box.cx + 7}\n                  y2={box.median}\n                  stroke=\"var(--muted)\"\n                  strokeWidth=\"2\"\n                />\n              </g>\n            ))}\n          </svg>\n        </div>\n\n        {/* X-axis tick labels */}\n        <div className=\"absolute right-1 bottom-0 left-11 flex h-7 items-center justify-around\">\n          {XAXIS_WIDTHS.map((w, i) => (\n            <Skeleton\n              key={i}\n              className=\"h-2.5 rounded-sm\"\n              style={{ width: w, animationDelay: `${0.05 + i * 0.1}s` }}\n            />\n          ))}\n        </div>\n      </div>\n\n      {hasFooter && (\n        <div className=\"mt-4 flex items-center gap-2 border-t border-border px-1 pt-3\">\n          <Skeleton\n            className=\"h-3.5 w-3.5 shrink-0 rounded-full\"\n            style={{ animationDelay: \"0.7s\" }}\n          />\n          <Skeleton\n            className=\"h-2.5 w-36 rounded-md\"\n            style={{ animationDelay: \"0.8s\" }}\n          />\n          <Skeleton\n            className=\"ml-auto h-2.5 w-20 rounded-md\"\n            style={{ animationDelay: \"0.9s\" }}\n          />\n        </div>\n      )}\n    </div>\n  )\n}\n\n// ── Variants ───────────────────────────────────────────────────────────────\n\nconst chartWrapperVariants = cva(\"flex w-full flex-col\")\nconst chartHeaderVariants = cva(\"flex flex-col px-1 pb-4\")\nconst chartTitleVariants = cva(\n  \"text-sm leading-tight font-semibold text-foreground\"\n)\nconst chartSubtitleVariants = cva(\"mt-0.5 text-xs text-muted-foreground\")\nconst chartFooterVariants = cva(\n  \"mt-4 flex items-center gap-2 border-t border-border px-1 pt-3 text-xs text-muted-foreground\"\n)\n\n// ── Helpers ────────────────────────────────────────────────────────────────\n\nfunction niceTicks(lo: number, hi: number, count = 5): number[] {\n  const raw = (hi - lo) / (count - 1)\n  const mag = Math.pow(10, Math.floor(Math.log10(raw)))\n  const nice = [1, 2, 2.5, 5, 10].find((f) => f * mag >= raw) ?? 10\n  const step = nice * mag\n  const start = Math.floor(lo / step) * step\n  const ticks: number[] = []\n  for (\n    let t = start;\n    t <= hi + step * 0.001;\n    t = Math.round((t + step) * 1e9) / 1e9\n  ) {\n    ticks.push(t)\n  }\n  return ticks\n}\n\nfunction resolveColors(data: BoxPlotItem[]): BoxPlotItem[] {\n  return data.map((d, i) => ({\n    ...d,\n    color: d.color ?? CHART_COLORS[i % CHART_COLORS.length],\n  }))\n}\n\n// ── Tooltip ────────────────────────────────────────────────────────────────\n\ninterface TooltipState {\n  item: BoxPlotItem\n  /** Position relative to the container element */\n  left: number\n  top: number\n}\n\nfunction BoxTooltip({\n  item,\n  left,\n  top,\n  fmt,\n  locale = \"en-US\",\n}: {\n  item: BoxPlotItem\n  left: number\n  top: number\n  fmt: (v: number) => string\n  locale?: UILocale\n}) {\n  const t = UI_I18N[locale].boxplot\n\n  return (\n    <div\n      className=\"pointer-events-none absolute z-50 min-w-40 rounded-lg border border-border bg-card px-3 py-2 shadow-lg\"\n      style={{ left, top, transform: \"translate(-50%, calc(-100% - 10px))\" }}\n    >\n      <p className=\"mb-1.5 text-xs font-semibold text-foreground\">\n        {item.name}\n      </p>\n      <div className=\"flex flex-col gap-0.5\">\n        {[\n          { label: t.max, value: item.max },\n          { label: t.q3, value: item.q3 },\n          { label: t.median, value: item.median },\n          ...(item.mean !== undefined\n            ? [{ label: t.mean, value: item.mean }]\n            : []),\n          { label: t.q1, value: item.q1 },\n          { label: t.min, value: item.min },\n        ].map(({ label, value }) => (\n          <div key={label} className=\"flex items-center gap-2\">\n            <span className=\"w-12 text-xs text-muted-foreground\">{label}</span>\n            <span className=\"ml-auto text-xs font-semibold text-foreground tabular-nums\">\n              {fmt(value)}\n            </span>\n          </div>\n        ))}\n      </div>\n    </div>\n  )\n}\n\n// ── Box rendering helpers ──────────────────────────────────────────────────\n\ninterface BoxProps {\n  item: BoxPlotItem\n  cx: number // center x (vertical) or center y (horizontal)\n  halfWidth: number\n  // value → pixel coordinate\n  scale: (v: number) => number\n  orientation: BoxPlotOrientation\n  notched: boolean\n  showMean: boolean\n  showOutliers: boolean\n  onHover: (item: BoxPlotItem, ev: React.MouseEvent) => void\n  onLeave: () => void\n  isHovered: boolean\n}\n\nfunction BoxShape({\n  item,\n  cx,\n  halfWidth,\n  scale,\n  orientation,\n  notched,\n  showMean,\n  showOutliers,\n  onHover,\n  onLeave,\n  isHovered,\n}: BoxProps) {\n  const color = item.color!\n  const notchDepth = halfWidth * 0.28\n\n  // All coordinates in SVG pixel space\n  if (orientation === \"vertical\") {\n    const yMin = scale(item.min)\n    const yQ1 = scale(item.q1)\n    const yMed = scale(item.median)\n    const yMean = item.mean !== undefined ? scale(item.mean) : null\n    const yQ3 = scale(item.q3)\n    const yMax = scale(item.max)\n    const x1 = cx - halfWidth\n    const x2 = cx + halfWidth\n\n    const boxPath = notched\n      ? [\n          `M ${x1} ${yQ3}`,\n          `L ${x1} ${yMed + notchDepth}`,\n          `L ${cx - notchDepth * 0.7} ${yMed}`,\n          `L ${x1} ${yMed - notchDepth}`,\n          `L ${x1} ${yQ1}`,\n          `L ${x2} ${yQ1}`,\n          `L ${x2} ${yMed - notchDepth}`,\n          `L ${cx + notchDepth * 0.7} ${yMed}`,\n          `L ${x2} ${yMed + notchDepth}`,\n          `L ${x2} ${yQ3}`,\n          \"Z\",\n        ].join(\" \")\n      : null\n\n    return (\n      <g\n        onMouseMove={(e) => onHover(item, e)}\n        onMouseLeave={onLeave}\n        opacity={isHovered ? 1 : 0.9}\n        style={{ cursor: \"default\" }}\n      >\n        {/* Whisker: min → Q1 */}\n        <line\n          x1={cx}\n          y1={yMin}\n          x2={cx}\n          y2={yQ1}\n          stroke={color}\n          strokeWidth={1.5}\n          strokeDasharray=\"3 2\"\n        />\n        {/* Whisker: Q3 → max */}\n        <line\n          x1={cx}\n          y1={yQ3}\n          x2={cx}\n          y2={yMax}\n          stroke={color}\n          strokeWidth={1.5}\n          strokeDasharray=\"3 2\"\n        />\n\n        {/* Whisker caps */}\n        <line\n          x1={x1 - 2}\n          y1={yMin}\n          x2={x2 + 2}\n          y2={yMin}\n          stroke={color}\n          strokeWidth={1.5}\n          strokeLinecap=\"round\"\n        />\n        <line\n          x1={x1 - 2}\n          y1={yMax}\n          x2={x2 + 2}\n          y2={yMax}\n          stroke={color}\n          strokeWidth={1.5}\n          strokeLinecap=\"round\"\n        />\n\n        {/* IQR Box */}\n        {notched ? (\n          <path\n            d={boxPath!}\n            fill={color}\n            fillOpacity={isHovered ? 0.28 : 0.18}\n            stroke={color}\n            strokeWidth={1.5}\n            strokeLinejoin=\"round\"\n          />\n        ) : (\n          <rect\n            x={x1}\n            y={yQ3}\n            width={halfWidth * 2}\n            height={yQ1 - yQ3}\n            fill={color}\n            fillOpacity={isHovered ? 0.28 : 0.18}\n            stroke={color}\n            strokeWidth={1.5}\n            rx={2}\n          />\n        )}\n\n        {/* Median line */}\n        <line\n          x1={x1}\n          y1={yMed}\n          x2={x2}\n          y2={yMed}\n          stroke={color}\n          strokeWidth={2.5}\n          strokeLinecap=\"round\"\n        />\n\n        {/* Mean diamond */}\n        {showMean && yMean !== null && (\n          <polygon\n            points={`${cx},${yMean - 5} ${cx + 5},${yMean} ${cx},${yMean + 5} ${cx - 5},${yMean}`}\n            fill={color}\n            stroke=\"var(--card)\"\n            strokeWidth={1.5}\n          />\n        )}\n\n        {/* Outliers */}\n        {showOutliers &&\n          item.outliers?.map((v, idx) => (\n            <circle\n              key={idx}\n              cx={cx}\n              cy={scale(v)}\n              r={3}\n              fill=\"none\"\n              stroke={color}\n              strokeWidth={1.5}\n              opacity={0.8}\n            />\n          ))}\n\n        {/* Invisible hit area */}\n        <rect\n          x={x1 - 4}\n          y={Math.min(yMax, yMin) - 4}\n          width={halfWidth * 2 + 8}\n          height={Math.abs(yQ1 - yQ3) + (yMin - yMax) + 8}\n          fill=\"transparent\"\n        />\n      </g>\n    )\n  }\n\n  // Horizontal orientation — swap x/y semantics\n  const xMin = scale(item.min)\n  const xQ1 = scale(item.q1)\n  const xMed = scale(item.median)\n  const xMean = item.mean !== undefined ? scale(item.mean) : null\n  const xQ3 = scale(item.q3)\n  const xMax = scale(item.max)\n  const y1 = cx - halfWidth\n  const y2 = cx + halfWidth\n\n  const boxPathH = notched\n    ? [\n        `M ${xQ1} ${y2}`,\n        `L ${xMed - notchDepth} ${y2}`,\n        `L ${xMed} ${y2 - notchDepth * 0.7}`,\n        `L ${xMed + notchDepth} ${y2}`,\n        `L ${xQ3} ${y2}`,\n        `L ${xQ3} ${y1}`,\n        `L ${xMed + notchDepth} ${y1}`,\n        `L ${xMed} ${y1 + notchDepth * 0.7}`,\n        `L ${xMed - notchDepth} ${y1}`,\n        `L ${xQ1} ${y1}`,\n        \"Z\",\n      ].join(\" \")\n    : null\n\n  return (\n    <g\n      onMouseMove={(e) => onHover(item, e)}\n      onMouseLeave={onLeave}\n      opacity={isHovered ? 1 : 0.9}\n      style={{ cursor: \"default\" }}\n    >\n      {/* Whisker: min → Q1 */}\n      <line\n        x1={xMin}\n        y1={cx}\n        x2={xQ1}\n        y2={cx}\n        stroke={color}\n        strokeWidth={1.5}\n        strokeDasharray=\"3 2\"\n      />\n      {/* Whisker: Q3 → max */}\n      <line\n        x1={xQ3}\n        y1={cx}\n        x2={xMax}\n        y2={cx}\n        stroke={color}\n        strokeWidth={1.5}\n        strokeDasharray=\"3 2\"\n      />\n\n      {/* Whisker caps */}\n      <line\n        x1={xMin}\n        y1={y1 - 2}\n        x2={xMin}\n        y2={y2 + 2}\n        stroke={color}\n        strokeWidth={1.5}\n        strokeLinecap=\"round\"\n      />\n      <line\n        x1={xMax}\n        y1={y1 - 2}\n        x2={xMax}\n        y2={y2 + 2}\n        stroke={color}\n        strokeWidth={1.5}\n        strokeLinecap=\"round\"\n      />\n\n      {/* IQR Box */}\n      {notched ? (\n        <path\n          d={boxPathH!}\n          fill={color}\n          fillOpacity={isHovered ? 0.28 : 0.18}\n          stroke={color}\n          strokeWidth={1.5}\n          strokeLinejoin=\"round\"\n        />\n      ) : (\n        <rect\n          x={xQ1}\n          y={y1}\n          width={xQ3 - xQ1}\n          height={halfWidth * 2}\n          fill={color}\n          fillOpacity={isHovered ? 0.28 : 0.18}\n          stroke={color}\n          strokeWidth={1.5}\n          rx={2}\n        />\n      )}\n\n      {/* Median line */}\n      <line\n        x1={xMed}\n        y1={y1}\n        x2={xMed}\n        y2={y2}\n        stroke={color}\n        strokeWidth={2.5}\n        strokeLinecap=\"round\"\n      />\n\n      {/* Mean diamond */}\n      {showMean && xMean !== null && (\n        <polygon\n          points={`${xMean - 5},${cx} ${xMean},${cx - 5} ${xMean + 5},${cx} ${xMean},${cx + 5}`}\n          fill={color}\n          stroke=\"var(--card)\"\n          strokeWidth={1.5}\n        />\n      )}\n\n      {/* Outliers */}\n      {showOutliers &&\n        item.outliers?.map((v, idx) => (\n          <circle\n            key={idx}\n            cx={scale(v)}\n            cy={cx}\n            r={3}\n            fill=\"none\"\n            stroke={color}\n            strokeWidth={1.5}\n            opacity={0.8}\n          />\n        ))}\n\n      {/* Invisible hit area */}\n      <rect\n        x={Math.min(xMin, xMax) - 4}\n        y={y1 - 4}\n        width={Math.abs(xMax - xMin) + 8}\n        height={halfWidth * 2 + 8}\n        fill=\"transparent\"\n      />\n    </g>\n  )\n}\n\n// ── BoxPlotChart ───────────────────────────────────────────────────────────\n\nexport function BoxPlotChart({\n  data,\n  title,\n  subtitle,\n  footer,\n  orientation = \"vertical\",\n  height = 320,\n  showGrid = true,\n  showMean = true,\n  showOutliers = true,\n  notched = false,\n  valueFormatter,\n  format,\n  decimals,\n  currency,\n  abbreviate,\n  loading = false,\n  locale = \"en-US\",\n  className,\n  ...props\n}: BoxPlotChartProps) {\n  // Hooks must be called unconditionally before any early returns\n  const containerRef = React.useRef<HTMLDivElement>(null)\n  const [width, setWidth] = React.useState(480)\n  const [tooltip, setTooltip] = React.useState<TooltipState | null>(null)\n\n  React.useEffect(() => {\n    const el = containerRef.current\n    if (!el) return\n    let raf = 0\n    const ro = new ResizeObserver(([entry]) => {\n      cancelAnimationFrame(raf)\n      raf = requestAnimationFrame(() => setWidth(entry.contentRect.width))\n    })\n    ro.observe(el)\n    setWidth(el.clientWidth)\n    return () => {\n      cancelAnimationFrame(raf)\n      ro.disconnect()\n    }\n  }, [])\n\n  const fmt = React.useCallback(\n    (v: number) =>\n      formatChartValue(v, {\n        format,\n        decimals,\n        locale,\n        currency,\n        abbreviate,\n        valueFormatter,\n      }),\n    [valueFormatter, format, decimals, locale, currency, abbreviate]\n  )\n\n  if (loading) {\n    return (\n      <BoxPlotChartSkeleton\n        height={height}\n        hasTitle={!!title}\n        hasSubtitle={!!subtitle}\n        hasFooter={!!footer}\n        className={className}\n      />\n    )\n  }\n\n  if (data.length === 0) {\n    return (\n      <div\n        className={cn(chartWrapperVariants(), className)}\n        data-slot=\"boxplot-chart\"\n        {...props}\n      >\n        {(title || subtitle) && (\n          <div\n            className={chartHeaderVariants()}\n            data-slot=\"boxplot-chart-header\"\n          >\n            {title && <p className={chartTitleVariants()}>{title}</p>}\n            {subtitle && <p className={chartSubtitleVariants()}>{subtitle}</p>}\n          </div>\n        )}\n        <div\n          className=\"flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border text-muted-foreground\"\n          data-slot=\"boxplot-chart-empty\"\n          style={{ height }}\n        >\n          <div className=\"flex size-10 items-center justify-center rounded-full bg-muted\">\n            <BarChart2 className=\"size-5\" />\n          </div>\n          <div className=\"text-center\">\n            <p className=\"text-sm font-medium text-foreground\">\n              {UI_I18N[locale].emptyState.noData}\n            </p>\n            <p className=\"mt-0.5 text-xs\">\n              {UI_I18N[locale].emptyState.dataWillAppear}\n            </p>\n          </div>\n        </div>\n        {footer && (\n          <div\n            className={chartFooterVariants()}\n            data-slot=\"boxplot-chart-footer\"\n          >\n            {footer}\n          </div>\n        )}\n      </div>\n    )\n  }\n\n  const items = resolveColors(data)\n\n  // Compute domain\n  const allValues = items.flatMap((d) => [\n    d.min,\n    d.q1,\n    d.median,\n    d.q3,\n    d.max,\n    ...(d.mean !== undefined ? [d.mean] : []),\n    ...(d.outliers ?? []),\n  ])\n  const domainMin = Math.min(...allValues)\n  const domainMax = Math.max(...allValues)\n  const pad = (domainMax - domainMin) * 0.08\n  const lo = domainMin - pad\n  const hi = domainMax + pad\n  const ticks = niceTicks(lo, hi, 5)\n  const tickLo = ticks[0]\n  const tickHi = ticks[ticks.length - 1]\n\n  const m = orientation === \"vertical\" ? MARGIN_V : MARGIN_H\n\n  // Drawable area\n  const drawW = Math.max(width - m.left - m.right, 60)\n  const drawH = Math.max(height - m.top - m.bottom, 60)\n\n  // Value axis scale (value → px from top-left of drawable area)\n  const valueScale =\n    orientation === \"vertical\"\n      ? (v: number) => drawH - ((v - tickLo) / (tickHi - tickLo)) * drawH\n      : (v: number) => ((v - tickLo) / (tickHi - tickLo)) * drawW\n\n  // Category axis — evenly split across the band axis\n  const n = items.length\n  const bandSize = (orientation === \"vertical\" ? drawW : drawH) / n\n  const boxHalf = Math.min(bandSize * 0.28, 32)\n  const centerOf = (i: number) => bandSize * (i + 0.5)\n\n  return (\n    <div\n      className={cn(chartWrapperVariants(), className)}\n      data-slot=\"boxplot-chart\"\n      {...props}\n    >\n      {(title || subtitle) && (\n        <div className={chartHeaderVariants()} data-slot=\"boxplot-chart-header\">\n          {title && <p className={chartTitleVariants()}>{title}</p>}\n          {subtitle && <p className={chartSubtitleVariants()}>{subtitle}</p>}\n        </div>\n      )}\n\n      <div\n        ref={containerRef}\n        className=\"relative w-full\"\n        data-slot=\"boxplot-chart-chart\"\n        style={{ height }}\n      >\n        <svg width={width} height={height} style={{ overflow: \"visible\" }}>\n          <g transform={`translate(${m.left}, ${m.top})`}>\n            {/* Grid & value-axis ticks */}\n            {ticks.map((t) => {\n              const px = valueScale(t)\n              return (\n                <g key={t}>\n                  {showGrid && (\n                    <line\n                      x1={orientation === \"vertical\" ? 0 : px}\n                      y1={orientation === \"vertical\" ? px : 0}\n                      x2={orientation === \"vertical\" ? drawW : px}\n                      y2={orientation === \"vertical\" ? px : drawH}\n                      stroke=\"var(--border)\"\n                      strokeWidth={1}\n                      strokeDasharray=\"3 3\"\n                    />\n                  )}\n                  {orientation === \"vertical\" ? (\n                    <text\n                      x={-8}\n                      y={px + 4}\n                      textAnchor=\"end\"\n                      fontSize={11}\n                      fill=\"var(--muted-foreground)\"\n                    >\n                      {fmt(t)}\n                    </text>\n                  ) : (\n                    <text\n                      x={px}\n                      y={drawH + 18}\n                      textAnchor=\"middle\"\n                      fontSize={11}\n                      fill=\"var(--muted-foreground)\"\n                    >\n                      {fmt(t)}\n                    </text>\n                  )}\n                </g>\n              )\n            })}\n\n            {/* Category labels & boxes */}\n            {items.map((item, i) => {\n              const center = centerOf(i)\n              const isHovered = tooltip?.item.name === item.name\n\n              return (\n                <g key={item.name}>\n                  {/* Category label */}\n                  {orientation === \"vertical\" ? (\n                    <text\n                      x={center}\n                      y={drawH + 18}\n                      textAnchor=\"middle\"\n                      fontSize={11}\n                      fill={\n                        isHovered\n                          ? \"var(--foreground)\"\n                          : \"var(--muted-foreground)\"\n                      }\n                      fontWeight={isHovered ? 600 : 400}\n                    >\n                      {item.name}\n                    </text>\n                  ) : (\n                    <text\n                      x={-8}\n                      y={center + 4}\n                      textAnchor=\"end\"\n                      fontSize={11}\n                      fill={\n                        isHovered\n                          ? \"var(--foreground)\"\n                          : \"var(--muted-foreground)\"\n                      }\n                      fontWeight={isHovered ? 600 : 400}\n                    >\n                      {item.name}\n                    </text>\n                  )}\n\n                  <BoxShape\n                    item={item}\n                    cx={center}\n                    halfWidth={boxHalf}\n                    scale={valueScale}\n                    orientation={orientation}\n                    notched={notched}\n                    showMean={showMean}\n                    showOutliers={showOutliers}\n                    onHover={(it, ev) => {\n                      const rect = containerRef.current?.getBoundingClientRect()\n                      if (!rect) return\n                      setTooltip({\n                        item: it,\n                        left: ev.clientX - rect.left,\n                        top: ev.clientY - rect.top,\n                      })\n                    }}\n                    onLeave={() => setTooltip(null)}\n                    isHovered={isHovered}\n                  />\n                </g>\n              )\n            })}\n\n            {/* Axis border lines */}\n            {orientation === \"vertical\" ? (\n              <>\n                <line\n                  x1={0}\n                  y1={0}\n                  x2={0}\n                  y2={drawH}\n                  stroke=\"var(--border)\"\n                  strokeWidth={1}\n                />\n                <line\n                  x1={0}\n                  y1={drawH}\n                  x2={drawW}\n                  y2={drawH}\n                  stroke=\"var(--border)\"\n                  strokeWidth={1}\n                />\n              </>\n            ) : (\n              <>\n                <line\n                  x1={0}\n                  y1={0}\n                  x2={0}\n                  y2={drawH}\n                  stroke=\"var(--border)\"\n                  strokeWidth={1}\n                />\n                <line\n                  x1={0}\n                  y1={drawH}\n                  x2={drawW}\n                  y2={drawH}\n                  stroke=\"var(--border)\"\n                  strokeWidth={1}\n                />\n              </>\n            )}\n          </g>\n        </svg>\n\n        {/* Floating tooltip */}\n        {tooltip && (\n          <BoxTooltip\n            item={tooltip.item}\n            left={tooltip.left}\n            top={tooltip.top}\n            fmt={fmt}\n            locale={locale}\n          />\n        )}\n      </div>\n\n      {footer && (\n        <div className={chartFooterVariants()} data-slot=\"boxplot-chart-footer\">\n          {footer}\n        </div>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/ds-boxplot-chart.tsx"
    }
  ],
  "type": "registry:ui"
}