{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "recharts-sankey-chart",
  "description": "Sankey chart component for visualizing flow data with nodes and links",
  "dependencies": [
    "recharts",
    "motion"
  ],
  "registryDependencies": [
    "@evilcharts/recharts-chart",
    "@evilcharts/recharts-tooltip",
    "@evilcharts/recharts-background"
  ],
  "files": [
    {
      "path": "src/registry/charts/recharts-sankey-chart.tsx",
      "content": "\"use client\";\n\nimport {\n  Sankey as RechartsSankey,\n  Layer,\n  type SankeyProps,\n  type SankeyNodeProps,\n  type SankeyLinkProps,\n  type SankeyData,\n  type SankeyNode as RechartsSankeyNode,\n} from \"recharts\";\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  use,\n  useCallback,\n  useId,\n  useMemo,\n  useState,\n  type FC,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport {\n  ChartTooltip,\n  ChartTooltipContent,\n  type TooltipRoundness,\n  type TooltipVariant,\n} from \"@/registry/ui/recharts-tooltip\";\nimport {\n  type ChartConfig,\n  ChartContainer,\n  getColorsCount,\n  LoadingIndicator,\n} from \"@/registry/ui/recharts-chart\";\nimport { ChartBackground, type BackgroundVariant } from \"@/registry/ui/recharts-background\";\nimport { motion } from \"motion/react\";\n\n// Constants\nconst LOADING_ANIMATION_DURATION = 2000; // full loading cycle duration in milliseconds\nconst DEFAULT_NODE_WIDTH = 10;\nconst DEFAULT_NODE_PADDING = 10;\nconst DEFAULT_LINK_CURVATURE = 0.5;\nconst DEFAULT_ITERATIONS = 32;\n\ntype LinkVariant = \"gradient\" | \"solid\" | \"source\" | \"target\";\ntype NodeLabelPosition = \"inside\" | \"outside\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Shared context\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Shared state for every part of the chart. Lifted into <EvilSankeyChart /> so\n * that <Node />, <Link />, and <Tooltip /> can read it without prop drilling.\n * A sankey chart's data is rigid — the root passes `nodes`/`links` straight to\n * Recharts — so the parts here configure how those nodes and links render.\n */\ntype SankeyChartContextValue = {\n  data: SankeyData; // the nodes + links rendered by the chart\n  config: ChartConfig; // colors + labels keyed by node name\n  chartId: string; // colon-free id scoping this chart's SVG defs\n  isLoading: boolean; // whether the chart shows its loading skeleton\n  selectedNode: string | null; // currently selected node name, or null when none\n  selectNode: (nodeName: string | null) => void; // sets the selected node\n};\n\nconst SankeyChartContext = createContext<SankeyChartContextValue | null>(null);\n\n// Reads the chart context, throwing a helpful error when used outside <EvilSankeyChart />\nfunction useSankeyChart() {\n  const context = use(SankeyChartContext);\n\n  if (!context) {\n    throw new Error(\n      \"Sankey chart parts (<Node />, <Link />, <Tooltip />, …) must be used within <EvilSankeyChart />\",\n    );\n  }\n\n  return context;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Root container\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype EvilSankeyChartBaseProps = {\n  data: SankeyData; // nodes + links rendered by the chart\n  config: ChartConfig; // node colors + labels keyed by node name\n  children: ReactNode; // composed parts — <Node />, <Link />, <Tooltip />, …\n  className?: string; // extra classes for the chart container\n  sankeyProps?: Omit<SankeyProps, \"data\">; // escape hatch for the raw Recharts Sankey\n  nodeWidth?: number; // width of each node in pixels\n  nodePadding?: number; // vertical gap between nodes in pixels\n  linkCurvature?: number; // link curve amount, 0 (straight) to 1 (maximum)\n  iterations?: number; // layout iterations — higher is more accurate\n  sort?: boolean; // sorts nodes automatically for an optimal layout\n  align?: \"left\" | \"justify\"; // horizontal node alignment strategy\n  verticalAlign?: \"justify\" | \"top\"; // vertical node alignment strategy\n  backgroundVariant?: BackgroundVariant; // background pattern behind the chart\n  defaultSelectedNode?: string | null; // node selected on first render\n  onSelectionChange?: (selection: { dataKey: string; value: number } | null) => void; // fires when the selected node changes\n  isLoading?: boolean; // shows the animated loading skeleton\n};\n\ntype EvilSankeyChartProps = EvilSankeyChartBaseProps;\n\n/**\n * Root of the composible sankey chart. Owns the flow data, the shared context,\n * the layout configuration, and the loading skeleton. The visual parts — the\n * nodes, links, and tooltip — are composed as children, so a consumer renders\n * exactly the parts they need with the styling they want.\n */\nexport function EvilSankeyChart({\n  data,\n  config,\n  children,\n  className,\n  sankeyProps,\n  nodeWidth = DEFAULT_NODE_WIDTH,\n  nodePadding = DEFAULT_NODE_PADDING,\n  linkCurvature = DEFAULT_LINK_CURVATURE,\n  iterations = DEFAULT_ITERATIONS,\n  sort = true,\n  align = \"justify\",\n  verticalAlign = \"justify\",\n  backgroundVariant,\n  defaultSelectedNode = null,\n  onSelectionChange,\n  isLoading = false,\n}: EvilSankeyChartProps) {\n  const chartId = useId().replace(/:/g, \"\"); // colon-free id keeps CSS/SVG selectors valid\n  const [selectedNode, setSelectedNode] = useState<string | null>(defaultSelectedNode);\n\n  // Updates selection state and notifies the parent with the node's flow value\n  const selectNode = useCallback(\n    (nodeName: string | null) => {\n      setSelectedNode(nodeName);\n\n      if (!onSelectionChange) return;\n\n      if (nodeName === null) {\n        onSelectionChange(null);\n        return;\n      }\n\n      onSelectionChange({ dataKey: nodeName, value: getNodeValue(data, nodeName) });\n    },\n    [onSelectionChange, data],\n  );\n\n  const contextValue = useMemo<SankeyChartContextValue>(\n    () => ({ data, config, chartId, isLoading, selectedNode, selectNode }),\n    [data, config, chartId, isLoading, selectedNode, selectNode],\n  );\n\n  return (\n    <SankeyChartContext value={contextValue}>\n      <ChartContainer className={className} config={config}>\n        <LoadingIndicator isLoading={isLoading} />\n        {backgroundVariant && <ChartBackground variant={backgroundVariant} />}\n        {!isLoading && (\n          <RechartsSankey\n            id={chartId}\n            data={data}\n            nodeWidth={nodeWidth}\n            nodePadding={nodePadding}\n            linkCurvature={linkCurvature}\n            iterations={iterations}\n            sort={sort}\n            align={align}\n            verticalAlign={verticalAlign}\n            {...resolveSankeyRenderers(children)}\n            {...sankeyProps}\n          >\n            {children}\n            <defs>\n              <NodeColorGradients config={config} chartId={chartId} />\n            </defs>\n          </RechartsSankey>\n        )}\n        {isLoading && (\n          <svg\n            viewBox=\"0 0 500 250\"\n            preserveAspectRatio=\"xMidYMid meet\"\n            width=\"100%\"\n            height=\"100%\"\n            className=\"absolute inset-0\"\n          >\n            <LoadingSankey />\n          </svg>\n        )}\n      </ChartContainer>\n    </SankeyChartContext>\n  );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Composible parts\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype NodeProps = {\n  radius?: number; // corner radius of node rectangles in pixels\n  isClickable?: boolean; // lets nodes be selected by clicking them\n  children?: ReactNode; // optional <NodeLabel /> composition\n};\n\n/**\n * Configures how the sankey nodes render. It is a configuration slot — the root\n * reads its props and wires them into the Recharts Sankey `node` renderer, so it\n * renders nothing itself. Compose a <NodeLabel /> inside it to show labels.\n */\nconst Node: FC<NodeProps> = () => null;\n\ntype NodeLabelProps = {\n  position?: NodeLabelPosition; // places labels inside or beside the nodes\n  showValues?: boolean; // appends each node's total flow value\n  valueFormatter?: (value: number) => string; // formats node values when shown\n};\n\n/**\n * Declares labels for the <Node /> it is composed inside. Like <Node />, it is a\n * configuration slot and renders nothing on its own.\n */\nconst NodeLabel: FC<NodeLabelProps> = () => null;\n\ntype LinkProps = {\n  variant?: LinkVariant; // coloring strategy for the link bands\n  verticalPadding?: number; // shrinks link width where it meets a node\n};\n\n/**\n * Configures how the sankey links render. Like <Node />, it is a configuration\n * slot read by the root and renders nothing itself. The `variant` controls how\n * each link band is colored.\n */\nconst Link: FC<LinkProps> = () => null;\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};\n\n/**\n * The hover tooltip. Reads the chart's loading state from context and is hidden\n * automatically while the chart shows its skeleton.\n */\nfunction Tooltip({ variant, roundness, defaultIndex }: TooltipProps) {\n  const { isLoading } = useSankeyChart();\n\n  if (isLoading) return null;\n\n  return (\n    <ChartTooltip\n      defaultIndex={defaultIndex}\n      content={\n        <ChartTooltipContent nameKey=\"name\" hideLabel roundness={roundness} variant={variant} />\n      }\n    />\n  );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Children resolution — turns composed <Node />/<Link /> into Sankey renderers\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Sums a node's outgoing flow, falling back to incoming flow for leaf nodes\nconst getNodeValue = (data: SankeyData, nodeName: string): number => {\n  const nodeIndex = data.nodes.findIndex((node) => node.name === nodeName);\n  if (nodeIndex === -1) return 0;\n\n  const outgoing = data.links\n    .filter((link) => link.source === nodeIndex)\n    .reduce((sum, link) => sum + link.value, 0);\n  const incoming = data.links\n    .filter((link) => link.target === nodeIndex)\n    .reduce((sum, link) => sum + link.value, 0);\n\n  return outgoing > 0 ? outgoing : incoming;\n};\n\n// Reads composed <Node /> and <Link /> children into the Sankey `node`/`link` render props\nconst resolveSankeyRenderers = (children: ReactNode): Pick<SankeyProps, \"node\" | \"link\"> => {\n  let nodeProps: NodeProps | null = null;\n  let linkProps: LinkProps | null = null;\n\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) return;\n\n    if (child.type === Node) {\n      nodeProps = (child as ReactElement<NodeProps>).props;\n    }\n\n    if (child.type === Link) {\n      linkProps = (child as ReactElement<LinkProps>).props;\n    }\n  });\n\n  return {\n    node: (props: SankeyNodeProps) => <SankeyNode {...props} nodeConfig={nodeProps} />,\n    link: (props: SankeyLinkProps) => <SankeyLink {...props} linkConfig={linkProps} />,\n  };\n};\n\n// Reads the <NodeLabel /> composed inside a <Node />, if any\nconst resolveNodeLabel = (children: ReactNode): NodeLabelProps | null => {\n  let label: NodeLabelProps | null = null;\n\n  Children.forEach(children, (child) => {\n    if (isValidElement(child) && child.type === NodeLabel) {\n      label = (child as ReactElement<NodeLabelProps>).props;\n    }\n  });\n\n  return label;\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Node renderer — draws a single sankey node from the resolved <Node /> config\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype SankeyNodeRendererProps = SankeyNodeProps & {\n  nodeConfig: NodeProps | null; // resolved props from the composed <Node />\n};\n\n/**\n * Renders a single sankey node rectangle, plus its optional label and value.\n * The root passes one of these per node, configured from the composed <Node />.\n */\nconst SankeyNode = ({ x, y, width, height, payload, nodeConfig }: SankeyNodeRendererProps) => {\n  const { config, chartId, data, selectedNode, selectNode } = useSankeyChart();\n\n  const radius = nodeConfig?.radius ?? 0;\n  const isClickable = nodeConfig?.isClickable ?? false;\n  const label = resolveNodeLabel(nodeConfig?.children);\n\n  const nodeName = payload.name;\n  const nodeValue = payload.value;\n  const nodeIcon = (payload as RechartsSankeyNode & { icon?: ReactNode }).icon;\n\n  const isHighlighted = isNodeConnected(data, selectedNode, nodeName);\n  const hasConfigColor = nodeName in config;\n  const configLabel = config[nodeName]?.label ?? nodeName;\n  const dimmed = isClickable && !isHighlighted;\n\n  const valueFormatter = label?.valueFormatter ?? ((value: number) => value.toLocaleString());\n  const showValues = label?.showValues ?? false;\n\n  const labelX = x + width / 2;\n  const labelY = showValues ? y + height / 2 - 8 : y + height / 2;\n  const valueY = y + height / 2 + 8;\n  const outsideLabelX = x + width + 8;\n  const outsideLabelY = y + height / 2;\n\n  return (\n    <Layer>\n      <rect\n        x={x}\n        y={y}\n        width={width}\n        height={height}\n        rx={radius}\n        ry={radius}\n        fill={hasConfigColor ? `url(#${chartId}-sankey-colors-${nodeName})` : \"currentColor\"}\n        fillOpacity={dimmed ? 0.15 : 0.9}\n        className=\"transition-opacity duration-200\"\n        style={isClickable ? { cursor: \"pointer\" } : undefined}\n        onClick={() => {\n          if (!isClickable) return;\n          selectNode(selectedNode === nodeName ? null : nodeName);\n        }}\n      />\n      {label?.position === \"inside\" && (\n        <>\n          <rect\n            x={x + 1}\n            y={y + 1}\n            width={width - 2}\n            height={height - 2}\n            rx={Math.max(0, radius - 1)}\n            ry={Math.max(0, radius - 1)}\n            opacity={dimmed ? 0.3 : 1}\n            className=\"fill-white/50 transition-opacity duration-200 dark:fill-black/60\"\n            style={{ pointerEvents: \"none\" }}\n          />\n          {nodeIcon && (\n            <foreignObject\n              x={labelX - 8}\n              y={labelY - 30}\n              width={16}\n              height={16}\n              opacity={dimmed ? 0.3 : 1}\n              className=\"transition-opacity duration-200\"\n              style={{ pointerEvents: \"none\" }}\n            >\n              <div className=\"text-foreground/80 flex items-center justify-center dark:text-white/80\">\n                {nodeIcon}\n              </div>\n            </foreignObject>\n          )}\n          <text\n            x={labelX}\n            y={nodeIcon ? labelY - 4 : labelY}\n            textAnchor=\"middle\"\n            dominantBaseline=\"middle\"\n            className=\"fill-foreground text-[10px] font-medium transition-opacity duration-200 dark:fill-white\"\n            opacity={dimmed ? 0.3 : 1}\n            style={{ pointerEvents: \"none\" }}\n          >\n            {configLabel}\n          </text>\n          {showValues && (\n            <text\n              x={labelX}\n              y={valueY}\n              textAnchor=\"middle\"\n              dominantBaseline=\"middle\"\n              className=\"fill-foreground/60 font-mono text-xs font-medium tabular-nums transition-opacity duration-200 dark:fill-white\"\n              opacity={dimmed ? 0.3 : 0.6}\n              style={{ pointerEvents: \"none\" }}\n            >\n              {valueFormatter(nodeValue)}\n            </text>\n          )}\n        </>\n      )}\n      {label?.position === \"outside\" && (\n        <>\n          <text\n            x={outsideLabelX}\n            y={outsideLabelY - (showValues ? 8 : 0)}\n            textAnchor=\"start\"\n            dominantBaseline=\"middle\"\n            className=\"fill-foreground text-xs\"\n            style={{ pointerEvents: \"none\" }}\n          >\n            {configLabel}\n          </text>\n          {showValues && (\n            <text\n              x={outsideLabelX}\n              y={outsideLabelY + 8}\n              textAnchor=\"start\"\n              dominantBaseline=\"middle\"\n              opacity={0.5}\n              className=\"fill-foreground font-mono text-xs tabular-nums dark:fill-white\"\n              style={{ pointerEvents: \"none\" }}\n            >\n              {valueFormatter(nodeValue)}\n            </text>\n          )}\n        </>\n      )}\n    </Layer>\n  );\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Link renderer — draws a single sankey link from the resolved <Link /> config\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype SankeyLinkRendererProps = SankeyLinkProps & {\n  linkConfig: LinkProps | null; // resolved props from the composed <Link />\n};\n\n/**\n * Renders a single sankey link band, colored by the composed <Link /> variant.\n * Highlights the bands connected to the selected node and dims the rest.\n */\nconst SankeyLink = ({\n  sourceX,\n  targetX,\n  sourceY,\n  targetY,\n  sourceControlX,\n  targetControlX,\n  linkWidth,\n  index,\n  payload,\n  linkConfig,\n}: SankeyLinkRendererProps) => {\n  const { config, chartId, selectedNode } = useSankeyChart();\n\n  const variant = linkConfig?.variant ?? \"gradient\";\n  const verticalPadding = linkConfig?.verticalPadding ?? 0;\n\n  const sourceName = payload.source.name;\n  const targetName = payload.target.name;\n\n  const isConnected =\n    selectedNode === null || selectedNode === sourceName || selectedNode === targetName;\n\n  const paddedLinkWidth = Math.max(1, linkWidth - verticalPadding);\n  const halfWidth = paddedLinkWidth / 2;\n\n  const linkAreaPath = `M${sourceX},${sourceY - halfWidth}\n    C${sourceControlX},${sourceY - halfWidth} ${targetControlX},${targetY - halfWidth} ${targetX},${targetY - halfWidth}\n    L${targetX},${targetY + halfWidth}\n    C${targetControlX},${targetY + halfWidth} ${sourceControlX},${sourceY + halfWidth} ${sourceX},${sourceY + halfWidth}\n    Z`;\n\n  return (\n    <Layer>\n      <defs>\n        {variant === \"gradient\" && (\n          <LinkGradient\n            chartId={chartId}\n            index={index}\n            config={config}\n            sourceName={sourceName}\n            targetName={targetName}\n          />\n        )}\n        <LinkStrokeGradient chartId={chartId} index={index} />\n      </defs>\n      <path\n        d={linkAreaPath}\n        fill={getLinkFill(variant, chartId, index, config, sourceName, targetName)}\n        fillOpacity={isConnected ? 0.4 : 0.1}\n        stroke={\n          selectedNode !== null && isConnected ? `url(#${chartId}-link-stroke-${index})` : \"none\"\n        }\n        strokeWidth={1}\n        strokeOpacity={1}\n        className=\"transition-opacity duration-200\"\n      />\n    </Layer>\n  );\n};\n\n// Checks whether a node is the selected one or directly linked to it\nconst isNodeConnected = (\n  data: SankeyData,\n  selectedNode: string | null,\n  nodeName: string,\n): boolean => {\n  if (selectedNode === null || selectedNode === nodeName) return true;\n\n  const selectedIdx = data.nodes.findIndex((node) => node.name === selectedNode);\n  const nodeIdx = data.nodes.findIndex((node) => node.name === nodeName);\n\n  return data.links.some(\n    (link) =>\n      (link.source === selectedIdx && link.target === nodeIdx) ||\n      (link.source === nodeIdx && link.target === selectedIdx),\n  );\n};\n\n// Resolves the SVG paint reference for a link band based on its variant\nconst getLinkFill = (\n  variant: LinkVariant,\n  chartId: string,\n  index: number,\n  config: ChartConfig,\n  sourceName: string,\n  targetName: string,\n): string => {\n  switch (variant) {\n    case \"gradient\":\n      return `url(#${chartId}-link-gradient-${index})`;\n    case \"source\":\n      return sourceName in config ? `url(#${chartId}-sankey-colors-${sourceName})` : \"currentColor\";\n    case \"target\":\n      return targetName in config ? `url(#${chartId}-sankey-colors-${targetName})` : \"currentColor\";\n    case \"solid\":\n    default:\n      return \"currentColor\";\n  }\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Style definitions — SVG defs scoped to the chart's unique id\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Vertical color gradient for every configured node, painted by name. */\nconst NodeColorGradients = ({ config, chartId }: { config: ChartConfig; chartId: string }) => {\n  return (\n    <>\n      {Object.entries(config).map(([dataKey, nodeConfig]) => {\n        const colorsCount = getColorsCount(nodeConfig);\n\n        return (\n          <linearGradient\n            key={`${chartId}-sankey-colors-${dataKey}`}\n            id={`${chartId}-sankey-colors-${dataKey}`}\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        );\n      })}\n    </>\n  );\n};\n\n/** Source-to-target fade gradient that fills a single gradient-variant link. */\nconst LinkGradient = ({\n  chartId,\n  index,\n  config,\n  sourceName,\n  targetName,\n}: {\n  chartId: string;\n  index: number;\n  config: ChartConfig;\n  sourceName: string;\n  targetName: string;\n}) => {\n  const sourceColor = sourceName in config ? `var(--color-${sourceName}-0)` : \"currentColor\";\n  const targetColor = targetName in config ? `var(--color-${targetName}-0)` : \"currentColor\";\n\n  return (\n    <linearGradient id={`${chartId}-link-gradient-${index}`} x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\">\n      <stop offset=\"0%\" stopColor={sourceColor} stopOpacity={0.2} />\n      <stop offset=\"50%\" stopColor={sourceColor} stopOpacity={0.5} />\n      <stop offset=\"100%\" stopColor={targetColor} stopOpacity={0.2} />\n    </linearGradient>\n  );\n};\n\n/** Primary-colored stroke gradient highlighting a link connected to the selection. */\nconst LinkStrokeGradient = ({ chartId, index }: { chartId: string; index: number }) => {\n  return (\n    <linearGradient id={`${chartId}-link-stroke-${index}`} x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\">\n      <stop offset=\"0%\" stopColor=\"var(--primary)\" stopOpacity={0} />\n      <stop offset=\"15%\" stopColor=\"var(--primary)\" stopOpacity={0.8} />\n      <stop offset=\"50%\" stopColor=\"var(--primary)\" stopOpacity={1} />\n      <stop offset=\"85%\" stopColor=\"var(--primary)\" stopOpacity={0.8} />\n      <stop offset=\"100%\" stopColor=\"var(--primary)\" stopOpacity={0} />\n    </linearGradient>\n  );\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Loading skeleton\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * The skeleton sankey shown while the chart is loading. Rendered by the root in\n * place of the real diagram — a fixed grid of pulsing nodes and links.\n */\nconst LoadingSankey = () => {\n  const nodes = [\n    { x: 30, y: 25, width: 12, height: 65, delay: 0 },\n    { x: 30, y: 110, width: 12, height: 50, delay: 0.3 },\n    { x: 30, y: 180, width: 12, height: 45, delay: 0.15 },\n    { x: 244, y: 20, width: 12, height: 55, delay: 0.45 },\n    { x: 244, y: 95, width: 12, height: 75, delay: 0.6 },\n    { x: 244, y: 190, width: 12, height: 40, delay: 0.25 },\n    { x: 458, y: 35, width: 12, height: 80, delay: 0.5 },\n    { x: 458, y: 135, width: 12, height: 90, delay: 0.1 },\n  ];\n\n  const links = [\n    { from: 0, to: 3, width: 26, delay: 0.2 },\n    { from: 0, to: 4, width: 18, delay: 0.7 },\n    { from: 1, to: 4, width: 24, delay: 0.4 },\n    { from: 1, to: 5, width: 12, delay: 0.9 },\n    { from: 2, to: 4, width: 16, delay: 0.1 },\n    { from: 2, to: 5, width: 14, delay: 0.55 },\n    { from: 3, to: 6, width: 22, delay: 0.35 },\n    { from: 3, to: 7, width: 18, delay: 0.8 },\n    { from: 4, to: 6, width: 28, delay: 0.05 },\n    { from: 4, to: 7, width: 32, delay: 0.65 },\n    { from: 5, to: 7, width: 16, delay: 0.45 },\n  ];\n\n  // Builds a bezier path connecting the right edge of one node to the left of another\n  const getLinkPath = (fromIdx: number, toIdx: number) => {\n    const from = nodes[fromIdx];\n    const to = nodes[toIdx];\n    const startX = from.x + from.width;\n    const startY = from.y + from.height / 2;\n    const endX = to.x;\n    const endY = to.y + to.height / 2;\n    const controlX1 = startX + (endX - startX) * 0.4;\n    const controlX2 = startX + (endX - startX) * 0.6;\n    return `M${startX},${startY} C${controlX1},${startY} ${controlX2},${endY} ${endX},${endY}`;\n  };\n\n  const baseDuration = LOADING_ANIMATION_DURATION / 1000;\n\n  return (\n    <>\n      {links.map((link, i) => (\n        <motion.path\n          key={`loading-link-${link.from}-${link.to}`}\n          d={getLinkPath(link.from, link.to)}\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth={link.width}\n          initial={{ opacity: 0.04 }}\n          animate={{ opacity: [0.04, 0.14, 0.04] }}\n          transition={{\n            duration: baseDuration * (0.8 + (i % 3) * 0.2),\n            delay: link.delay,\n            repeat: Infinity,\n            ease: \"easeInOut\",\n          }}\n        />\n      ))}\n      {nodes.map((node, i) => (\n        <motion.rect\n          key={`loading-node-${node.x}-${node.y}`}\n          x={node.x}\n          y={node.y}\n          width={node.width}\n          height={node.height}\n          rx={2}\n          fill=\"currentColor\"\n          initial={{ opacity: 0.15 }}\n          animate={{ opacity: [0.15, 0.4, 0.15] }}\n          transition={{\n            duration: baseDuration * (0.9 + (i % 4) * 0.1),\n            delay: node.delay,\n            repeat: Infinity,\n            ease: \"easeInOut\",\n          }}\n        />\n      ))}\n    </>\n  );\n};\n\n// Compound API: every part hangs off the root as a static member, so a consumer\n// writes <EvilSankeyChart.Node/>, <EvilSankeyChart.Tooltip/>, … from a single\n// import — no colliding named marker exports when several charts share one file.\nEvilSankeyChart.Node = Node;\nEvilSankeyChart.NodeLabel = NodeLabel;\nEvilSankeyChart.Link = Link;\nEvilSankeyChart.Tooltip = Tooltip;\n",
      "type": "registry:component",
      "target": "components/evilcharts/charts/recharts-sankey-chart.tsx"
    }
  ],
  "type": "registry:component"
}