{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "y-axis",
  "type": "registry:component",
  "title": "Y Axis",
  "description": "Y-axis component for value labels in line and area charts",
  "registryDependencies": [
    "@bklit/chart-context",
    "@bklit/utils"
  ],
  "files": [
    {
      "path": "src/charts/y-axis.tsx",
      "content": "\"use client\";\n\nimport { memo, useEffect, useMemo, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useChartStable, useYScale } from \"./chart-context\";\nimport { DEFAULT_Y_DOMAIN_TWEEN_MS } from \"./chart-phase\";\nimport { LINE_LOADING_PULSE_EASE } from \"./line-loading-timing\";\nimport { resolveReferenceDataRange } from \"./reference-area-geometry\";\nimport type { YAxisOrientation } from \"./y-axis-scales\";\nimport { normalizeYAxisId } from \"./y-axis-scales\";\nimport {\n  resolveYAxisTickCount,\n  Y_AXIS_DEFAULT_TICK_COUNT,\n} from \"./y-axis-ticks\";\n\nconst Y_AXIS_POSITION_TWEEN_MS = DEFAULT_Y_DOMAIN_TWEEN_MS;\n\nexport interface YAxisProps {\n  /** Scale group id (Recharts `yAxisId`). Default: `\"left\"`. */\n  yAxisId?: string | number;\n  /** Which side of the chart to render labels. Default: `\"left\"`. */\n  orientation?: YAxisOrientation;\n  /**\n   * Approximate tick count hint for `scale.ticks()` (d3). Actual label count may differ.\n   * Clamped to {@link Y_AXIS_MIN_TICK_COUNT}–{@link Y_AXIS_MAX_TICK_COUNT}. Default: 5.\n   */\n  numTicks?: number;\n  /** Format large numbers (e.g. 1000 as \"1k\"). Default: true */\n  formatLargeNumbers?: boolean;\n  /** Custom formatter for tick labels (e.g. USD). Overrides formatLargeNumbers when set. */\n  formatValue?: (value: number) => string;\n}\n\nfunction formatLabel(\n  value: number,\n  formatLargeNumbers: boolean,\n  formatValue?: (value: number) => string\n): string {\n  if (formatValue) {\n    return formatValue(value);\n  }\n  if (formatLargeNumbers && value >= 1000) {\n    return `${(value / 1000).toFixed(0)}k`;\n  }\n  return String(value);\n}\n\nfunction resolveTickLabelColor(\n  tickY: number,\n  axisId: string,\n  yScale: ReturnType<typeof useYScale>,\n  referenceAreas: ReturnType<typeof useChartStable>[\"referenceAreas\"]\n): string | undefined {\n  for (const area of referenceAreas) {\n    if (!area.axisLabelColor) {\n      continue;\n    }\n    if (normalizeYAxisId(area.yAxisId) !== axisId) {\n      continue;\n    }\n    const [low, high] = resolveReferenceDataRange(\n      area.y1,\n      area.y2,\n      yScale.domain() as [number, number]\n    );\n    const topPixel = yScale(high) ?? 0;\n    const bottomPixel = yScale(low) ?? 0;\n    const bandTop = Math.min(topPixel, bottomPixel);\n    const bandBottom = Math.max(topPixel, bottomPixel);\n    if (tickY >= bandTop && tickY <= bandBottom) {\n      return area.axisLabelColor;\n    }\n  }\n  return undefined;\n}\n\nexport function YAxis(props: YAxisProps) {\n  const { containerRef } = useChartStable();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  const container = containerRef.current;\n  if (!(mounted && container)) {\n    return null;\n  }\n\n  return <YAxisInner {...props} container={container} />;\n}\n\nconst YAxisInner = memo(function YAxisInner({\n  yAxisId,\n  orientation = \"left\",\n  numTicks = Y_AXIS_DEFAULT_TICK_COUNT,\n  formatLargeNumbers = true,\n  formatValue,\n  container,\n}: YAxisProps & { container: HTMLDivElement }) {\n  const { margin, referenceAreas } = useChartStable();\n  const yScale = useYScale(yAxisId);\n  const isLeft = orientation === \"left\";\n  const axisId = normalizeYAxisId(yAxisId);\n\n  const ticks = useMemo(() => {\n    const tickValues = yScale.ticks(resolveYAxisTickCount(numTicks));\n    return tickValues.map((value) => {\n      const y = (yScale(value) ?? 0) + margin.top;\n      return {\n        value,\n        y,\n        label: formatLabel(value, formatLargeNumbers, formatValue),\n        labelColor: resolveTickLabelColor(\n          y - margin.top,\n          axisId,\n          yScale,\n          referenceAreas\n        ),\n      };\n    });\n  }, [\n    yScale,\n    margin.top,\n    numTicks,\n    formatLargeNumbers,\n    formatValue,\n    axisId,\n    referenceAreas,\n  ]);\n\n  return createPortal(\n    <div className=\"pointer-events-none absolute inset-0\">\n      <div\n        className=\"absolute top-0 bottom-0\"\n        style={\n          isLeft\n            ? { left: 0, width: margin.left }\n            : { right: 0, width: margin.right }\n        }\n      >\n        {ticks.map((tick) => (\n          <div\n            className=\"absolute flex items-center\"\n            key={tick.value}\n            style={{\n              top: tick.y,\n              transform: \"translateY(-50%)\",\n              transition: `top ${Y_AXIS_POSITION_TWEEN_MS}ms cubic-bezier(${LINE_LOADING_PULSE_EASE.join(\", \")})`,\n              ...(isLeft\n                ? { right: 0, justifyContent: \"flex-end\", paddingRight: 8 }\n                : { left: 0, justifyContent: \"flex-start\", paddingLeft: 8 }),\n            }}\n          >\n            <span\n              className=\"text-chart-label text-xs\"\n              style={tick.labelColor ? { color: tick.labelColor } : undefined}\n            >\n              {tick.label}\n            </span>\n          </div>\n        ))}\n      </div>\n    </div>,\n    container\n  );\n});\n\nYAxis.displayName = \"YAxis\";\n\nexport default YAxis;\n",
      "type": "registry:component",
      "target": "components/charts/y-axis.tsx"
    },
    {
      "path": "src/charts/reference-area-geometry.ts",
      "content": "export type ReferenceAreaIfOverflow = \"hidden\" | \"visible\" | \"discard\";\n\nexport interface ReferenceAreaRect {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\nexport interface ComputeReferenceAreaRectOptions {\n  innerWidth: number;\n  innerHeight: number;\n  x1?: Date | number;\n  x2?: Date | number;\n  y1?: number;\n  y2?: number;\n  ifOverflow?: ReferenceAreaIfOverflow;\n  xScale: (value: Date) => number;\n  yScale: (value: number) => number;\n}\n\nfunction toDate(value: Date | number): Date {\n  return value instanceof Date ? value : new Date(value);\n}\n\nfunction resolveXPixel(\n  xScale: (value: Date) => number,\n  value: Date | number | undefined,\n  fallback: number\n): number {\n  if (value == null) {\n    return fallback;\n  }\n  return xScale(toDate(value));\n}\n\nfunction resolveYPixel(\n  yScale: (value: number) => number,\n  value: number | undefined,\n  fallback: number\n): number {\n  if (value == null) {\n    return fallback;\n  }\n  return yScale(value);\n}\n\nfunction clampRectToPlot(\n  rect: ReferenceAreaRect,\n  innerWidth: number,\n  innerHeight: number\n): ReferenceAreaRect | null {\n  const x1 = Math.max(0, rect.x);\n  const y1 = Math.max(0, rect.y);\n  const x2 = Math.min(innerWidth, rect.x + rect.width);\n  const y2 = Math.min(innerHeight, rect.y + rect.height);\n  const width = x2 - x1;\n  const height = y2 - y1;\n  if (width <= 0 || height <= 0) {\n    return null;\n  }\n  return { x: x1, y: y1, width, height };\n}\n\nfunction isFullyInsidePlot(\n  rect: ReferenceAreaRect,\n  innerWidth: number,\n  innerHeight: number\n): boolean {\n  return (\n    rect.x >= 0 &&\n    rect.y >= 0 &&\n    rect.x + rect.width <= innerWidth &&\n    rect.y + rect.height <= innerHeight\n  );\n}\n\n/** Map data bounds to plot pixels for a reference rectangle. */\nexport function computeReferenceAreaRect(\n  options: ComputeReferenceAreaRectOptions\n): ReferenceAreaRect | null {\n  const {\n    innerWidth,\n    innerHeight,\n    x1,\n    x2,\n    y1,\n    y2,\n    ifOverflow = \"hidden\",\n    xScale,\n    yScale,\n  } = options;\n\n  if (innerWidth <= 0 || innerHeight <= 0) {\n    return null;\n  }\n\n  const left = resolveXPixel(xScale, x1, 0);\n  const right = resolveXPixel(xScale, x2, innerWidth);\n  const top = resolveYPixel(yScale, y1, 0);\n  const bottom = resolveYPixel(yScale, y2, innerHeight);\n\n  const x = Math.min(left, right);\n  const y = Math.min(top, bottom);\n  const width = Math.abs(right - left);\n  const height = Math.abs(bottom - top);\n\n  if (width <= 0 || height <= 0) {\n    return null;\n  }\n\n  const rect: ReferenceAreaRect = { x, y, width, height };\n\n  if (ifOverflow === \"visible\") {\n    return rect;\n  }\n\n  if (ifOverflow === \"discard\") {\n    return isFullyInsidePlot(rect, innerWidth, innerHeight) ? rect : null;\n  }\n\n  return clampRectToPlot(rect, innerWidth, innerHeight);\n}\n\n/** Inclusive data range for axis label highlighting. */\nexport function resolveReferenceDataRange(\n  y1: number | undefined,\n  y2: number | undefined,\n  domain: [number, number]\n): [number, number] {\n  const dMin = Math.min(domain[0], domain[1]);\n  const dMax = Math.max(domain[0], domain[1]);\n  const low = y1 ?? dMin;\n  const high = y2 ?? dMax;\n  return [Math.min(low, high), Math.max(low, high)];\n}\n",
      "type": "registry:lib",
      "target": "components/charts/reference-area-geometry.ts"
    }
  ]
}