"use client";
import { motion, useSpring } from "motion/react";
import { useEffect } from "react";
import { type SpringConfig, useChartConfig } from "../chart-config-context";
import { chartCssVars } from "../chart-context";
import {
type IndicatorFadeEdges,
indicatorFadeGradientStops,
resolveVerticalFadeSides,
} from "../indicator-fade";
export type IndicatorWidth =
| number // Pixel width
| "line" // 1px line (default)
| "thin" // 2px
| "medium" // 4px
| "thick"; // 8px
export interface TooltipIndicatorProps {
/** X position in pixels (center of the indicator) */
x: number;
/** Height of the indicator */
height: number;
/** Whether the indicator is visible */
visible: boolean;
/**
* Width of the indicator - number (pixels) or preset.
* Ignored if `span` is provided.
*/
width?: IndicatorWidth;
/**
* Number of columns/days to span, with current point centered.
* Requires `columnWidth` to be set.
*/
span?: number;
/** Width of a single column/day in pixels. Required when using `span`. */
columnWidth?: number;
/** Primary color at edges (10% and 90%) */
colorEdge?: string;
/** Secondary color at center (50%) */
colorMid?: string;
/** Vertical fade: both ends, top, bottom, or none (solid). */
fadeEdges?: IndicatorFadeEdges | boolean;
/** Fade zone size as a percentage of indicator height. Default: 10 */
fadeLength?: number;
/** Animate position with a spring. Default: true */
animate?: boolean;
/** Unique ID for the gradient */
gradientId?: string;
/** Per-chart override; falls back to `ChartConfigProvider.tooltipSpring`. */
springConfig?: SpringConfig;
/** SVG stroke dash pattern. When set, renders a dashed stroke instead of a solid fill. */
strokeDasharray?: string;
}
function resolveWidth(width: IndicatorWidth): number {
if (typeof width === "number") {
return width;
}
switch (width) {
case "line":
return 1;
case "thin":
return 2;
case "medium":
return 4;
case "thick":
return 8;
default:
return 1;
}
}
// Inner-only-on-visible so `useSpring` initializes at the real cursor x
// instead of 0 on first hover.
export function TooltipIndicator(props: TooltipIndicatorProps) {
if (!props.visible) {
return null;
}
return ;
}
function TooltipIndicatorInner({
x,
visible,
height,
width = "line",
span,
columnWidth,
colorEdge = chartCssVars.crosshair,
colorMid = chartCssVars.crosshair,
fadeEdges = "both",
fadeLength = 10,
animate = true,
gradientId = "tooltip-indicator-gradient",
springConfig,
strokeDasharray,
}: TooltipIndicatorProps) {
const { tooltipSpring } = useChartConfig();
const effectiveSpring = springConfig ?? tooltipSpring;
const pixelWidth =
span !== undefined && columnWidth !== undefined
? span * columnWidth
: resolveWidth(width);
const rectX = x - pixelWidth / 2;
const lineX = x;
const animatedX = useSpring(rectX, effectiveSpring);
const animatedLineX = useSpring(lineX, effectiveSpring);
if (animate) {
animatedX.set(rectX);
animatedLineX.set(lineX);
}
// biome-ignore lint/correctness/useExhaustiveDependencies: we need to jump the animatedX when the visible prop changes
useEffect(() => {
animatedX.set(rectX);
animatedLineX.set(lineX);
}, [animatedLineX, animatedX, lineX, rectX, visible]);
const indicatorFill = colorMid || colorEdge;
const fadeSides = resolveVerticalFadeSides(fadeEdges);
const dashed = Boolean(strokeDasharray);
if (dashed) {
const strokeWidth = Math.max(1, pixelWidth);
return animate ? (
) : (
);
}
if (!fadeSides.any) {
return animate ? (
) : (
);
}
const fadeStops = indicatorFadeGradientStops(fadeSides, fadeLength);
return (
{fadeStops.map((stop) => (
))}
{animate ? (
) : (
)}
);
}
TooltipIndicator.displayName = "TooltipIndicator";
export default TooltipIndicator;