{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "recharts-composed-chart",
  "description": "Composed chart component combining bar and line charts",
  "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-composed-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  Bar as RechartsBar,\n  CartesianGrid,\n  ComposedChart as RechartsComposedChart,\n  Line as RechartsLine,\n  XAxis as RechartsXAxis,\n  YAxis as RechartsYAxis,\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 { ChartLegend, ChartLegendContent, type ChartLegendVariant } from \"@/registry/ui/recharts-legend\";\nimport { Brush, EvilBrush, useEvilBrush, type BrushProps, type EvilBrushRange } from \"@/registry/ui/recharts-brush\";\nimport { ChartDot, type DotVariant } from \"@/registry/ui/recharts-dot\";\nimport { motion, useReducedMotion } from \"motion/react\";\n\n// Constants\nconst STROKE_WIDTH = 2;\nconst DEFAULT_BAR_RADIUS = 4;\nconst LOADING_DATA_KEY = \"loading\";\nconst LOADING_ANIMATION_DURATION = 2000; // in milliseconds\nconst REVEAL_DURATION = 1; // line intro wipe length, in seconds\nconst REVEAL_EASE: [number, number, number, number] = [0, 0.7, 0.5, 1]; // intro easing\nconst BAR_GROW_DURATION = 0.5; // per-bar grow-in length, in seconds\nconst BAR_STAGGER = 0.05; // delay between consecutive bars, in seconds\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\";\ntype BarVariant = \"default\" | \"hatched\" | \"duotone\" | \"duotone-reverse\" | \"gradient\" | \"stripped\";\n\n/**\n * Direction of the custom motion.dev intro. Recharts' own animation is\n * permanently disabled — lines wipe in along this direction, while bars grow up\n * from their baseline staggered in this same order.\n *\n * NOTE: the intro is a per-frame animation, heavier than a static chart.\n * `\"none\"` opts out — as does a device with the OS \"reduce motion\" preference.\n */\ntype ComposedAnimationType = \"none\" | \"left-to-right\" | \"right-to-left\" | \"center-out\" | \"edges-in\";\ntype RevealAnimationType = Exclude<ComposedAnimationType, \"none\">;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Shared context\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Shared state for every part of the chart. Lifted into <EvilComposedChart /> so\n * that <Bar />, <Line />, <XAxis />, <Legend />, and friends can read it without\n * prop drilling. Sub-components are composed freely — the provider is the single\n * source of truth.\n */\ntype ComposedChartContextValue = {\n  config: ChartConfig; // colors + labels for every bar and line series\n  curveType: CurveType; // default curve interpolation each <Line /> inherits\n  animationType: ComposedAnimationType; // default intro each <Bar />/<Line /> inherits\n  introStartedAt: number; // timestamp the chart mounted — anchors the one-shot intro\n  dataLength: number; // number of rows currently rendered\n  isLoading: boolean; // whether the chart shows its loading skeleton\n  hoveredIndex: number | null; // data index currently hovered, or null when none\n  selectedDataKey: string | null; // currently selected series, or null when none\n  selectDataKey: (dataKey: string | null) => void; // sets the selected series\n};\n\nconst ComposedChartContext = createContext<ComposedChartContextValue | null>(null);\n\n// Reads the chart context, throwing a helpful error when used outside <EvilComposedChart />\nfunction useComposedChart() {\n  const context = use(ComposedChartContext);\n\n  if (!context) {\n    throw new Error(\n      \"Composed chart parts (<Bar />, <Line />, <XAxis />, …) must be used within <EvilComposedChart />\",\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 EvilComposedChartBaseProps<\n  TData extends Record<string, unknown>,\n  TConfig extends Record<string, ChartConfig[string]>,\n> = {\n  config: TConfig & ValidateConfigKeys<TData, TConfig>; // series colors + labels for bars and lines\n  data: TData[]; // rows rendered by the chart\n  children: ReactNode; // composed parts — <Bar />, <Line />, <XAxis />, <Legend />, …\n  className?: string; // extra classes for the chart container\n  chartProps?: ComponentProps<typeof RechartsComposedChart>; // escape hatch for the raw Recharts chart\n  curveType?: CurveType; // default curve interpolation for every <Line />\n  animationType?: ComposedAnimationType; // default intro for every <Bar /> and <Line />\n  barGap?: number; // gap between bars sharing a category\n  barCategoryGap?: number; // gap between bar categories\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  loadingBars?: number; // number of bars in the loading skeleton\n  xDataKey?: keyof TData & string; // x-axis key — only needed for the <Brush /> footer\n};\n\ntype EvilComposedChartProps<\n  TData extends Record<string, unknown>,\n  TConfig extends Record<string, ChartConfig[string]>,\n> = EvilComposedChartBaseProps<TData, TConfig>;\n\n/**\n * Root of the composible composed chart. Owns the data, the shared context, the\n * loading skeleton, and the optional zoom brush. Everything visual — axes, grid,\n * tooltip, legend, and the bars and lines themselves — is composed as children,\n * so a consumer renders exactly the parts they need.\n */\nexport function EvilComposedChart<\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  barGap,\n  barCategoryGap,\n  defaultSelectedDataKey = null,\n  onSelectionChange,\n  isLoading = false,\n  loadingBars,\n  xDataKey,\n}: EvilComposedChartProps<TData, TConfig>) {\n  const chartId = useId().replace(/:/g, \"\"); // colon-free id keeps CSS/SVG selectors valid\n  // Anchors the intro to a fixed moment so it plays exactly once — re-renders\n  // and Recharts remounts read elapsed time from here instead of replaying.\n  const [introStartedAt] = useState(() => Date.now());\n  const [selectedDataKey, setSelectedDataKey] = useState<string | null>(defaultSelectedDataKey);\n  const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);\n  const { loadingData, onShimmerExit } = useLoadingData(isLoading, loadingBars);\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<ComposedChartContextValue>(\n    () => ({\n      config,\n      curveType,\n      animationType,\n      introStartedAt,\n      dataLength: displayData.length,\n      isLoading,\n      hoveredIndex,\n      selectedDataKey,\n      selectDataKey,\n    }),\n    [\n      config,\n      curveType,\n      animationType,\n      introStartedAt,\n      displayData.length,\n      isLoading,\n      hoveredIndex,\n      selectedDataKey,\n      selectDataKey,\n    ],\n  );\n\n  return (\n    <ComposedChartContext 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=\"area\"\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        <RechartsComposedChart\n          id={chartId}\n          accessibilityLayer\n          data={isLoading ? loadingData : displayData}\n          barGap={barGap}\n          barCategoryGap={barCategoryGap}\n          onMouseLeave={() => setHoveredIndex(null)}\n          {...chartProps}\n        >\n          {brush.chartChildren}\n          {isLoading && (\n            <LoadingBar\n              chartId={chartId}\n              barRadius={DEFAULT_BAR_RADIUS}\n              onShimmerExit={onShimmerExit}\n            />\n          )}\n        </RechartsComposedChart>\n      </ChartContainer>\n    </ComposedChartContext>\n  );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Composible parts\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype BarProps = {\n  dataKey: string; // series key — must exist on the data and config\n  variant?: BarVariant; // fill style for this bar only\n  radius?: number; // corner radius of the bar in pixels\n  glow?: boolean; // applies a soft neon glow to this bar\n  animationType?: ComposedAnimationType; // grow-in order — falls back to the chart default\n  isClickable?: boolean; // lets this bar be selected by clicking it\n  enableHoverHighlight?: boolean; // dims this bar when another column is hovered\n  barProps?: ComponentProps<typeof RechartsBar>; // escape hatch for raw Recharts Bar props\n};\n\n/**\n * A single bar series. Each <Bar /> is fully self-contained: it generates its\n * own gradient/pattern definitions under a unique id, so any number of bars —\n * each with its own variant, glow, and clickability — can live in one chart\n * without style collisions.\n */\nfunction Bar({\n  dataKey,\n  variant = \"default\",\n  radius = DEFAULT_BAR_RADIUS,\n  glow = false,\n  animationType,\n  isClickable = false,\n  enableHoverHighlight = false,\n  barProps,\n}: BarProps) {\n  const {\n    config,\n    animationType: defaultAnimation,\n    introStartedAt,\n    dataLength,\n    isLoading,\n    hoveredIndex,\n    selectedDataKey,\n    selectDataKey,\n  } = useComposedChart();\n  const id = useId().replace(/:/g, \"\"); // unique id scopes this bar's style defs\n  // Devices set to \"reduce motion\" skip the grow-in animation entirely\n  const shouldReduceMotion = useReducedMotion();\n\n  // The root renders the skeleton bar while loading, so real bars step aside\n  if (isLoading) return null;\n\n  const isSelected = selectedDataKey === null || selectedDataKey === dataKey;\n  const filter = glow ? `url(#${id}-glow)` : undefined;\n\n  // The grow-in is a per-frame animation — heavier than a static chart — so\n  // `\"none\"` and the OS reduce-motion preference both opt out of it.\n  const revealType: ComposedAnimationType = shouldReduceMotion\n    ? \"none\"\n    : (animationType ?? defaultAnimation);\n\n  return (\n    <>\n      <RechartsBar\n        dataKey={dataKey}\n        fill={`url(#${id}-bar-colors)`}\n        radius={radius}\n        // Recharts' built-in bar animation is permanently disabled — every bar\n        // instead grows in from its baseline via the staggered motion.dev shape.\n        isAnimationActive={false}\n        style={isClickable || enableHoverHighlight ? { cursor: \"pointer\" } : undefined}\n        shape={(props: unknown) => {\n          const barShapeProps = props as BarShapeProps;\n          const index = typeof barShapeProps.index === \"number\" ? barShapeProps.index : -1;\n\n          return (\n            <CustomBar\n              {...barShapeProps}\n              id={id}\n              variant={variant}\n              barRadius={radius}\n              filter={filter}\n              fillOpacity={getBarOpacity({\n                isClickable,\n                isSelected,\n                selectedDataKey,\n                enableHoverHighlight,\n                hoveredIndex,\n                index,\n              })}\n              isClickable={isClickable}\n              enableHoverHighlight={enableHoverHighlight}\n              animationType={revealType}\n              dataLength={dataLength}\n              introStartedAt={introStartedAt}\n              onClick={() => {\n                if (!isClickable) return;\n                selectDataKey(selectedDataKey === dataKey ? null : dataKey);\n              }}\n            />\n          );\n        }}\n        {...barProps}\n      />\n      <defs>\n        <VerticalColorGradient id={id} dataKey={dataKey} config={config} />\n        {variant === \"hatched\" && <HatchedPattern id={id} dataKey={dataKey} />}\n        {variant === \"duotone\" && <DuotonePattern id={id} dataKey={dataKey} config={config} />}\n        {variant === \"duotone-reverse\" && (\n          <DuotoneReversePattern id={id} dataKey={dataKey} config={config} />\n        )}\n        {variant === \"gradient\" && <GradientPattern id={id} dataKey={dataKey} />}\n        {variant === \"stripped\" && <StrippedPattern id={id} dataKey={dataKey} />}\n        {glow && <BarGlowFilter id={id} />}\n      </defs>\n    </>\n  );\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  curveType?: CurveType; // curve interpolation — falls back to the chart default\n  animationType?: ComposedAnimationType; // intro reveal — falls back to the chart default\n  connectNulls?: boolean; // join segments across null/missing values\n  glow?: boolean; // applies a soft neon glow to this line\n  isClickable?: boolean; // lets this line be selected by clicking it\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 color gradient and glow filter under a unique id, so any number of lines —\n * each with its own stroke, curve, 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  curveType,\n  animationType,\n  connectNulls = false,\n  glow = false,\n  isClickable = false,\n  children,\n  lineProps,\n}: LineProps) {\n  const {\n    config,\n    curveType: defaultCurve,\n    animationType: defaultAnimation,\n    isLoading,\n    selectedDataKey,\n    selectDataKey,\n  } = useComposedChart();\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 bar 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: ComposedAnimationType = shouldReduceMotion\n    ? \"none\"\n    : (animationType ?? defaultAnimation);\n  const maskId = revealType === \"none\" ? undefined : `${id}-reveal-mask`;\n\n  const opacity = getOpacity(selectedDataKey, dataKey);\n  const hasSelection = selectedDataKey !== null;\n  const filter = glow ? `url(#${id}-glow)` : undefined;\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  const handleLineClick = () => {\n    if (!isClickable) return;\n    selectDataKey(selectedDataKey === dataKey ? null : dataKey);\n  };\n\n  return (\n    <>\n      {isClickable && (\n        <RechartsLine\n          type={resolvedCurve}\n          dataKey={dataKey}\n          connectNulls={connectNulls}\n          stroke=\"transparent\"\n          strokeWidth={20}\n          dot={false}\n          activeDot={false}\n          isAnimationActive={false}\n          legendType=\"none\"\n          tooltipType=\"none\"\n          style={{ cursor: \"pointer\" }}\n          onClick={handleLineClick}\n        />\n      )}\n      <RechartsLine\n        type={resolvedCurve}\n        dataKey={dataKey}\n        connectNulls={connectNulls}\n        strokeOpacity={opacity.stroke}\n        stroke={`url(#${id}-line-colors-${dataKey})`}\n        filter={filter}\n        dot={dot}\n        activeDot={activeDot}\n        strokeWidth={STROKE_WIDTH}\n        strokeDasharray={isDashed ? \"5 5\" : undefined}\n        // Recharts' built-in line animation is permanently disabled — the\n        // motion.dev reveal mask drives the intro, wiping stroke and dots in together.\n        isAnimationActive={false}\n        style={{\n          ...(maskId ? { mask: `url(#${maskId})` } : {}),\n          ...(isClickable ? { cursor: \"pointer\", pointerEvents: \"none\" } : {}),\n        }}\n        {...lineProps}\n      >\n        {isAnimatedDashed && !hasSelection && <AnimatedDashedStroke />}\n      </RechartsLine>\n      <defs>\n        {revealType !== \"none\" && <RevealMask id={id} type={revealType} />}\n        <HorizontalColorGradient id={id} dataKey={dataKey} config={config} />\n        {glow && <LineGlowFilter id={id} />}\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 } = useComposedChart();\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. Forwards every Recharts YAxis prop straight through.\n * Hidden automatically while the chart is 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 } = useComposedChart();\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 } = useComposedChart();\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 <Bar /> and <Line />.\n */\nfunction Legend({\n  variant,\n  align = \"right\",\n  verticalAlign = \"top\",\n  isClickable = false,\n}: LegendProps) {\n  const { selectedDataKey, selectDataKey } = useComposedChart();\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 for a line — 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// Returns the fill opacity for a bar, accounting for both selection and hover state\nconst getBarOpacity = ({\n  isClickable,\n  isSelected,\n  selectedDataKey,\n  enableHoverHighlight,\n  hoveredIndex,\n  index,\n}: {\n  isClickable: boolean;\n  isSelected: boolean;\n  selectedDataKey: string | null;\n  enableHoverHighlight: boolean;\n  hoveredIndex: number | null;\n  index: number;\n}) => {\n  const clickOpacity = isClickable && selectedDataKey !== null ? (isSelected ? 1 : 0.15) : 1;\n\n  if (enableHoverHighlight && hoveredIndex !== null) {\n    return hoveredIndex === index ? clickOpacity : clickOpacity * 0.3;\n  }\n\n  return clickOpacity;\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}-line`}\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\n          type={variant}\n          dataKey={dataKey}\n          chartId={`${id}-line`}\n          fillOpacity={dotOpacity}\n        />\n      );\n    }\n  });\n\n  return { dot, activeDot };\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Custom bar shape\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Props Recharts passes to a bar's custom shape renderer\ntype BarShapeProps = {\n  x?: number;\n  y?: number;\n  width?: number;\n  height?: number;\n  fill?: string;\n  fillOpacity?: number;\n  dataKey?: string;\n  index?: number;\n  background?: {\n    x?: number;\n    y?: number;\n    width?: number;\n    height?: number;\n  };\n  [key: string]: unknown;\n};\n\ntype CustomBarProps = {\n  id: string; // unique id of the owning <Bar />\n  variant: BarVariant; // fill style of the bar\n  barRadius: number; // corner radius of the bar\n  filter?: string; // optional glow filter reference\n  isClickable?: boolean; // whether the bar is selectable by click\n  enableHoverHighlight?: boolean; // whether hovering a column dims the others\n  animationType?: ComposedAnimationType; // grow-in order for this bar\n  dataLength?: number; // total bars in the series — drives the stagger\n  introStartedAt?: number; // chart-mount timestamp anchoring the one-shot grow-in\n  onClick?: () => void; // fired when a clickable bar is clicked\n} & BarShapeProps;\n\n// Renders a single bar rectangle with its variant fill, glow, and hit area\nconst CustomBar = ({\n  x = 0,\n  y = 0,\n  width = 0,\n  height = 0,\n  fillOpacity = 1,\n  background,\n  index = -1,\n  id,\n  variant,\n  barRadius,\n  filter,\n  isClickable,\n  enableHoverHighlight,\n  animationType = \"none\",\n  dataLength = 0,\n  introStartedAt = 0,\n  onClick,\n}: CustomBarProps) => {\n  const cursorStyle = isClickable || enableHoverHighlight ? { cursor: \"pointer\" } : undefined;\n  const hitAreaX = background?.x ?? x;\n  const hitAreaY = background?.y ?? y;\n  const hitAreaWidth = background?.width ?? width;\n  const hitAreaHeight = background?.height ?? height;\n\n  // motion.dev grow-in props for this bar — an empty object once it has finished\n  const grow = getBarGrowAnimation(animationType, index, dataLength, introStartedAt) ?? {};\n\n  const getFill = () => {\n    switch (variant) {\n      case \"hatched\":\n        return `url(#${id}-hatched)`;\n      case \"duotone\":\n        return `url(#${id}-duotone)`;\n      case \"duotone-reverse\":\n        return `url(#${id}-duotone-reverse)`;\n      case \"gradient\":\n        return `url(#${id}-gradient)`;\n      case \"stripped\":\n        return `url(#${id}-stripped)`;\n      default:\n        return `url(#${id}-bar-colors)`;\n    }\n  };\n\n  // Full-height invisible rect — keeps the column hoverable even mid grow-in\n  const hitArea = enableHoverHighlight ? (\n    <rect\n      x={hitAreaX}\n      y={hitAreaY}\n      width={hitAreaWidth}\n      height={hitAreaHeight}\n      fill=\"transparent\"\n    />\n  ) : null;\n\n  if (variant === \"stripped\") {\n    return (\n      <g style={cursorStyle} onClick={onClick}>\n        <motion.g\n          {...grow}\n          filter={filter}\n          opacity={fillOpacity}\n          className=\"transition-opacity duration-200\"\n        >\n          <rect x={x} y={y} width={width} height={height} fill={getFill()} />\n          <rect x={x} y={y} width={width} height={2} fill={`url(#${id}-bar-colors)`} />\n        </motion.g>\n        {hitArea}\n      </g>\n    );\n  }\n\n  return (\n    <g style={cursorStyle} onClick={onClick}>\n      <motion.g {...grow}>\n        <rect\n          x={x}\n          y={y}\n          width={width}\n          height={height}\n          rx={barRadius}\n          ry={barRadius}\n          fill={getFill()}\n          opacity={fillOpacity}\n          filter={filter}\n          className=\"transition-opacity duration-200\"\n        />\n      </motion.g>\n      {hitArea}\n    </g>\n  );\n};\n\n/**\n * Builds the motion.dev grow-in animation for a single bar, or returns `null`\n * when it should render statically (`\"none\"`, an unknown index, or — crucially —\n * once the bar has already finished growing).\n *\n * The intro is anchored to `introStartedAt` (stamped once when the chart mounts)\n * rather than to component mount. Recharts remounts every bar whenever the chart\n * re-renders, so a mount-based animation would replay endlessly; reading elapsed\n * time instead makes it a true one-shot — a bar past its window renders static,\n * a bar caught mid-grow resumes from where it should already be.\n */\nconst getBarGrowAnimation = (\n  animationType: ComposedAnimationType,\n  index: number,\n  dataLength: number,\n  introStartedAt: number,\n) => {\n  if (animationType === \"none\" || index < 0 || dataLength <= 0) return null;\n\n  const lastIndex = dataLength - 1;\n  const center = lastIndex / 2;\n\n  // How many bars this one waits behind before it starts growing\n  let step: number;\n  switch (animationType) {\n    case \"right-to-left\":\n      step = lastIndex - index;\n      break;\n    case \"center-out\":\n      step = Math.abs(index - center);\n      break;\n    case \"edges-in\":\n      step = center - Math.abs(index - center);\n      break;\n    default: // left-to-right\n      step = index;\n  }\n\n  const startMs = step * BAR_STAGGER * 1000;\n  const durationMs = BAR_GROW_DURATION * 1000;\n  const endMs = startMs + durationMs;\n  const elapsed = Date.now() - introStartedAt;\n\n  // Already finished — render static so re-renders/remounts can't replay it\n  if (elapsed >= endMs) return null;\n\n  // Resume from wherever this bar should already be (0 before it starts)\n  const from = elapsed <= startMs ? 0 : (elapsed - startMs) / durationMs;\n\n  return {\n    initial: { scaleY: from },\n    animate: { scaleY: 1 },\n    transition: {\n      duration: (endMs - Math.max(elapsed, startMs)) / 1000,\n      ease: REVEAL_EASE,\n      delay: Math.max(0, startMs - elapsed) / 1000,\n    },\n    style: { originY: 1 }, // grow upward from the baseline\n  };\n};\n\n// motion `originX` for each single-rect line reveal — the edge the wipe grows from\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, replacing Recharts' built-in animation.\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// Style definitions — one set per <Bar /> / <Line />, scoped to its unique id\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype StyleProps = {\n  id: string; // unique id of the owning series\n  dataKey: string; // series key the colors belong 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/** Vertical top-to-bottom color gradient — the fill source for every bar variant. */\nconst VerticalColorGradient = ({ id, dataKey, config }: StyleProps & { config: ChartConfig }) => {\n  const colorsCount = getColorsCount(config[dataKey] ?? {});\n\n  return (\n    <linearGradient id={`${id}-bar-colors`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\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/** Horizontal left-to-right color gradient — the stroke source for a line series. */\nconst HorizontalColorGradient = ({ id, dataKey, config }: StyleProps & { config: ChartConfig }) => {\n  const colorsCount = getColorsCount(config[dataKey] ?? {});\n\n  return (\n    <linearGradient id={`${id}-line-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/** Hatched diagonal-stripe fill for a bar, masked from the series color gradient. */\nconst HatchedPattern = ({ id }: StyleProps) => {\n  return (\n    <>\n      <pattern\n        id={`${id}-hatched-mask-pattern`}\n        x=\"0\"\n        y=\"0\"\n        width=\"5\"\n        height=\"5\"\n        patternUnits=\"userSpaceOnUse\"\n        patternTransform=\"rotate(-45)\"\n      >\n        <rect width=\"5\" height=\"5\" fill=\"white\" fillOpacity={0.3} />\n        <rect width=\"1.5\" height=\"5\" fill=\"white\" fillOpacity={1} />\n      </pattern>\n      <mask id={`${id}-hatched-mask`}>\n        <rect width=\"100%\" height=\"100%\" fill={`url(#${id}-hatched-mask-pattern)`} />\n      </mask>\n      <pattern id={`${id}-hatched`} patternUnits=\"userSpaceOnUse\" width=\"100%\" height=\"100%\">\n        <rect\n          width=\"100%\"\n          height=\"100%\"\n          fill={`url(#${id}-bar-colors)`}\n          mask={`url(#${id}-hatched-mask)`}\n        />\n      </pattern>\n    </>\n  );\n};\n\n/** Two-tone fill that splits each bar into a light and a full-strength half. */\nconst DuotonePattern = ({ id, dataKey, config }: StyleProps & { config: ChartConfig }) => {\n  const colorsCount = getColorsCount(config[dataKey] ?? {});\n\n  return (\n    <>\n      <linearGradient\n        id={`${id}-duotone-mask-gradient`}\n        gradientUnits=\"objectBoundingBox\"\n        x1=\"0\"\n        y1=\"0\"\n        x2=\"1\"\n        y2=\"0\"\n      >\n        <stop offset=\"50%\" stopColor=\"white\" stopOpacity={0.4} />\n        <stop offset=\"50%\" stopColor=\"white\" stopOpacity={1} />\n      </linearGradient>\n      <linearGradient\n        id={`${id}-duotone-colors`}\n        gradientUnits=\"objectBoundingBox\"\n        x1=\"0\"\n        y1=\"0\"\n        x2=\"0\"\n        y2=\"1\"\n      >\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      <mask id={`${id}-duotone-mask`} maskContentUnits=\"objectBoundingBox\">\n        <rect x=\"0\" y=\"0\" width=\"1\" height=\"1\" fill={`url(#${id}-duotone-mask-gradient)`} />\n      </mask>\n      <pattern\n        id={`${id}-duotone`}\n        patternUnits=\"objectBoundingBox\"\n        patternContentUnits=\"objectBoundingBox\"\n        width=\"1\"\n        height=\"1\"\n      >\n        <rect\n          x=\"0\"\n          y=\"0\"\n          width=\"1\"\n          height=\"1\"\n          fill={`url(#${id}-duotone-colors)`}\n          mask={`url(#${id}-duotone-mask)`}\n        />\n      </pattern>\n    </>\n  );\n};\n\n/** Two-tone fill mirrored from `duotone` — the full-strength half comes first. */\nconst DuotoneReversePattern = ({ id, dataKey, config }: StyleProps & { config: ChartConfig }) => {\n  const colorsCount = getColorsCount(config[dataKey] ?? {});\n\n  return (\n    <>\n      <linearGradient\n        id={`${id}-duotone-reverse-mask-gradient`}\n        gradientUnits=\"objectBoundingBox\"\n        x1=\"0\"\n        y1=\"0\"\n        x2=\"1\"\n        y2=\"0\"\n      >\n        <stop offset=\"50%\" stopColor=\"white\" stopOpacity={1} />\n        <stop offset=\"50%\" stopColor=\"white\" stopOpacity={0.4} />\n      </linearGradient>\n      <linearGradient\n        id={`${id}-duotone-reverse-colors`}\n        gradientUnits=\"objectBoundingBox\"\n        x1=\"0\"\n        y1=\"0\"\n        x2=\"0\"\n        y2=\"1\"\n      >\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      <mask id={`${id}-duotone-reverse-mask`} maskContentUnits=\"objectBoundingBox\">\n        <rect x=\"0\" y=\"0\" width=\"1\" height=\"1\" fill={`url(#${id}-duotone-reverse-mask-gradient)`} />\n      </mask>\n      <pattern\n        id={`${id}-duotone-reverse`}\n        patternUnits=\"objectBoundingBox\"\n        patternContentUnits=\"objectBoundingBox\"\n        width=\"1\"\n        height=\"1\"\n      >\n        <rect\n          x=\"0\"\n          y=\"0\"\n          width=\"1\"\n          height=\"1\"\n          fill={`url(#${id}-duotone-reverse-colors)`}\n          mask={`url(#${id}-duotone-reverse-mask)`}\n        />\n      </pattern>\n    </>\n  );\n};\n\n/** Gradient fill for a bar that fades from visible at the top toward transparent. */\nconst GradientPattern = ({ id }: StyleProps) => {\n  return (\n    <>\n      <linearGradient id={`${id}-gradient-mask-gradient`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n        <stop offset=\"20%\" stopColor=\"white\" stopOpacity={1} />\n        <stop offset=\"90%\" stopColor=\"white\" stopOpacity={0} />\n      </linearGradient>\n      <mask id={`${id}-gradient-mask`}>\n        <rect width=\"100%\" height=\"100%\" fill={`url(#${id}-gradient-mask-gradient)`} />\n      </mask>\n      <pattern id={`${id}-gradient`} patternUnits=\"userSpaceOnUse\" width=\"100%\" height=\"100%\">\n        <rect\n          width=\"100%\"\n          height=\"100%\"\n          fill={`url(#${id}-bar-colors)`}\n          mask={`url(#${id}-gradient-mask)`}\n        />\n      </pattern>\n    </>\n  );\n};\n\n/** Low-opacity gradient fill paired with the solid top strip drawn by `CustomBar`. */\nconst StrippedPattern = ({ id }: StyleProps) => {\n  return (\n    <>\n      <linearGradient id={`${id}-stripped-mask-gradient`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n        <stop offset=\"0%\" stopColor=\"white\" stopOpacity={0.4} />\n        <stop offset=\"100%\" stopColor=\"white\" stopOpacity={0.1} />\n      </linearGradient>\n      <mask id={`${id}-stripped-mask`}>\n        <rect width=\"100%\" height=\"100%\" fill={`url(#${id}-stripped-mask-gradient)`} />\n      </mask>\n      <pattern id={`${id}-stripped`} patternUnits=\"userSpaceOnUse\" width=\"100%\" height=\"100%\">\n        <rect\n          width=\"100%\"\n          height=\"100%\"\n          fill={`url(#${id}-bar-colors)`}\n          mask={`url(#${id}-stripped-mask)`}\n        />\n      </pattern>\n    </>\n  );\n};\n\n/** Soft outer-glow filter applied to a glowing bar. */\nconst BarGlowFilter = ({ id }: { id: string }) => {\n  return (\n    <filter id={`${id}-glow`} x=\"-100%\" y=\"-100%\" width=\"300%\" height=\"300%\">\n      <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"8\" result=\"blur\" />\n      <feColorMatrix\n        in=\"blur\"\n        type=\"matrix\"\n        values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 0.5 0\"\n        result=\"glow\"\n      />\n      <feMerge>\n        <feMergeNode in=\"glow\" />\n        <feMergeNode in=\"SourceGraphic\" />\n      </feMerge>\n    </filter>\n  );\n};\n\n/** Soft outer-glow filter applied to a glowing line. */\nconst LineGlowFilter = ({ id }: { id: string }) => {\n  return (\n    <filter id={`${id}-glow`} 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  0 1 0 0 0  0 0 1 0 0  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, loadingBars: number = 12) {\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(loadingBars, 20, 80),\n    // loadingDataKey toggle triggers re-computation when the shimmer exits\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [loadingBars, loadingDataKey],\n  );\n\n  return { loadingData, onShimmerExit };\n}\n\n/**\n * The skeleton bar shown while the chart is loading. Rendered by the root in\n * place of the real bars and lines, paired with its own masked shimmer pattern.\n */\nconst LoadingBar = ({\n  chartId,\n  barRadius,\n  onShimmerExit,\n}: {\n  chartId: string;\n  barRadius: number;\n  onShimmerExit: () => void;\n}) => {\n  return (\n    <>\n      <RechartsBar\n        dataKey={LOADING_DATA_KEY}\n        fill=\"currentColor\"\n        fillOpacity={0.15}\n        radius={barRadius}\n        isAnimationActive={false}\n        legendType=\"none\"\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 <EvilComposedChart.Bar/>, <EvilComposedChart.Line/>, … from a single import\n// — no colliding named marker exports when several charts share one file.\nEvilComposedChart.Bar = Bar;\nEvilComposedChart.Line = Line;\nEvilComposedChart.Dot = Dot;\nEvilComposedChart.ActiveDot = ActiveDot;\nEvilComposedChart.XAxis = XAxis;\nEvilComposedChart.YAxis = YAxis;\nEvilComposedChart.Grid = Grid;\nEvilComposedChart.Tooltip = Tooltip;\nEvilComposedChart.Legend = Legend;\nEvilComposedChart.Brush = Brush;\n",
      "type": "registry:component",
      "target": "components/evilcharts/charts/recharts-composed-chart.tsx"
    }
  ],
  "type": "registry:component"
}