{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-tooltip",
  "type": "registry:component",
  "title": "Chart Tooltip",
  "description": "Composable tooltip components for charts",
  "dependencies": [
    "@number-flow/react",
    "motion"
  ],
  "registryDependencies": [
    "@bklit/chart-context",
    "@bklit/utils"
  ],
  "files": [
    {
      "path": "src/charts/chart-config-context.tsx",
      "content": "\"use client\";\n\nimport { createContext, type ReactNode, useContext, useMemo } from \"react\";\n\nexport interface SpringConfig {\n  stiffness: number;\n  damping: number;\n}\n\nexport interface ChartConfigValue {\n  /** Crosshair indicator, tooltip dot, date pill. */\n  tooltipSpring: SpringConfig;\n  /** Floating tooltip panel. */\n  tooltipBoxSpring: SpringConfig;\n  /** Line/area hover-highlight band (x + width). */\n  highlightSpring: SpringConfig;\n}\n\nexport const DEFAULT_CHART_CONFIG: ChartConfigValue = {\n  tooltipSpring: { stiffness: 300, damping: 30 },\n  tooltipBoxSpring: { stiffness: 100, damping: 20 },\n  highlightSpring: { stiffness: 180, damping: 28 },\n};\n\nconst ChartConfigContext = createContext<ChartConfigValue | null>(null);\n\nexport interface ChartConfigProviderProps {\n  value?: Partial<ChartConfigValue>;\n  children: ReactNode;\n}\n\nexport function ChartConfigProvider({\n  value,\n  children,\n}: ChartConfigProviderProps) {\n  const merged = useMemo<ChartConfigValue>(\n    () => ({\n      ...DEFAULT_CHART_CONFIG,\n      ...value,\n    }),\n    [value]\n  );\n\n  return (\n    <ChartConfigContext.Provider value={merged}>\n      {children}\n    </ChartConfigContext.Provider>\n  );\n}\n\nexport function useChartConfig(): ChartConfigValue {\n  return useContext(ChartConfigContext) ?? DEFAULT_CHART_CONFIG;\n}\n\nconst DEFAULT_TOOLTIP_BOX_DAMPING =\n  DEFAULT_CHART_CONFIG.tooltipBoxSpring.damping;\n\n/** Maps a damping slider to the floating tooltip panel follow spring. `0` = instant. */\nexport function resolveTooltipBoxMotion(damping?: number): {\n  animate: boolean;\n  springConfig: SpringConfig;\n} {\n  if (damping === 0) {\n    return {\n      animate: false,\n      springConfig: DEFAULT_CHART_CONFIG.tooltipBoxSpring,\n    };\n  }\n\n  const effectiveDamping = damping ?? DEFAULT_TOOLTIP_BOX_DAMPING;\n  let stiffness = DEFAULT_CHART_CONFIG.tooltipBoxSpring.stiffness;\n\n  if (effectiveDamping < DEFAULT_TOOLTIP_BOX_DAMPING) {\n    const t =\n      (DEFAULT_TOOLTIP_BOX_DAMPING - effectiveDamping) /\n      DEFAULT_TOOLTIP_BOX_DAMPING;\n    stiffness += t * 400;\n  } else if (effectiveDamping > DEFAULT_TOOLTIP_BOX_DAMPING) {\n    const t =\n      (effectiveDamping - DEFAULT_TOOLTIP_BOX_DAMPING) /\n      (100 - DEFAULT_TOOLTIP_BOX_DAMPING);\n    stiffness -= t * 85;\n  }\n\n  return {\n    animate: true,\n    springConfig: {\n      stiffness: Math.max(12, Math.round(stiffness)),\n      damping: effectiveDamping,\n    },\n  };\n}\n",
      "type": "registry:component",
      "target": "components/charts/chart-config-context.tsx"
    },
    {
      "path": "src/charts/indicator-fade.ts",
      "content": "/** Vertical fade on the tooltip crosshair indicator. */\nexport type IndicatorFadeEdges = \"both\" | \"none\" | \"top\" | \"bottom\";\n\nexport interface VerticalFadeSides {\n  top: boolean;\n  bottom: boolean;\n  any: boolean;\n}\n\nexport function resolveVerticalFadeSides(\n  fade: IndicatorFadeEdges | boolean\n): VerticalFadeSides {\n  if (fade === false || fade === \"none\") {\n    return { top: false, bottom: false, any: false };\n  }\n  if (fade === true || fade === \"both\") {\n    return { top: true, bottom: true, any: true };\n  }\n  if (fade === \"top\") {\n    return { top: true, bottom: false, any: true };\n  }\n  return { top: false, bottom: true, any: true };\n}\n\nexport interface IndicatorFadeGradientStop {\n  offset: string;\n  opacity: number;\n}\n\n/** Opacity stops for the crosshair vertical gradient. */\nexport function indicatorFadeGradientStops(\n  sides: VerticalFadeSides,\n  fadeLengthPercent = 10\n): IndicatorFadeGradientStop[] {\n  const fade = Math.min(40, Math.max(2, fadeLengthPercent));\n  const innerEnd = 100 - fade;\n\n  if (!sides.any) {\n    return [{ offset: \"0%\", opacity: 1 }];\n  }\n\n  if (sides.top && sides.bottom) {\n    return [\n      { offset: \"0%\", opacity: 0 },\n      { offset: `${fade}%`, opacity: 1 },\n      { offset: \"50%\", opacity: 1 },\n      { offset: `${innerEnd}%`, opacity: 1 },\n      { offset: \"100%\", opacity: 0 },\n    ];\n  }\n\n  if (sides.top) {\n    return [\n      { offset: \"0%\", opacity: 0 },\n      { offset: `${fade}%`, opacity: 1 },\n      { offset: \"100%\", opacity: 1 },\n    ];\n  }\n\n  return [\n    { offset: \"0%\", opacity: 1 },\n    { offset: `${innerEnd}%`, opacity: 1 },\n    { offset: \"100%\", opacity: 0 },\n  ];\n}\n",
      "type": "registry:lib",
      "target": "components/charts/indicator-fade.ts"
    },
    {
      "path": "src/charts/tooltip/chart-tooltip.tsx",
      "content": "\"use client\";\n\nimport { motion, useSpring } from \"motion/react\";\nimport { memo, useEffect, useMemo, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n  resolveTooltipBoxMotion,\n  type SpringConfig,\n  useChartConfig,\n} from \"../chart-config-context\";\nimport {\n  chartCssVars,\n  type LineConfig,\n  useChart,\n  useChartStable,\n} from \"../chart-context\";\nimport { weekdayDateFmt } from \"../chart-formatters\";\nimport type { IndicatorFadeEdges } from \"../indicator-fade\";\nimport { DateTicker } from \"./date-ticker\";\nimport { TooltipBox } from \"./tooltip-box\";\nimport { TooltipContent, type TooltipRow } from \"./tooltip-content\";\nimport { TooltipDot } from \"./tooltip-dot\";\nimport { TooltipIndicator } from \"./tooltip-indicator\";\n\nexport interface ChartTooltipProps {\n  /** Whether to show the date pill at bottom. Default: true */\n  showDatePill?: boolean;\n  /** Whether to show the vertical crosshair line. Default: true */\n  showCrosshair?: boolean;\n  /** Whether to show dots on the lines. Default: true */\n  showDots?: boolean;\n  /** Dot style: filled circle or transparent ring. Default: \"dot\" */\n  dotVariant?: \"dot\" | \"ring\";\n  /** Dot / ring radius in pixels. Default: 5 */\n  dotSize?: number;\n  /** Ring corner radius as a fraction of side length (0 = square, 0.5 = circle). */\n  dotRadiusFraction?: number;\n  /** Multiplier applied to the computed dot / ring pixel radius. Default: 1 */\n  dotScale?: number;\n  /** Ring stroke width in pixels. Default: 1.5 for ring variant */\n  dotStrokeWidth?: number;\n  /**\n   * Color for the crosshair/indicator line. When a function, receives the hovered point\n   * (e.g. for candlestick: match candle color from close vs open). Default: --chart-crosshair.\n   */\n  indicatorColor?: string | ((point: Record<string, unknown>) => string);\n  /** Custom content renderer for the tooltip box */\n  content?: (props: {\n    point: Record<string, unknown>;\n    index: number;\n  }) => React.ReactNode;\n  /** Custom row renderer - return array of TooltipRow */\n  rows?: (point: Record<string, unknown>) => TooltipRow[];\n  /**\n   * Override tooltip dot fill. When omitted and `rows` is set, dot colors match row colors.\n   * When a function, receives the hovered point and line config.\n   */\n  dotColor?:\n    | string\n    | ((point: Record<string, unknown>, line: LineConfig) => string);\n  /** Additional content to show below rows (e.g., markers) */\n  children?: React.ReactNode;\n  /** Custom class name */\n  className?: string;\n  /** Per-chart override for the crosshair / dot / date-pill spring. */\n  springConfig?: SpringConfig;\n  /**\n   * When `true`, the floating panel uses the crosshair spring and stays in sync.\n   * Default `false` — panel follow uses `damping` (`20`).\n   */\n  matchCrosshair?: boolean;\n  /**\n   * Spring damping for the floating tooltip panel when `matchCrosshair` is `false`.\n   * `0` disables spring motion (instant). Default: `20`.\n   */\n  damping?: number;\n  /** SVG stroke dash pattern for the crosshair. Omit for solid. */\n  indicatorDasharray?: string;\n  /** Vertical crosshair fade: `both`, `top`, `bottom`, or `none` (solid). Default: `both`. */\n  indicatorFadeEdges?: IndicatorFadeEdges;\n  /** Crosshair fade zone size (% of height). Default: `10`. */\n  indicatorFadeLength?: number;\n  /** Per-chart override for the floating-panel spring. */\n  boxSpringConfig?: SpringConfig;\n  /** Inline styles for the tooltip panel (background, blur, etc.). */\n  panelStyle?: React.CSSProperties;\n  /**\n   * Tooltip panel background color (CSS variable or color value).\n   * Default: `var(--chart-tooltip-background)`.\n   */\n  backgroundColor?: string;\n}\n\ninterface ChartTooltipInnerProps extends ChartTooltipProps {\n  container: HTMLElement;\n}\n\nconst ChartTooltipInner = memo(function ChartTooltipInner({\n  showDatePill = true,\n  showCrosshair = true,\n  showDots = true,\n  dotVariant = \"dot\",\n  dotSize = 5,\n  dotRadiusFraction,\n  dotScale = 1,\n  dotStrokeWidth,\n  indicatorColor: indicatorColorProp,\n  content,\n  rows: rowsRenderer,\n  dotColor: dotColorProp,\n  children,\n  className = \"\",\n  container,\n  springConfig,\n  matchCrosshair = false,\n  damping,\n  indicatorDasharray,\n  indicatorFadeEdges,\n  indicatorFadeLength,\n  boxSpringConfig,\n  panelStyle,\n  backgroundColor,\n}: ChartTooltipInnerProps) {\n  const {\n    tooltipData,\n    width,\n    height,\n    innerHeight,\n    margin,\n    columnWidth,\n    lines,\n    xAccessor,\n    dateLabels,\n    containerRef,\n    orientation,\n    barXAccessor,\n    bandWidth,\n    squareSnap,\n  } = useChart();\n  const { tooltipSpring } = useChartConfig();\n\n  const isHorizontal = orientation === \"horizontal\";\n  const discreteInteraction = dateLabels.length > 60;\n\n  const resolvedDotSize = useMemo(() => {\n    if (dotVariant !== \"ring\" || !bandWidth || lines.length === 0) {\n      return dotSize * dotScale;\n    }\n    const seriesCount = lines.length;\n    const gap = squareSnap?.groupGap ?? (seriesCount > 1 ? 4 : 0);\n    const squareSize = (bandWidth - gap * (seriesCount - 1)) / seriesCount;\n    return (squareSize / 2) * dotScale;\n  }, [\n    bandWidth,\n    dotScale,\n    dotSize,\n    dotVariant,\n    lines.length,\n    squareSnap?.groupGap,\n  ]);\n  const boxMotion = useMemo(() => {\n    if (boxSpringConfig) {\n      return {\n        animate: !discreteInteraction,\n        springConfig: boxSpringConfig,\n      };\n    }\n    if (matchCrosshair) {\n      return {\n        animate: !discreteInteraction,\n        springConfig: springConfig ?? tooltipSpring,\n      };\n    }\n    return resolveTooltipBoxMotion(damping);\n  }, [\n    boxSpringConfig,\n    damping,\n    discreteInteraction,\n    matchCrosshair,\n    springConfig,\n    tooltipSpring,\n  ]);\n\n  const visible = tooltipData !== null;\n  const x = tooltipData?.x ?? 0;\n  const xWithMargin = x + margin.left;\n\n  // For horizontal charts, get the y position from the first line's yPosition (center of bar)\n  const firstLineDataKey = lines[0]?.dataKey;\n  const firstLineY = firstLineDataKey\n    ? (tooltipData?.yPositions[firstLineDataKey] ?? 0)\n    : 0;\n  const yWithMargin = firstLineY + margin.top;\n\n  const tooltipRows = useMemo(() => {\n    if (!tooltipData) {\n      return [];\n    }\n\n    if (rowsRenderer) {\n      return rowsRenderer(tooltipData.point);\n    }\n\n    // Default: generate rows from registered lines\n    return lines.map((line) => ({\n      color: line.stroke,\n      label: line.dataKey,\n      value: (tooltipData.point[line.dataKey] as number) ?? 0,\n    }));\n  }, [tooltipData, lines, rowsRenderer]);\n\n  const resolveDotColor = useMemo(() => {\n    return (line: LineConfig, index: number): string => {\n      if (rowsRenderer && tooltipRows[index]?.color) {\n        return tooltipRows[index].color;\n      }\n      if (dotColorProp != null) {\n        if (typeof dotColorProp === \"function\" && tooltipData) {\n          return dotColorProp(tooltipData.point, line);\n        }\n        if (typeof dotColorProp === \"string\") {\n          return dotColorProp;\n        }\n      }\n      return line.stroke;\n    };\n  }, [dotColorProp, rowsRenderer, tooltipData, tooltipRows]);\n\n  // Resolve indicator color (static or from hovered point)\n  const indicatorColor = useMemo(() => {\n    if (indicatorColorProp == null) {\n      return chartCssVars.crosshair;\n    }\n    if (typeof indicatorColorProp === \"function\") {\n      return tooltipData\n        ? indicatorColorProp(tooltipData.point)\n        : chartCssVars.crosshair;\n    }\n    return indicatorColorProp;\n  }, [indicatorColorProp, tooltipData]);\n\n  // Title from date or category\n  const title = useMemo(() => {\n    if (!tooltipData) {\n      return undefined;\n    }\n    // For bar charts (horizontal or vertical), use the category name\n    if (barXAccessor) {\n      return barXAccessor(tooltipData.point);\n    }\n    // For line/area charts, use the date\n    return weekdayDateFmt.format(xAccessor(tooltipData.point));\n  }, [tooltipData, barXAccessor, xAccessor]);\n\n  const tooltipContent = (\n    <>\n      {/* Crosshair indicator - rendered as SVG overlay */}\n      {showCrosshair && (\n        <svg\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0\"\n          height=\"100%\"\n          width=\"100%\"\n        >\n          <g transform={`translate(${margin.left},${margin.top})`}>\n            <TooltipIndicator\n              animate={!discreteInteraction}\n              colorEdge={indicatorColor}\n              colorMid={indicatorColor}\n              columnWidth={columnWidth}\n              fadeEdges={\n                indicatorDasharray ? \"none\" : (indicatorFadeEdges ?? \"both\")\n              }\n              fadeLength={indicatorFadeLength}\n              height={innerHeight}\n              springConfig={springConfig}\n              strokeDasharray={indicatorDasharray}\n              visible={visible}\n              width=\"line\"\n              x={x}\n            />\n          </g>\n        </svg>\n      )}\n\n      {/* Dots on bars/lines - show for vertical charts only */}\n      {showDots && visible && !isHorizontal && (\n        <svg\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0\"\n          height=\"100%\"\n          width=\"100%\"\n        >\n          <g transform={`translate(${margin.left},${margin.top})`}>\n            {lines.map((line, index) => (\n              <TooltipDot\n                color={resolveDotColor(line, index)}\n                cornerRadiusFraction={\n                  dotVariant === \"ring\" ? dotRadiusFraction : undefined\n                }\n                key={line.dataKey}\n                size={resolvedDotSize}\n                springConfig={springConfig}\n                strokeColor={chartCssVars.background}\n                strokeWidth={dotVariant === \"ring\" ? dotStrokeWidth : undefined}\n                variant={dotVariant}\n                visible={visible}\n                x={tooltipData?.xPositions?.[line.dataKey] ?? x}\n                y={tooltipData?.yPositions[line.dataKey] ?? 0}\n              />\n            ))}\n          </g>\n        </svg>\n      )}\n\n      {/* Tooltip Box */}\n      <TooltipBox\n        animate={boxMotion.animate}\n        backgroundColor={backgroundColor}\n        className={className}\n        containerHeight={height}\n        containerRef={containerRef}\n        containerWidth={width}\n        panelStyle={panelStyle}\n        springConfig={boxMotion.springConfig}\n        top={isHorizontal ? undefined : margin.top}\n        visible={visible}\n        x={xWithMargin}\n        y={isHorizontal ? yWithMargin : margin.top}\n      >\n        {content && tooltipData\n          ? content({\n              point: tooltipData.point,\n              index: tooltipData.index,\n            })\n          : !content && (\n              <TooltipContent rows={tooltipRows} title={title}>\n                {children}\n              </TooltipContent>\n            )}\n      </TooltipBox>\n\n      {/* Date/Category Ticker - only show for vertical charts */}\n      <DatePillTracker\n        currentIndex={tooltipData?.index ?? 0}\n        discreteInteraction={discreteInteraction}\n        enabled={showDatePill && !isHorizontal}\n        labels={dateLabels}\n        springConfig={springConfig}\n        visible={visible}\n        xWithMargin={xWithMargin}\n      />\n    </>\n  );\n\n  return createPortal(tooltipContent, container);\n});\n\nexport function ChartTooltip(props: ChartTooltipProps) {\n  const { containerRef } = useChartStable();\n  const [mounted, setMounted] = useState(false);\n\n  // Only render portals on client side after mount\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  const container = containerRef.current;\n  if (!(mounted && container)) {\n    return null;\n  }\n\n  return <ChartTooltipInner {...props} container={container} />;\n}\n\nChartTooltip.displayName = \"ChartTooltip\";\n\ninterface DatePillTrackerProps {\n  enabled: boolean;\n  visible: boolean;\n  labels: string[];\n  currentIndex: number;\n  xWithMargin: number;\n  discreteInteraction: boolean;\n  springConfig?: SpringConfig;\n}\n\n// Inner-only-on-visible so `useSpring` initializes at the real cursor x\n// instead of `margin.left` on first hover.\nfunction DatePillTracker(props: DatePillTrackerProps) {\n  if (!(props.enabled && props.visible && props.labels.length > 0)) {\n    return null;\n  }\n  return <DatePillTrackerInner {...props} />;\n}\n\nfunction DatePillTrackerInner({\n  labels,\n  currentIndex,\n  xWithMargin,\n  discreteInteraction,\n  springConfig,\n  visible,\n}: DatePillTrackerProps) {\n  const { tooltipSpring } = useChartConfig();\n  const effectiveSpring = springConfig ?? tooltipSpring;\n  const animatedX = useSpring(xWithMargin, effectiveSpring);\n\n  if (!discreteInteraction) {\n    animatedX.set(xWithMargin);\n  }\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: we need to jump the animatedX when the visible prop changes\n  useEffect(() => {\n    animatedX.set(xWithMargin);\n  }, [animatedX, visible]);\n\n  return (\n    <motion.div\n      className=\"pointer-events-none absolute z-50\"\n      style={{\n        left: discreteInteraction ? xWithMargin : animatedX,\n        transform: \"translateX(-50%)\",\n        bottom: 4,\n      }}\n    >\n      <DateTicker\n        currentIndex={currentIndex}\n        labels={labels}\n        visible={visible}\n      />\n    </motion.div>\n  );\n}\n\nexport default ChartTooltip;\n",
      "type": "registry:component",
      "target": "components/charts/tooltip/chart-tooltip.tsx"
    },
    {
      "path": "src/charts/tooltip/tooltip-box.tsx",
      "content": "\"use client\";\n\nimport { motion, useSpring } from \"motion/react\";\nimport type { RefObject } from \"react\";\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/lib/utils\";\nimport { type SpringConfig, useChartConfig } from \"../chart-config-context\";\nimport { chartCssVars } from \"../chart-context\";\n\nexport interface TooltipBoxProps {\n  /** X position in pixels (relative to container) */\n  x: number;\n  /** Y position in pixels (relative to container) */\n  y: number;\n  /** Whether the tooltip is visible */\n  visible: boolean;\n  /** Container ref for portal rendering */\n  containerRef: RefObject<HTMLDivElement | null>;\n  /** Container width for flip detection */\n  containerWidth: number;\n  /** Container height for bounds clamping */\n  containerHeight: number;\n  /** Offset from the target position */\n  offset?: number;\n  /** Custom class name */\n  className?: string;\n  /** Tooltip content */\n  children: React.ReactNode;\n  /** Override left position (bypasses internal calculation) */\n  left?: number | ReturnType<typeof useSpring>;\n  /** Override top position (bypasses internal calculation) */\n  top?: number | ReturnType<typeof useSpring>;\n  /** Force flip direction (for custom positioning) */\n  flipped?: boolean;\n  /** Per-chart override; falls back to `ChartConfigProvider.tooltipBoxSpring`. */\n  springConfig?: SpringConfig;\n  /** Animate panel position with a spring. Default: true */\n  animate?: boolean;\n  /** Fade/scale the panel on show. Default: true */\n  entrance?: boolean;\n  /** Inline styles for the inner tooltip panel. */\n  panelStyle?: React.CSSProperties;\n  /**\n   * Tooltip panel background color (CSS variable or color value).\n   * Default: `var(--chart-tooltip-background)`.\n   */\n  backgroundColor?: string;\n}\n\n// Inner-only-on-visible so `useSpring` initializes at the cursor's actual x/y\n// instead of (0, 0) on first hover.\nexport function TooltipBox(props: TooltipBoxProps) {\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  const container = props.containerRef.current;\n  if (!(mounted && container)) {\n    return null;\n  }\n  if (!props.visible) {\n    return null;\n  }\n  return <TooltipBoxInner {...props} container={container} />;\n}\n\nfunction TooltipBoxInner({\n  x,\n  y,\n  containerWidth,\n  containerHeight,\n  offset = 16,\n  className = \"\",\n  children,\n  left: leftOverride,\n  top: topOverride,\n  flipped: flippedOverride,\n  springConfig,\n  animate = true,\n  entrance = true,\n  panelStyle,\n  backgroundColor = chartCssVars.tooltipBackground,\n  container,\n}: Omit<TooltipBoxProps, \"visible\" | \"containerRef\"> & {\n  container: HTMLElement;\n}) {\n  const { tooltipBoxSpring } = useChartConfig();\n  const effectiveSpring = springConfig ?? tooltipBoxSpring;\n\n  const tooltipRef = useRef<HTMLDivElement>(null);\n  const tooltipWidthRef = useRef(180);\n  const tooltipHeightRef = useRef(80);\n  const [staticPosition, setStaticPosition] = useState({ left: x, top: y });\n\n  const tw = tooltipWidthRef.current;\n  const th = tooltipHeightRef.current;\n  const shouldFlipX = x + tw + offset > containerWidth;\n  const targetX = shouldFlipX ? x - offset - tw : x + offset;\n  const targetY = Math.max(\n    offset,\n    Math.min(y - th / 2, containerHeight - th - offset)\n  );\n\n  const animatedLeft = useSpring(targetX, effectiveSpring);\n  const animatedTop = useSpring(targetY, effectiveSpring);\n\n  if (animate && leftOverride === undefined) {\n    animatedLeft.set(targetX);\n  }\n  if (animate && topOverride === undefined) {\n    animatedTop.set(targetY);\n  }\n\n  useLayoutEffect(() => {\n    if (!tooltipRef.current) {\n      return;\n    }\n    const el = tooltipRef.current;\n    const w = el.offsetWidth;\n    const h = el.offsetHeight;\n    if (w > 0) {\n      tooltipWidthRef.current = w;\n    }\n    if (h > 0) {\n      tooltipHeightRef.current = h;\n    }\n    const w2 = tooltipWidthRef.current;\n    const h2 = tooltipHeightRef.current;\n    const flip = x + w2 + offset > containerWidth;\n    const tx = flip ? x - offset - w2 : x + offset;\n    const ty = Math.max(\n      offset,\n      Math.min(y - h2 / 2, containerHeight - h2 - offset)\n    );\n    if (!animate) {\n      setStaticPosition({ left: tx, top: ty });\n      return;\n    }\n    if (leftOverride === undefined) {\n      animatedLeft.set(tx);\n    }\n    if (topOverride === undefined) {\n      animatedTop.set(ty);\n    }\n  }, [\n    x,\n    y,\n    containerWidth,\n    containerHeight,\n    offset,\n    leftOverride,\n    topOverride,\n    animate,\n    animatedLeft,\n    animatedTop,\n  ]);\n\n  const prevFlipRef = useRef(shouldFlipX);\n  const [flipKey, setFlipKey] = useState(0);\n\n  useEffect(() => {\n    if (prevFlipRef.current !== shouldFlipX) {\n      setFlipKey((k) => k + 1);\n      prevFlipRef.current = shouldFlipX;\n    }\n  }, [shouldFlipX]);\n\n  const finalLeft = animate\n    ? (leftOverride ?? animatedLeft)\n    : staticPosition.left;\n  const finalTop = animate ? (topOverride ?? animatedTop) : staticPosition.top;\n  const isFlipped = flippedOverride ?? shouldFlipX;\n  const transformOrigin = isFlipped ? \"right top\" : \"left top\";\n\n  const panelClassName = cn(\n    \"min-w-[140px] overflow-hidden rounded-lg text-chart-tooltip-foreground shadow-lg\",\n    panelStyle?.backgroundColor === undefined &&\n      backgroundColor === chartCssVars.tooltipBackground &&\n      \"bg-chart-tooltip-background\",\n    panelStyle?.backdropFilter === undefined && \"backdrop-blur-md\"\n  );\n  const panelStyleResolved = {\n    transformOrigin,\n    ...(panelStyle?.backgroundColor === undefined && {\n      backgroundColor,\n    }),\n    ...panelStyle,\n  };\n\n  if (!entrance) {\n    return createPortal(\n      <div\n        className={cn(\"pointer-events-none absolute z-50\", className)}\n        ref={tooltipRef}\n        style={{ left: staticPosition.left, top: staticPosition.top }}\n      >\n        <div className={panelClassName} style={panelStyleResolved}>\n          {children}\n        </div>\n      </div>,\n      container\n    );\n  }\n\n  return createPortal(\n    <motion.div\n      animate={{ opacity: 1 }}\n      className={cn(\"pointer-events-none absolute z-50\", className)}\n      exit={{ opacity: 0 }}\n      initial={{ opacity: 0 }}\n      ref={tooltipRef}\n      style={{ left: finalLeft, top: finalTop }}\n      transition={{ duration: 0.1 }}\n    >\n      <motion.div\n        animate={{ scale: 1, opacity: 1, x: 0 }}\n        className={panelClassName}\n        initial={{ scale: 0.85, opacity: 0, x: isFlipped ? 20 : -20 }}\n        key={flipKey}\n        style={panelStyleResolved}\n        transition={{ type: \"spring\", stiffness: 300, damping: 25 }}\n      >\n        {children}\n      </motion.div>\n    </motion.div>,\n    container\n  );\n}\n\nTooltipBox.displayName = \"TooltipBox\";\n\nexport default TooltipBox;\n",
      "type": "registry:component",
      "target": "components/charts/tooltip/tooltip-box.tsx"
    },
    {
      "path": "src/charts/tooltip/tooltip-content.tsx",
      "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { intFmt } from \"../chart-formatters\";\n\nexport interface TooltipRow {\n  color: string;\n  label: string;\n  value: string | number;\n}\n\nexport interface TooltipContentProps {\n  title?: string;\n  rows: TooltipRow[];\n  /** Optional additional content (e.g., markers) */\n  children?: ReactNode;\n}\n\nexport function TooltipContent({ title, rows, children }: TooltipContentProps) {\n  return (\n    <div className=\"overflow-hidden\">\n      <div className=\"px-3 py-2.5\">\n        {title && (\n          <div className=\"mb-2 text-left font-medium text-chart-tooltip-foreground text-xs\">\n            {title}\n          </div>\n        )}\n        <div className=\"space-y-1.5\">\n          {rows.map((row) => (\n            <div\n              className=\"flex items-center justify-between gap-4\"\n              key={`${row.label}-${row.color}`}\n            >\n              <div className=\"flex items-center gap-2\">\n                <span\n                  className=\"h-2.5 w-2.5 shrink-0 rounded-full\"\n                  style={{ backgroundColor: row.color }}\n                />\n                <span className=\"text-chart-tooltip-muted text-sm\">\n                  {row.label}\n                </span>\n              </div>\n              <span className=\"font-medium text-chart-tooltip-foreground text-sm tabular-nums\">\n                {typeof row.value === \"number\" ? intFmt(row.value) : row.value}\n              </span>\n            </div>\n          ))}\n        </div>\n\n        {children && (\n          <div className=\"mt-2 transition-opacity duration-200 ease-out\">\n            {children}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nTooltipContent.displayName = \"TooltipContent\";\n\nexport default TooltipContent;\n",
      "type": "registry:component",
      "target": "components/charts/tooltip/tooltip-content.tsx"
    },
    {
      "path": "src/charts/tooltip/tooltip-dot.tsx",
      "content": "\"use client\";\n\nimport { motion, useSpring, useTransform } from \"motion/react\";\nimport { type SpringConfig, useChartConfig } from \"../chart-config-context\";\nimport { chartCssVars } from \"../chart-context\";\n\nexport interface TooltipDotProps {\n  x: number;\n  y: number;\n  visible: boolean;\n  color: string;\n  /** Half of width/height for dots; half-extent for ring squares. Default: 5 */\n  size?: number;\n  strokeColor?: string;\n  strokeWidth?: number;\n  /** Dot fill or transparent ring around the hovered mark. Default: \"dot\" */\n  variant?: \"dot\" | \"ring\";\n  /**\n   * Ring corner radius as a fraction of side length (0 = square, 0.5 = circle).\n   * Same semantics as bar square radius.\n   */\n  cornerRadiusFraction?: number;\n  /** Per-chart override; falls back to `ChartConfigProvider.tooltipSpring`. */\n  springConfig?: SpringConfig;\n  /** Animate position with a spring. Default: true */\n  animate?: boolean;\n}\n\nfunction ringCornerRadius(\n  halfExtent: number,\n  cornerRadiusFraction: number\n): number {\n  const side = halfExtent * 2;\n  return side * Math.max(0, Math.min(0.5, cornerRadiusFraction));\n}\n\nfunction AnimatedRingDot({\n  x,\n  y,\n  halfExtent,\n  cornerRadiusFraction,\n  fill,\n  stroke,\n  strokeWidth,\n  springConfig,\n}: {\n  x: number;\n  y: number;\n  halfExtent: number;\n  cornerRadiusFraction: number;\n  fill: string;\n  stroke: string;\n  strokeWidth: number;\n  springConfig?: SpringConfig;\n}) {\n  const { tooltipSpring } = useChartConfig();\n  const effectiveSpring = springConfig ?? tooltipSpring;\n  const animatedX = useSpring(x, effectiveSpring);\n  const animatedY = useSpring(y, effectiveSpring);\n  const side = halfExtent * 2;\n  const rx = ringCornerRadius(halfExtent, cornerRadiusFraction);\n  const rectX = useTransform(animatedX, (value) => value - halfExtent);\n  const rectY = useTransform(animatedY, (value) => value - halfExtent);\n\n  animatedX.set(x);\n  animatedY.set(y);\n\n  return (\n    <motion.rect\n      fill={fill}\n      height={side}\n      rx={rx}\n      ry={rx}\n      stroke={stroke}\n      strokeWidth={strokeWidth}\n      width={side}\n      x={rectX}\n      y={rectY}\n    />\n  );\n}\n\nexport function TooltipDot({\n  x,\n  y,\n  visible,\n  color,\n  size = 5,\n  strokeColor = chartCssVars.background,\n  strokeWidth = 2,\n  variant = \"dot\",\n  cornerRadiusFraction = 0.25,\n  springConfig,\n  animate = true,\n}: TooltipDotProps) {\n  const { tooltipSpring } = useChartConfig();\n  const effectiveSpring = springConfig ?? tooltipSpring;\n  const animatedX = useSpring(x, effectiveSpring);\n  const animatedY = useSpring(y, effectiveSpring);\n\n  const isRing = variant === \"ring\";\n  const fill = isRing ? \"transparent\" : color;\n  const stroke = isRing ? color : strokeColor;\n  const effectiveStrokeWidth = isRing ? (strokeWidth ?? 1.5) : strokeWidth;\n\n  if (animate && !isRing) {\n    animatedX.set(x);\n    animatedY.set(y);\n  }\n\n  if (!visible) {\n    return null;\n  }\n\n  if (isRing) {\n    if (animate) {\n      return (\n        <AnimatedRingDot\n          cornerRadiusFraction={cornerRadiusFraction}\n          fill={fill}\n          halfExtent={size}\n          springConfig={springConfig}\n          stroke={stroke}\n          strokeWidth={effectiveStrokeWidth}\n          x={x}\n          y={y}\n        />\n      );\n    }\n\n    const side = size * 2;\n    const rx = ringCornerRadius(size, cornerRadiusFraction);\n\n    return (\n      <rect\n        fill={fill}\n        height={side}\n        rx={rx}\n        ry={rx}\n        stroke={stroke}\n        strokeWidth={effectiveStrokeWidth}\n        width={side}\n        x={x - size}\n        y={y - size}\n      />\n    );\n  }\n\n  if (!animate) {\n    return (\n      <circle\n        cx={x}\n        cy={y}\n        fill={fill}\n        r={size}\n        stroke={stroke}\n        strokeWidth={effectiveStrokeWidth}\n      />\n    );\n  }\n\n  return (\n    <motion.circle\n      cx={animatedX}\n      cy={animatedY}\n      fill={fill}\n      r={size}\n      stroke={stroke}\n      strokeWidth={effectiveStrokeWidth}\n    />\n  );\n}\n\nTooltipDot.displayName = \"TooltipDot\";\n\nexport default TooltipDot;\n",
      "type": "registry:component",
      "target": "components/charts/tooltip/tooltip-dot.tsx"
    },
    {
      "path": "src/charts/tooltip/tooltip-indicator.tsx",
      "content": "\"use client\";\n\nimport { motion, useSpring } from \"motion/react\";\nimport { useEffect } from \"react\";\nimport { type SpringConfig, useChartConfig } from \"../chart-config-context\";\nimport { chartCssVars } from \"../chart-context\";\nimport {\n  type IndicatorFadeEdges,\n  indicatorFadeGradientStops,\n  resolveVerticalFadeSides,\n} from \"../indicator-fade\";\n\nexport type IndicatorWidth =\n  | number // Pixel width\n  | \"line\" // 1px line (default)\n  | \"thin\" // 2px\n  | \"medium\" // 4px\n  | \"thick\"; // 8px\n\nexport interface TooltipIndicatorProps {\n  /** X position in pixels (center of the indicator) */\n  x: number;\n  /** Height of the indicator */\n  height: number;\n  /** Whether the indicator is visible */\n  visible: boolean;\n  /**\n   * Width of the indicator - number (pixels) or preset.\n   * Ignored if `span` is provided.\n   */\n  width?: IndicatorWidth;\n  /**\n   * Number of columns/days to span, with current point centered.\n   * Requires `columnWidth` to be set.\n   */\n  span?: number;\n  /** Width of a single column/day in pixels. Required when using `span`. */\n  columnWidth?: number;\n  /** Primary color at edges (10% and 90%) */\n  colorEdge?: string;\n  /** Secondary color at center (50%) */\n  colorMid?: string;\n  /** Vertical fade: both ends, top, bottom, or none (solid). */\n  fadeEdges?: IndicatorFadeEdges | boolean;\n  /** Fade zone size as a percentage of indicator height. Default: 10 */\n  fadeLength?: number;\n  /** Animate position with a spring. Default: true */\n  animate?: boolean;\n  /** Unique ID for the gradient */\n  gradientId?: string;\n  /** Per-chart override; falls back to `ChartConfigProvider.tooltipSpring`. */\n  springConfig?: SpringConfig;\n  /** SVG stroke dash pattern. When set, renders a dashed stroke instead of a solid fill. */\n  strokeDasharray?: string;\n}\n\nfunction resolveWidth(width: IndicatorWidth): number {\n  if (typeof width === \"number\") {\n    return width;\n  }\n  switch (width) {\n    case \"line\":\n      return 1;\n    case \"thin\":\n      return 2;\n    case \"medium\":\n      return 4;\n    case \"thick\":\n      return 8;\n    default:\n      return 1;\n  }\n}\n\n// Inner-only-on-visible so `useSpring` initializes at the real cursor x\n// instead of 0 on first hover.\nexport function TooltipIndicator(props: TooltipIndicatorProps) {\n  if (!props.visible) {\n    return null;\n  }\n  return <TooltipIndicatorInner {...props} />;\n}\n\nfunction TooltipIndicatorInner({\n  x,\n  visible,\n  height,\n  width = \"line\",\n  span,\n  columnWidth,\n  colorEdge = chartCssVars.crosshair,\n  colorMid = chartCssVars.crosshair,\n  fadeEdges = \"both\",\n  fadeLength = 10,\n  animate = true,\n  gradientId = \"tooltip-indicator-gradient\",\n  springConfig,\n  strokeDasharray,\n}: TooltipIndicatorProps) {\n  const { tooltipSpring } = useChartConfig();\n  const effectiveSpring = springConfig ?? tooltipSpring;\n\n  const pixelWidth =\n    span !== undefined && columnWidth !== undefined\n      ? span * columnWidth\n      : resolveWidth(width);\n\n  const rectX = x - pixelWidth / 2;\n  const lineX = x;\n  const animatedX = useSpring(rectX, effectiveSpring);\n  const animatedLineX = useSpring(lineX, effectiveSpring);\n\n  if (animate) {\n    animatedX.set(rectX);\n    animatedLineX.set(lineX);\n  }\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: we need to jump the animatedX when the visible prop changes\n  useEffect(() => {\n    animatedX.set(rectX);\n    animatedLineX.set(lineX);\n  }, [animatedLineX, animatedX, lineX, rectX, visible]);\n\n  const indicatorFill = colorMid || colorEdge;\n  const fadeSides = resolveVerticalFadeSides(fadeEdges);\n  const dashed = Boolean(strokeDasharray);\n\n  if (dashed) {\n    const strokeWidth = Math.max(1, pixelWidth);\n    return animate ? (\n      <motion.line\n        stroke={indicatorFill}\n        strokeDasharray={strokeDasharray}\n        strokeWidth={strokeWidth}\n        x1={animatedLineX}\n        x2={animatedLineX}\n        y1={0}\n        y2={height}\n      />\n    ) : (\n      <line\n        stroke={indicatorFill}\n        strokeDasharray={strokeDasharray}\n        strokeWidth={strokeWidth}\n        x1={lineX}\n        x2={lineX}\n        y1={0}\n        y2={height}\n      />\n    );\n  }\n\n  if (!fadeSides.any) {\n    return animate ? (\n      <motion.rect\n        fill={indicatorFill}\n        height={height}\n        width={pixelWidth}\n        x={animatedX}\n        y={0}\n      />\n    ) : (\n      <rect\n        fill={indicatorFill}\n        height={height}\n        width={pixelWidth}\n        x={rectX}\n        y={0}\n      />\n    );\n  }\n\n  const fadeStops = indicatorFadeGradientStops(fadeSides, fadeLength);\n\n  return (\n    <g>\n      <defs>\n        <linearGradient id={gradientId} x1=\"0%\" x2=\"0%\" y1=\"0%\" y2=\"100%\">\n          {fadeStops.map((stop) => (\n            <stop\n              key={stop.offset}\n              offset={stop.offset}\n              style={{ stopColor: indicatorFill, stopOpacity: stop.opacity }}\n            />\n          ))}\n        </linearGradient>\n      </defs>\n      {animate ? (\n        <motion.rect\n          fill={`url(#${gradientId})`}\n          height={height}\n          width={pixelWidth}\n          x={animatedX}\n          y={0}\n        />\n      ) : (\n        <rect\n          fill={`url(#${gradientId})`}\n          height={height}\n          width={pixelWidth}\n          x={rectX}\n          y={0}\n        />\n      )}\n    </g>\n  );\n}\n\nTooltipIndicator.displayName = \"TooltipIndicator\";\n\nexport default TooltipIndicator;\n",
      "type": "registry:component",
      "target": "components/charts/tooltip/tooltip-indicator.tsx"
    },
    {
      "path": "src/charts/tooltip/date-ticker.tsx",
      "content": "\"use client\";\n\nimport { motion, useSpring } from \"motion/react\";\nimport { memo, useMemo, useRef } from \"react\";\n\nconst TICKER_ITEM_HEIGHT = 24;\n/** Full scroll stacks are skipped above this count — single label + instant updates. */\nconst COMPACT_TICKER_THRESHOLD = 60;\n\nexport interface DateTickerProps {\n  currentIndex: number;\n  labels: string[];\n  visible: boolean;\n}\n\nconst DateTickerCompact = memo(function DateTickerCompact({\n  currentIndex,\n  labels,\n}: Omit<DateTickerProps, \"visible\">) {\n  const label = labels[currentIndex] ?? labels[0] ?? \"\";\n\n  return (\n    <div className=\"overflow-hidden rounded-full bg-zinc-900 px-4 py-1 text-white shadow-lg dark:bg-zinc-100 dark:text-zinc-900\">\n      <div className=\"flex h-6 items-center justify-center\">\n        <span className=\"whitespace-nowrap font-medium text-sm\">{label}</span>\n      </div>\n    </div>\n  );\n});\n\nconst DateTickerInner = memo(function DateTickerInner({\n  currentIndex,\n  labels,\n}: Omit<DateTickerProps, \"visible\">) {\n  // Parse labels into month and day parts\n  const parsedLabels = useMemo(() => {\n    return labels.map((label, index) => {\n      const parts = label.split(\" \");\n      const month = parts[0] || \"\";\n      const day = parts[1] || \"\";\n      return { month, day, full: label, key: `${label}::${index}` };\n    });\n  }, [labels]);\n\n  // Month segments: one entry per consecutive run (Jan → Feb → …), keyed by start index\n  const monthSegments = useMemo(() => {\n    const segments: { month: string; key: string; startIndex: number }[] = [];\n\n    parsedLabels.forEach((label, index) => {\n      const prev = segments.at(-1);\n      if (!prev || prev.month !== label.month) {\n        segments.push({\n          month: label.month,\n          key: `${label.month}-${index}`,\n          startIndex: index,\n        });\n      }\n    });\n\n    return segments;\n  }, [parsedLabels]);\n\n  // Index into monthSegments for the current data point\n  const currentMonthIndex = useMemo(() => {\n    if (currentIndex < 0 || currentIndex >= parsedLabels.length) {\n      return 0;\n    }\n    for (let i = monthSegments.length - 1; i >= 0; i--) {\n      const segment = monthSegments[i];\n      if (segment && segment.startIndex <= currentIndex) {\n        return i;\n      }\n    }\n    return 0;\n  }, [currentIndex, parsedLabels.length, monthSegments]);\n\n  // Track previous month index\n  const prevMonthIndexRef = useRef(-1);\n\n  // Animated Y offsets\n  const dayY = useSpring(0, { stiffness: 400, damping: 35 });\n  const monthY = useSpring(0, { stiffness: 400, damping: 35 });\n\n  dayY.set(-currentIndex * TICKER_ITEM_HEIGHT);\n\n  if (currentMonthIndex >= 0) {\n    const isFirstRender = prevMonthIndexRef.current === -1;\n    const monthChanged = prevMonthIndexRef.current !== currentMonthIndex;\n    if (isFirstRender || monthChanged) {\n      monthY.set(-currentMonthIndex * TICKER_ITEM_HEIGHT);\n      prevMonthIndexRef.current = currentMonthIndex;\n    }\n  }\n\n  return (\n    <div className=\"overflow-hidden rounded-full bg-zinc-900 px-4 py-1 text-white shadow-lg dark:bg-zinc-100 dark:text-zinc-900\">\n      <div className=\"relative h-6 overflow-hidden\">\n        <div className=\"flex items-center justify-center gap-1\">\n          {/* Month stack */}\n          <div className=\"relative h-6 overflow-hidden\">\n            <motion.div className=\"flex flex-col\" style={{ y: monthY }}>\n              {monthSegments.map((segment) => (\n                <div\n                  className=\"flex h-6 shrink-0 items-center justify-center\"\n                  key={segment.key}\n                >\n                  <span className=\"whitespace-nowrap font-medium text-sm\">\n                    {segment.month}\n                  </span>\n                </div>\n              ))}\n            </motion.div>\n          </div>\n\n          {/* Day stack */}\n          <div className=\"relative h-6 overflow-hidden\">\n            <motion.div className=\"flex flex-col\" style={{ y: dayY }}>\n              {parsedLabels.map((label) => (\n                <div\n                  className=\"flex h-6 shrink-0 items-center justify-center\"\n                  key={label.key}\n                >\n                  <span className=\"whitespace-nowrap font-medium text-sm\">\n                    {label.day}\n                  </span>\n                </div>\n              ))}\n            </motion.div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n});\n\nexport function DateTicker({ currentIndex, labels, visible }: DateTickerProps) {\n  if (!visible || labels.length === 0) {\n    return null;\n  }\n\n  if (labels.length > COMPACT_TICKER_THRESHOLD) {\n    return <DateTickerCompact currentIndex={currentIndex} labels={labels} />;\n  }\n\n  return <DateTickerInner currentIndex={currentIndex} labels={labels} />;\n}\n\nDateTicker.displayName = \"DateTicker\";\n\nexport default DateTicker;\n",
      "type": "registry:component",
      "target": "components/charts/tooltip/date-ticker.tsx"
    },
    {
      "path": "src/charts/tooltip/index.ts",
      "content": "export { ChartTooltip, type ChartTooltipProps } from \"./chart-tooltip\";\nexport { DateTicker, type DateTickerProps } from \"./date-ticker\";\nexport { TooltipBox, type TooltipBoxProps } from \"./tooltip-box\";\nexport {\n  TooltipContent,\n  type TooltipContentProps,\n  type TooltipRow,\n} from \"./tooltip-content\";\nexport { TooltipDot, type TooltipDotProps } from \"./tooltip-dot\";\nexport {\n  type IndicatorWidth,\n  TooltipIndicator,\n  type TooltipIndicatorProps,\n} from \"./tooltip-indicator\";\n",
      "type": "registry:component",
      "target": "components/charts/tooltip/index.ts"
    }
  ]
}