{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "recharts-line-chart",
  "description": "Line chart component",
  "dependencies": [
    "recharts",
    "motion"
  ],
  "registryDependencies": [
    "@evilcharts/recharts-chart",
    "@evilcharts/recharts-tooltip",
    "@evilcharts/recharts-legend",
    "@evilcharts/recharts-dot",
    "@evilcharts/recharts-brush",
    "@evilcharts/recharts-background"
  ],
  "files": [
    {
      "path": "src/registry/charts/recharts-line-chart.tsx",
      "content": "\"use client\";\n\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  use,\n  useCallback,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type FC,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport {\n  CartesianGrid,\n  Curve,\n  Line as RechartsLine,\n  LineChart as RechartsLineChart,\n  XAxis as RechartsXAxis,\n  YAxis as RechartsYAxis,\n  type CurveProps,\n} from \"recharts\";\nimport {\n  type ChartConfig,\n  ChartContainer,\n  getColorsCount,\n  getLoadingData,\n  LoadingIndicator,\n} from \"@/registry/ui/recharts-chart\";\nimport {\n  ChartTooltip,\n  ChartTooltipContent,\n  type TooltipRoundness,\n  type TooltipVariant,\n} from \"@/registry/ui/recharts-tooltip\";\nimport {\n  Brush,\n  EvilBrush,\n  useEvilBrush,\n  type BrushProps,\n  type EvilBrushRange,\n} from \"@/registry/ui/recharts-brush\";\nimport {\n  ChartLegend,\n  ChartLegendContent,\n  type ChartLegendVariant,\n} from \"@/registry/ui/recharts-legend\";\nimport { ChartDot, type DotVariant } from \"@/registry/ui/recharts-dot\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\n// Constants\nconst STROKE_WIDTH = 0.8; // default series stroke — <Line strokeWidth> overrides it\nconst LOADING_LINE_DATA_KEY = \"loading\";\nconst LOADING_ANIMATION_DURATION = 2000; // in milliseconds\nconst REVEAL_DURATION = 1; // intro wipe length, in seconds\nconst REVEAL_EASE: [number, number, number, number] = [0, 0.7, 0.5, 1]; // intro wipe easing\n\ntype CurveType = ComponentProps<typeof RechartsLine>[\"type\"];\ntype LineDotProp = ComponentProps<typeof RechartsLine>[\"dot\"];\ntype LineActiveDotProp = ComponentProps<typeof RechartsLine>[\"activeDot\"];\ntype StrokeVariant = \"solid\" | \"dashed\" | \"animated-dashed\";\n\n/**\n * Direction of the custom motion.dev intro reveal. Recharts' own line animation\n * is permanently disabled (it drew the line after the dots had already popped\n * in) — these reveals replace it.\n *\n * NOTE: a reveal is a per-frame animated SVG mask, so it is heavier than a\n * static chart. `\"none\"` opts out entirely; it is also what a device with the\n * OS \"reduce motion\" preference falls back to automatically.\n */\ntype LineAnimationType = \"none\" | \"left-to-right\" | \"right-to-left\" | \"center-out\" | \"edges-in\";\ntype RevealAnimationType = Exclude<LineAnimationType, \"none\">;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Shared context\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Shared state for every part of the chart. Lifted into <EvilLineChart /> so that\n * <Line />, <XAxis />, <Legend />, and friends can read it without prop drilling.\n * Sub-components are composed freely — the provider is the single source of truth.\n */\ntype LineChartContextValue = {\n  config: ChartConfig; // colors + labels for every series\n  curveType: CurveType; // default curve interpolation each <Line /> inherits\n  animationType: LineAnimationType; // default intro reveal each <Line /> inherits\n  isLoading: boolean; // whether the chart shows its loading skeleton\n  selectedDataKey: string | null; // currently selected series, or null when none\n  selectDataKey: (dataKey: string | null) => void; // sets the selected series\n};\n\nconst LineChartContext = createContext<LineChartContextValue | null>(null);\n\n// Reads the chart context, throwing a helpful error when used outside <EvilLineChart />\nfunction useLineChart() {\n  const context = use(LineChartContext);\n\n  if (!context) {\n    throw new Error(\n      \"Line chart parts (<Line />, <XAxis />, …) must be used within <EvilLineChart />\",\n    );\n  }\n\n  return context;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Root container\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Validates that every config key also exists on the data row type\ntype ValidateConfigKeys<TData, TConfig> = {\n  [K in keyof TConfig]: K extends keyof TData ? ChartConfig[string] : never;\n};\n\ntype EvilLineChartBaseProps<\n  TData extends Record<string, unknown>,\n  TConfig extends Record<string, ChartConfig[string]>,\n> = {\n  config: TConfig & ValidateConfigKeys<TData, TConfig>; // series colors + labels\n  data: TData[]; // rows rendered by the chart\n  children: ReactNode; // composed parts — <Line />, <XAxis />, <Legend />, …\n  className?: string; // extra classes for the chart container\n  chartProps?: ComponentProps<typeof RechartsLineChart>; // escape hatch for the raw Recharts chart\n  curveType?: CurveType; // default curve interpolation for every <Line />\n  animationType?: LineAnimationType; // default intro reveal for every <Line />\n  defaultSelectedDataKey?: string | null; // series selected on first render\n  onSelectionChange?: (selectedDataKey: string | null) => void; // fires when the selected series changes\n  isLoading?: boolean; // shows the animated loading skeleton\n  loadingPoints?: number; // number of points in the loading skeleton\n  xDataKey?: keyof TData & string; // x-axis key — only needed for the <Brush /> footer\n};\n\ntype EvilLineChartProps<\n  TData extends Record<string, unknown>,\n  TConfig extends Record<string, ChartConfig[string]>,\n> = EvilLineChartBaseProps<TData, TConfig>;\n\n/**\n * Root of the composible line chart. Owns the data, the shared context, the\n * loading skeleton, and the optional zoom brush. Everything visual — axes,\n * grid, tooltip, legend, and the lines themselves — is composed as children,\n * so a consumer renders exactly the parts they need.\n */\nexport function EvilLineChart<\n  TData extends Record<string, unknown>,\n  TConfig extends Record<string, ChartConfig[string]>,\n>({\n  config,\n  data,\n  children,\n  className,\n  chartProps,\n  curveType = \"linear\",\n  animationType = \"left-to-right\",\n  defaultSelectedDataKey = null,\n  onSelectionChange,\n  isLoading = false,\n  loadingPoints,\n  xDataKey,\n}: EvilLineChartProps<TData, TConfig>) {\n  const chartId = useId().replace(/:/g, \"\"); // colon-free id keeps CSS/SVG selectors valid\n  const [selectedDataKey, setSelectedDataKey] = useState<string | null>(defaultSelectedDataKey);\n  const { loadingData, onShimmerExit } = useLoadingData(isLoading, loadingPoints);\n  const { visibleData, brushProps } = useEvilBrush({ data });\n\n  // Brush is a <Brush /> child now (not props): pull it out of the children so\n  // it never reaches the Recharts tree, and drive the footer from its props.\n  const brush = useMemo(() => {\n    // Pull the <Brush> element out of the children (config-only, never rendered\n    // into the Recharts tree); toArray also assigns stable keys to the rest.\n    const parts = Children.toArray(children);\n    const brushEl = parts.find((child) => isValidElement(child) && child.type === Brush);\n    const bp = (isValidElement(brushEl) ? brushEl.props : {}) as BrushProps;\n    return {\n      slot: {\n        present: isValidElement(brushEl),\n        height: bp.height,\n        formatLabel: bp.formatLabel,\n        onChange: bp.onChange,\n      },\n      chartChildren: parts.filter((child) => !(isValidElement(child) && child.type === Brush)),\n    };\n  }, [children]);\n  const showBrush = brush.slot.present;\n\n  const displayData = showBrush && !isLoading ? visibleData : data;\n\n  // Updates selection state and notifies the parent\n  const selectDataKey = useCallback(\n    (newSelectedDataKey: string | null) => {\n      setSelectedDataKey(newSelectedDataKey);\n      onSelectionChange?.(newSelectedDataKey);\n    },\n    [onSelectionChange],\n  );\n\n  const contextValue = useMemo<LineChartContextValue>(\n    () => ({\n      config,\n      curveType,\n      animationType,\n      isLoading,\n      selectedDataKey,\n      selectDataKey,\n    }),\n    [config, curveType, animationType, isLoading, selectedDataKey, selectDataKey],\n  );\n\n  return (\n    <LineChartContext value={contextValue}>\n      <ChartContainer\n        className={className}\n        config={config}\n        footer={\n          showBrush &&\n          !isLoading && (\n            <EvilBrush\n              data={data}\n              chartConfig={config}\n              xDataKey={xDataKey}\n              variant=\"line\"\n              curveType={curveType}\n              height={brush.slot.height}\n              formatLabel={brush.slot.formatLabel}\n              skipStyle\n              className=\"mt-1\"\n              {...brushProps}\n              onChange={(range) => {\n                brushProps.onChange(range);\n                brush.slot.onChange?.(range);\n              }}\n            />\n          )\n        }\n      >\n        <LoadingIndicator isLoading={isLoading} />\n        <RechartsLineChart\n          id={chartId}\n          accessibilityLayer\n          data={isLoading ? loadingData : displayData}\n          {...chartProps}\n        >\n          {brush.chartChildren}\n          {isLoading && (\n            <LoadingLine chartId={chartId} curveType={curveType} onShimmerExit={onShimmerExit} />\n          )}\n        </RechartsLineChart>\n      </ChartContainer>\n    </LineChartContext>\n  );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Composible parts\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype LineProps = {\n  dataKey: string; // series key — must exist on the data and config\n  strokeVariant?: StrokeVariant; // stroke style for this line only\n  strokeWidth?: number; // stroke thickness in pixels for this line\n  curveType?: CurveType; // curve interpolation — falls back to the chart default\n  animationType?: LineAnimationType; // intro reveal — falls back to the chart default\n  connectNulls?: boolean; // join segments across null/missing values\n  isClickable?: boolean; // lets this line be selected by clicking it\n  glowing?: boolean; // applies a soft outer glow to this line\n  enableBufferLine?: boolean; // renders this line's last segment as a dashed buffer\n  children?: ReactNode; // optional <Dot /> and <ActiveDot /> composition\n  lineProps?: ComponentProps<typeof RechartsLine>; // escape hatch for raw Recharts Line props\n};\n\n/**\n * A single line series. Each <Line /> is fully self-contained: it generates its\n * own gradient and glow definitions under a unique id, so any number of lines —\n * each with its own stroke, glow, and clickability — can live in one chart\n * without style collisions. Compose <Dot /> and <ActiveDot /> inside it to add\n * point markers.\n */\nfunction Line({\n  dataKey,\n  strokeVariant = \"solid\",\n  strokeWidth = STROKE_WIDTH,\n  curveType,\n  animationType,\n  connectNulls = false,\n  isClickable = false,\n  glowing = false,\n  enableBufferLine = false,\n  children,\n  lineProps,\n}: LineProps) {\n  const {\n    config,\n    curveType: defaultCurve,\n    animationType: defaultAnimation,\n    isLoading,\n    selectedDataKey,\n    selectDataKey,\n  } = useLineChart();\n  const id = useId().replace(/:/g, \"\"); // unique id scopes this line's style defs\n  // Devices set to \"reduce motion\" skip the intro reveal entirely\n  const shouldReduceMotion = useReducedMotion();\n\n  // The root renders the skeleton line while loading, so real lines step aside\n  if (isLoading) return null;\n\n  const resolvedCurve = curveType ?? defaultCurve;\n\n  // The reveal is an animated SVG mask — heavier than a static chart — so\n  // `\"none\"` and the OS reduce-motion preference both opt out of it.\n  const revealType: LineAnimationType = shouldReduceMotion\n    ? \"none\"\n    : (animationType ?? defaultAnimation);\n  const maskId = revealType === \"none\" ? undefined : `${id}-reveal-mask`;\n\n  const isSelected = selectedDataKey === dataKey;\n  const hasSelection = selectedDataKey !== null;\n  const opacity = getOpacity(selectedDataKey, dataKey);\n\n  const { dot, activeDot } = resolveDots(children, id, dataKey, opacity.dot, maskId);\n\n  const isAnimatedDashed = strokeVariant === \"animated-dashed\";\n  const isDashed = strokeVariant === \"dashed\" || isAnimatedDashed;\n\n  return (\n    <>\n      <g key={dataKey}>\n        {isClickable && (\n          <RechartsLine\n            type={resolvedCurve}\n            dataKey={dataKey}\n            connectNulls={connectNulls}\n            stroke=\"transparent\"\n            strokeWidth={15}\n            dot={false}\n            activeDot={false}\n            isAnimationActive={false}\n            legendType=\"none\"\n            tooltipType=\"none\"\n            style={{ cursor: \"pointer\" }}\n            onClick={() => selectDataKey(isSelected ? null : dataKey)}\n          />\n        )}\n        <RechartsLine\n          type={resolvedCurve}\n          dataKey={dataKey}\n          connectNulls={connectNulls}\n          strokeOpacity={opacity.stroke}\n          stroke={`url(#${id}-colors-${dataKey})`}\n          filter={glowing ? `url(#${id}-glow-${dataKey})` : undefined}\n          dot={dot}\n          activeDot={activeDot}\n          strokeWidth={strokeWidth}\n          strokeDasharray={getStrokeDasharray(enableBufferLine, isDashed)}\n          shape={enableBufferLine ? bufferLineShape : undefined}\n          // Recharts' built-in line animation is permanently disabled — it drew\n          // the line after the dots had already popped in. The motion.dev reveal\n          // mask drives the intro instead, wiping stroke and dots in together.\n          isAnimationActive={false}\n          style={{\n            ...(maskId ? { mask: `url(#${maskId})` } : {}),\n            ...(isClickable ? { cursor: \"pointer\" } : {}),\n          }}\n          onClick={() => {\n            if (!isClickable) return;\n            // Clicking the selected line clears the selection, otherwise selects it\n            selectDataKey(isSelected ? null : dataKey);\n          }}\n          {...lineProps}\n        >\n          {isAnimatedDashed && !hasSelection && <AnimatedDashedStroke />}\n        </RechartsLine>\n      </g>\n      <defs>\n        {revealType !== \"none\" && <RevealMask id={id} type={revealType} />}\n        <ColorGradient id={id} dataKey={dataKey} config={config} />\n        {glowing && <GlowFilter id={id} dataKey={dataKey} />}\n      </defs>\n    </>\n  );\n}\n\ntype DotProps = {\n  variant?: DotVariant; // visual style of the point marker\n};\n\n/**\n * Declares a resting point marker for the <Line /> it is composed inside.\n * It renders nothing on its own — the parent <Line /> reads its variant and\n * wires it into the Recharts dot slot.\n */\nconst Dot: FC<DotProps> = () => null;\n\n/**\n * Declares the hovered/active point marker for the <Line /> it is composed\n * inside. Like <Dot />, it is a configuration slot and renders nothing itself.\n */\nconst ActiveDot: FC<DotProps> = () => null;\n\ntype XAxisProps = ComponentProps<typeof RechartsXAxis>;\n\n/**\n * The horizontal category axis. Ships with the chart's flat default styling and\n * forwards every Recharts XAxis prop, so `dataKey`, `tickFormatter`, etc. are\n * passed straight through. Hidden automatically while the chart is loading.\n */\nfunction XAxis({\n  tickLine = false,\n  axisLine = false,\n  tickMargin = 8,\n  minTickGap = 8,\n  ...props\n}: XAxisProps) {\n  const { isLoading } = useLineChart();\n\n  if (isLoading) return null;\n\n  return (\n    <RechartsXAxis\n      tickLine={tickLine}\n      axisLine={axisLine}\n      tickMargin={tickMargin}\n      minTickGap={minTickGap}\n      {...props}\n    />\n  );\n}\n\ntype YAxisProps = ComponentProps<typeof RechartsYAxis>;\n\n/**\n * The vertical value axis. Ships with the chart's flat default styling and\n * forwards every Recharts YAxis prop. Hidden automatically while the chart is\n * loading.\n */\nfunction YAxis({\n  tickLine = false,\n  axisLine = false,\n  tickMargin = 8,\n  minTickGap = 8,\n  width = \"auto\",\n  ...props\n}: YAxisProps) {\n  const { isLoading } = useLineChart();\n\n  if (isLoading) return null;\n\n  return (\n    <RechartsYAxis\n      tickLine={tickLine}\n      axisLine={axisLine}\n      tickMargin={tickMargin}\n      minTickGap={minTickGap}\n      width={width}\n      {...props}\n    />\n  );\n}\n\ntype GridProps = ComponentProps<typeof CartesianGrid>;\n\n/**\n * The background grid lines. Defaults to horizontal-only dashed lines and\n * forwards every Recharts CartesianGrid prop for full control.\n */\nfunction Grid({ vertical = false, strokeDasharray = \"3 3\", ...props }: GridProps) {\n  return <CartesianGrid vertical={vertical} strokeDasharray={strokeDasharray} {...props} />;\n}\n\ntype TooltipProps = {\n  variant?: TooltipVariant; // visual style of the tooltip surface\n  roundness?: TooltipRoundness; // border-radius of the tooltip\n  defaultIndex?: number; // data index shown by default with no hover\n  cursor?: boolean; // whether the vertical cursor line follows the pointer\n};\n\n/**\n * The hover tooltip. Reads the chart's selection from context so its content\n * dims unselected series. Hidden automatically while the chart is loading.\n */\nfunction Tooltip({ variant, roundness, defaultIndex, cursor = true }: TooltipProps) {\n  const { isLoading, selectedDataKey } = useLineChart();\n\n  if (isLoading) return null;\n\n  return (\n    <ChartTooltip\n      defaultIndex={defaultIndex}\n      cursor={cursor ? { strokeDasharray: \"3 3\", strokeWidth: STROKE_WIDTH } : false}\n      content={\n        <ChartTooltipContent selected={selectedDataKey} roundness={roundness} variant={variant} />\n      }\n    />\n  );\n}\n\ntype LegendProps = {\n  variant?: ChartLegendVariant; // visual style of the legend indicators\n  align?: \"left\" | \"center\" | \"right\"; // horizontal placement\n  verticalAlign?: \"top\" | \"middle\" | \"bottom\"; // vertical placement\n  isClickable?: boolean; // lets each entry toggle selection of its series\n};\n\n/**\n * The series legend. When `isClickable` is set, each entry toggles selection of\n * its series, driving the shared selection state read by every <Line />.\n */\nfunction Legend({\n  variant,\n  align = \"right\",\n  verticalAlign = \"top\",\n  isClickable = false,\n}: LegendProps) {\n  const { selectedDataKey, selectDataKey } = useLineChart();\n\n  return (\n    <ChartLegend\n      verticalAlign={verticalAlign}\n      align={align}\n      content={\n        <ChartLegendContent\n          selected={selectedDataKey}\n          onSelectChange={selectDataKey}\n          isClickable={isClickable}\n          variant={variant}\n        />\n      }\n    />\n  );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Selection + dot helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Returns stroke/dot opacity — dims a series only when another is selected\nconst getOpacity = (selectedDataKey: string | null, dataKey: string) => {\n  if (selectedDataKey === null) {\n    return { stroke: 1, dot: 1 };\n  }\n\n  return selectedDataKey === dataKey ? { stroke: 1, dot: 1 } : { stroke: 0.3, dot: 0.3 };\n};\n\n// Resolves a line's stroke-dasharray — the buffer line manages its own dashes\nconst getStrokeDasharray = (enableBufferLine: boolean, isDashed: boolean) => {\n  if (enableBufferLine) return undefined;\n\n  return isDashed ? \"5 5\" : undefined;\n};\n\n// Pulls <Dot /> and <ActiveDot /> out of a line's children into Recharts dot slots.\n// When a `maskId` is given the resting dot is wired to the intro reveal mask so it\n// wipes in with the line; the active dot is always left unmasked since it only\n// appears on hover, after the intro has finished.\nconst resolveDots = (\n  children: ReactNode,\n  id: string,\n  dataKey: string,\n  dotOpacity: number,\n  maskId: string | undefined,\n): { dot: LineDotProp; activeDot: LineActiveDotProp } => {\n  let dot: LineDotProp = false;\n  let activeDot: LineActiveDotProp = false;\n\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) return;\n\n    if (child.type === Dot) {\n      const { variant } = (child as ReactElement<DotProps>).props;\n      dot = (\n        <ChartDot\n          type={variant}\n          dataKey={dataKey}\n          chartId={id}\n          fillOpacity={dotOpacity}\n          maskId={maskId}\n        />\n      );\n    }\n\n    if (child.type === ActiveDot) {\n      const { variant } = (child as ReactElement<DotProps>).props;\n      activeDot = (\n        <ChartDot type={variant} dataKey={dataKey} chartId={id} fillOpacity={dotOpacity} />\n      );\n    }\n  });\n\n  return { dot, activeDot };\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Buffer line\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Buffer line shape — renders the last segment as dashed while the rest stays solid.\n// Renders a single <Curve> and uses a ref callback to measure the actual SVG path\n// length via getTotalLength() + getPointAtLength(), then sets stroke-dasharray\n// imperatively. Works correctly with any curve type (linear, natural, monotone, etc.).\ntype CurvePoint = NonNullable<NonNullable<CurveProps[\"points\"]>[number]>;\ntype DrawableCurvePoint = CurvePoint & { x: number; y: number };\n\nconst isDrawableCurvePoint = (point: CurvePoint): point is DrawableCurvePoint => {\n  return typeof point.x === \"number\" && typeof point.y === \"number\";\n};\n\nconst BUFFER_DASH_SIZE = 4;\nconst BUFFER_GAP_SIZE = 3;\n\n// Binary-search the path to find the length at which path.x ≈ targetX,\n// using the browser's native getPointAtLength for exact curve measurement.\nconst findLengthAtX = (path: SVGPathElement, totalLength: number, targetX: number): number => {\n  let lo = 0;\n  let hi = totalLength;\n  // ~0.5px precision is more than enough for a dasharray split\n  while (hi - lo > 0.5) {\n    const mid = (lo + hi) / 2;\n    const pt = path.getPointAtLength(mid);\n    if (pt.x < targetX) lo = mid;\n    else hi = mid;\n  }\n  return (lo + hi) / 2;\n};\n\nconst bufferLineShape = (props: CurveProps) => {\n  const { points, ...rest } = props;\n\n  if (!points || points.length < 2) {\n    return <Curve {...props} />;\n  }\n\n  const drawablePoints = points.filter(isDrawableCurvePoint);\n\n  if (drawablePoints.length < 2) {\n    return <Curve {...props} />;\n  }\n\n  // x coordinate of the second-to-last point — where solid meets dashed\n  const splitX = drawablePoints[drawablePoints.length - 2].x;\n\n  // Ref callback runs synchronously during React commit (before browser paint),\n  // so there's no visible flash of an un-dashed line.\n  const gRef = (g: SVGGElement | null) => {\n    if (!g) return;\n    const path = g.querySelector(\"path\");\n    if (!path) return;\n\n    const totalLength = path.getTotalLength();\n    const solidLength = findLengthAtX(path, totalLength, splitX);\n    const lastSegmentLength = totalLength - solidLength;\n\n    // Build dasharray: solid run, then repeating dash-gap for the buffer segment\n    const reps = Math.ceil(lastSegmentLength / (BUFFER_DASH_SIZE + BUFFER_GAP_SIZE)) + 1;\n    const dashedPart = Array.from(\n      { length: reps },\n      () => `${BUFFER_DASH_SIZE} ${BUFFER_GAP_SIZE}`,\n    ).join(\" \");\n\n    path.setAttribute(\"stroke-dasharray\", `${solidLength} 0 ${dashedPart}`);\n  };\n\n  return (\n    <g ref={gRef}>\n      <Curve {...rest} points={drawablePoints} />\n    </g>\n  );\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Style definitions — one set per <Line />, scoped to its unique id\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype StyleProps = {\n  id: string; // unique id of the owning <Line />\n  dataKey: string; // series key the style belongs to\n};\n\n// Animated dashed-stroke effect, rendered as a child of the Recharts Line\nconst AnimatedDashedStroke = () => {\n  return (\n    <>\n      <animate\n        attributeName=\"stroke-dasharray\"\n        values=\"5 5; 0 5; 5 5\"\n        dur=\"1s\"\n        repeatCount=\"indefinite\"\n        keyTimes=\"0;0.5;1\"\n      />\n      <animate\n        attributeName=\"stroke-dashoffset\"\n        values=\"0; -10\"\n        dur=\"1s\"\n        repeatCount=\"indefinite\"\n        keyTimes=\"0;1\"\n      />\n    </>\n  );\n};\n\n// motion `originX` for each single-rect reveal — the edge the wipe grows from.\n// 0 = left edge, 1 = right edge, 0.5 = centre (grows outward to both edges).\nconst SINGLE_REVEAL_ORIGIN: Record<Exclude<RevealAnimationType, \"edges-in\">, number> = {\n  \"left-to-right\": 0,\n  \"right-to-left\": 1,\n  \"center-out\": 0.5,\n};\n\n/**\n * Wipe mask driven by motion.dev, played once when a <Line /> mounts. The same\n * mask is applied to the line's stroke and its resting dots, so both reveal in\n * lockstep — fixing Recharts' default, where the dots appeared before the line\n * had finished drawing.\n *\n * `maskUnits`/`maskContentUnits` are both userSpaceOnUse so every masked element\n * shares one coordinate space and the wipe edge lands at the same x on each.\n *\n * Each rect animates `scaleX` 0 → 1; `originX` decides which edge it grows from.\n * \"edges-in\" needs two rects — each half grows inward from an opposite edge.\n */\nconst RevealMask = ({ id, type }: { id: string; type: RevealAnimationType }) => {\n  const reveal = {\n    initial: { scaleX: 0 },\n    animate: { scaleX: 1 },\n    transition: { duration: REVEAL_DURATION, ease: REVEAL_EASE },\n  };\n\n  return (\n    <mask\n      id={`${id}-reveal-mask`}\n      maskUnits=\"userSpaceOnUse\"\n      maskContentUnits=\"userSpaceOnUse\"\n      x=\"0\"\n      y=\"0\"\n      width=\"100%\"\n      height=\"100%\"\n    >\n      {type === \"edges-in\" ? (\n        <>\n          {/* left half wipes inward from the left edge toward the centre */}\n          <motion.rect\n            {...reveal}\n            x=\"0\"\n            y=\"0\"\n            width=\"50%\"\n            height=\"100%\"\n            fill=\"white\"\n            style={{ originX: 0 }}\n          />\n          {/* right half wipes inward from the right edge toward the centre */}\n          <motion.rect\n            {...reveal}\n            x=\"50%\"\n            y=\"0\"\n            width=\"50%\"\n            height=\"100%\"\n            fill=\"white\"\n            style={{ originX: 1 }}\n          />\n        </>\n      ) : (\n        <motion.rect\n          {...reveal}\n          x=\"0\"\n          y=\"0\"\n          width=\"100%\"\n          height=\"100%\"\n          fill=\"white\"\n          style={{ originX: SINGLE_REVEAL_ORIGIN[type] }}\n        />\n      )}\n    </mask>\n  );\n};\n\n/**\n * Horizontal left-to-right color gradient for a series. Always rendered — the\n * line's stroke and its dots all paint from this single gradient.\n */\nconst ColorGradient = ({ id, dataKey, config }: StyleProps & { config: ChartConfig }) => {\n  const colorsCount = getColorsCount(config[dataKey] ?? {});\n\n  return (\n    <linearGradient id={`${id}-colors-${dataKey}`} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">\n      {colorsCount === 1 ? (\n        <>\n          <stop offset=\"0%\" stopColor={`var(--color-${dataKey}-0)`} />\n          <stop offset=\"100%\" stopColor={`var(--color-${dataKey}-0)`} />\n        </>\n      ) : (\n        Array.from({ length: colorsCount }, (_, index) => {\n          const offset = `${(index / (colorsCount - 1)) * 100}%`;\n          return (\n            <stop\n              key={offset}\n              offset={offset}\n              stopColor={`var(--color-${dataKey}-${index}, var(--color-${dataKey}-0))`}\n            />\n          );\n        })\n      )}\n    </linearGradient>\n  );\n};\n\n/** Soft outer glow filter applied to a glowing line. */\nconst GlowFilter = ({ id, dataKey }: StyleProps) => {\n  return (\n    <filter id={`${id}-glow-${dataKey}`} x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">\n      <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"10\" result=\"blur\" />\n      <feColorMatrix\n        in=\"blur\"\n        type=\"matrix\"\n        values=\"1 0 0 0 0\n                0 1 0 0 0\n                0 0 1 0 0\n                0 0 0 2 0\"\n        result=\"glow\"\n      />\n      <feMerge>\n        <feMergeNode in=\"glow\" />\n        <feMergeNode in=\"SourceGraphic\" />\n      </feMerge>\n    </filter>\n  );\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Loading skeleton\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Builds bell-curve eased gradient stops for the loading shimmer\nconst generateEasedGradientStops = (\n  steps: number = 17,\n  minOpacity: number = 0.05,\n  maxOpacity: number = 0.9,\n) => {\n  return Array.from({ length: steps }, (_, i) => {\n    const t = i / (steps - 1); // 0 to 1\n    // Sine-based bell curve easing: peaks at center (t=0.5), smooth falloff at edges\n    const eased = Math.sin(t * Math.PI) ** 2;\n    const opacity = minOpacity + eased * (maxOpacity - minOpacity);\n    return { offset: `${(t * 100).toFixed(0)}%`, opacity: Number(opacity.toFixed(3)) };\n  });\n};\n\n/**\n * Hook to manage loading data with pixel-perfect shimmer synchronization.\n *\n * Uses motion.dev's onUpdate callback to ensure chart data is only regenerated\n * when the shimmer has completely exited the visible area. This eliminates\n * timing drift issues from setTimeout/setInterval.\n */\nexport function useLoadingData(isLoading: boolean, loadingPoints: number = 14) {\n  const [loadingDataKey, setLoadingDataKey] = useState(false);\n\n  // Callback fired by motion.dev when the shimmer exits the visible area\n  const onShimmerExit = useCallback(() => {\n    if (isLoading) {\n      setLoadingDataKey((prev) => !prev);\n    }\n  }, [isLoading]);\n\n  const loadingData = useMemo(\n    () => getLoadingData(loadingPoints),\n    // loadingDataKey toggle triggers re-computation when the shimmer exits\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [loadingPoints, loadingDataKey],\n  );\n\n  return { loadingData, onShimmerExit };\n}\n\n/**\n * The skeleton line shown while the chart is loading. Rendered by the root in\n * place of the real lines, paired with its own masked shimmer pattern.\n */\nconst LoadingLine = ({\n  chartId,\n  curveType,\n  onShimmerExit,\n}: {\n  chartId: string;\n  curveType: CurveType;\n  onShimmerExit: () => void;\n}) => {\n  return (\n    <>\n      <RechartsLine\n        type={curveType}\n        dataKey={LOADING_LINE_DATA_KEY}\n        min={0}\n        max={100}\n        stroke=\"currentColor\"\n        strokeOpacity={0.5}\n        isAnimationActive={false}\n        legendType=\"none\"\n        tooltipType=\"none\"\n        activeDot={false}\n        dot={false}\n        strokeWidth={STROKE_WIDTH}\n        style={{ mask: `url(#${chartId}-loading-mask)` }}\n      />\n      <defs>\n        <LoadingPattern chartId={chartId} onShimmerExit={onShimmerExit} />\n      </defs>\n    </>\n  );\n};\n\n/**\n * Animated shimmer pattern for the loading skeleton.\n *\n * The visible chart area is normalized to 0-1, the shimmer gradient has width 1,\n * and the pattern is 3x wide so the shimmer has buffer on both sides. The motion\n * rect travels x from -1 to 2; onShimmerExit fires as it crosses x=1, letting the\n * data swap happen while the shimmer is off-screen for a seamless loop.\n */\nconst LoadingPattern = ({\n  chartId,\n  onShimmerExit,\n}: {\n  chartId: string;\n  onShimmerExit: () => void;\n}) => {\n  const gradientStops = generateEasedGradientStops();\n\n  // 1 (left buffer) + 1 (visible) + 1 (right buffer)\n  const patternWidth = 3;\n  const startX = -1;\n  const endX = 2;\n\n  // Tracks the last x value to detect the exit threshold crossing\n  const lastXRef = useRef(startX);\n\n  return (\n    <>\n      <linearGradient id={`${chartId}-loading-gradient`} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">\n        {gradientStops.map(({ offset, opacity }) => (\n          <stop key={offset} offset={offset} stopColor=\"white\" stopOpacity={opacity} />\n        ))}\n      </linearGradient>\n      <pattern\n        id={`${chartId}-loading-pattern`}\n        patternUnits=\"objectBoundingBox\"\n        patternContentUnits=\"objectBoundingBox\"\n        patternTransform=\"rotate(25)\"\n        width={patternWidth}\n        height=\"1\"\n        x=\"0\"\n        y=\"0\"\n      >\n        <motion.rect\n          y=\"0\"\n          width=\"1\"\n          height=\"1\"\n          fill={`url(#${chartId}-loading-gradient)`}\n          initial={{ x: startX }}\n          animate={{ x: endX }}\n          transition={{\n            duration: LOADING_ANIMATION_DURATION / 1000,\n            ease: \"linear\",\n            repeat: Infinity,\n            repeatType: \"loop\",\n          }}\n          onUpdate={(latest) => {\n            const xValue = typeof latest.x === \"number\" ? latest.x : startX;\n            const lastX = lastXRef.current;\n\n            // Fire once per loop, when the shimmer fully exits the visible area\n            if (xValue >= 1 && lastX < 1) {\n              onShimmerExit();\n            }\n\n            lastXRef.current = xValue;\n          }}\n        />\n      </pattern>\n      <mask id={`${chartId}-loading-mask`} maskUnits=\"userSpaceOnUse\">\n        <rect width=\"100%\" height=\"100%\" fill={`url(#${chartId}-loading-pattern)`} />\n      </mask>\n    </>\n  );\n};\n\n// Compound API: every part hangs off the root as a static member, so a consumer\n// writes <EvilLineChart.Line/>, <EvilLineChart.Tooltip/>, … from a single import\n// — no colliding named marker exports when several charts share one file.\nEvilLineChart.Line = Line;\nEvilLineChart.Dot = Dot;\nEvilLineChart.ActiveDot = ActiveDot;\nEvilLineChart.XAxis = XAxis;\nEvilLineChart.YAxis = YAxis;\nEvilLineChart.Grid = Grid;\nEvilLineChart.Tooltip = Tooltip;\nEvilLineChart.Legend = Legend;\nEvilLineChart.Brush = Brush;\n",
      "type": "registry:component",
      "target": "components/evilcharts/charts/recharts-line-chart.tsx"
    }
  ],
  "type": "registry:component"
}