{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sankey-chart",
  "type": "registry:component",
  "title": "Sankey Diagram",
  "description": "A flow diagram for visualizing data transfers between nodes",
  "dependencies": [
    "@visx/event@4.0.1-alpha.0",
    "@visx/gradient@4.0.1-alpha.0",
    "@visx/pattern@4.0.1-alpha.0",
    "@visx/responsive@4.0.1-alpha.0",
    "@visx/sankey@4.0.1-alpha.0",
    "d3-sankey",
    "motion"
  ],
  "registryDependencies": [
    "@bklit/chart-animation",
    "@bklit/utils",
    "@bklit/chart-utils",
    "@bklit/chart-tooltip"
  ],
  "files": [
    {
      "path": "src/charts/sankey/sankey-chart.tsx",
      "content": "\"use client\";\n\nimport { localPoint } from \"@visx/event\";\nimport { ParentSize } from \"@visx/responsive\";\nimport { sankey, sankeyCenter, sankeyLinkHorizontal } from \"@visx/sankey\";\nimport type { Transition } from \"motion/react\";\nimport {\n  memo,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  type Margin,\n  type SankeyLinkDatum,\n  type SankeyNodeDatum,\n  SankeyProvider,\n  type SankeyTooltipData,\n} from \"./sankey-context\";\n\nexport interface SankeyData {\n  nodes: SankeyNodeDatum[];\n  links: SankeyLinkDatum[];\n}\n\nexport interface SankeyChartProps {\n  /** Sankey data with nodes and links */\n  data: SankeyData;\n  /** Chart margins */\n  margin?: Partial<Margin>;\n  /** Animation duration in milliseconds. Default: 1100 */\n  animationDuration?: number;\n  /** Motion enter transition (spring or cubic-bezier tween). */\n  enterTransition?: Transition;\n  /** Signature of motion URL state — triggers enter replay when it changes. */\n  revealSignature?: string;\n  /** Aspect ratio as \"width / height\". Default: \"2 / 1\" */\n  aspectRatio?: string;\n  /** Node width in pixels. Default: 16 */\n  nodeWidth?: number;\n  /** Node padding in pixels. Default: 24 */\n  nodePadding?: number;\n  /** Additional class name for the container */\n  className?: string;\n  /** Child components (SankeyNode, SankeyLink, SankeyTooltip) */\n  children: ReactNode;\n  /** Controlled hovered node index (e.g. from ChartLegend). */\n  hoveredNodeIndex?: number | null;\n  /** Called when node hover changes from the chart surface. */\n  onNodeHoverChange?: (index: number | null) => void;\n}\n\nconst DEFAULT_MARGIN: Margin = { top: 40, right: 180, bottom: 40, left: 180 };\n\ninterface SankeyChartInnerProps {\n  data: SankeyData;\n  width: number;\n  height: number;\n  margin: Margin;\n  animationDuration: number;\n  enterTransition?: Transition;\n  revealSignature?: string;\n  nodeWidth: number;\n  nodePadding: number;\n  children: ReactNode;\n  hoveredNodeIndexProp?: number | null;\n  onNodeHoverChange?: (index: number | null) => void;\n}\n\nfunction SankeyChartInner(props: SankeyChartInnerProps) {\n  const { width, height } = props;\n\n  if (width < 10 || height < 10) {\n    return null;\n  }\n\n  return <SankeyChartCore {...props} />;\n}\n\nconst SankeyChartCore = memo(function SankeyChartCore({\n  data,\n  width,\n  height,\n  margin,\n  animationDuration,\n  enterTransition,\n  revealSignature = \"\",\n  nodeWidth,\n  nodePadding,\n  children,\n  hoveredNodeIndexProp,\n  onNodeHoverChange,\n}: SankeyChartInnerProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [isLoaded, setIsLoaded] = useState(false);\n  const [revealEpoch, setRevealEpoch] = useState(0);\n  const [internalHoveredNodeIndex, setInternalHoveredNodeIndex] = useState<\n    number | null\n  >(null);\n  const isNodeHoverControlled = hoveredNodeIndexProp !== undefined;\n  const hoveredNodeIndex = isNodeHoverControlled\n    ? hoveredNodeIndexProp\n    : internalHoveredNodeIndex;\n  const setHoveredNodeIndex = useCallback(\n    (index: number | null) => {\n      if (isNodeHoverControlled) {\n        onNodeHoverChange?.(index);\n      } else {\n        setInternalHoveredNodeIndex(index);\n      }\n    },\n    [isNodeHoverControlled, onNodeHoverChange]\n  );\n  const [hoveredLinkIndex, setHoveredLinkIndex] = useState<number | null>(null);\n  const [tooltipData, setTooltipData] = useState<SankeyTooltipData | null>(\n    null\n  );\n  const [mousePos, setMousePos] = useState<{ x: number; y: number } | null>(\n    null\n  );\n\n  const innerWidth = width - margin.left - margin.right;\n  const innerHeight = height - margin.top - margin.bottom;\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: revealSignature\n  useEffect(() => {\n    setRevealEpoch((n) => n + 1);\n    setIsLoaded(false);\n    const timeout = setTimeout(() => {\n      setIsLoaded(true);\n    }, animationDuration);\n    return () => clearTimeout(timeout);\n  }, [animationDuration, revealSignature]);\n\n  const sankeyGenerator = useMemo(() => {\n    return sankey<SankeyNodeDatum, SankeyLinkDatum>()\n      .nodeWidth(nodeWidth)\n      .nodePadding(nodePadding)\n      .nodeAlign(sankeyCenter)\n      .extent([\n        [0, 0],\n        [innerWidth, innerHeight],\n      ]);\n  }, [innerWidth, innerHeight, nodeWidth, nodePadding]);\n\n  const graph = useMemo(() => {\n    const clonedData = {\n      nodes: data.nodes.map((node) => ({ ...node })),\n      links: data.links.map((link) => ({ ...link })),\n    };\n    return sankeyGenerator(clonedData);\n  }, [data, sankeyGenerator]);\n\n  const createPath = useCallback(\n    // biome-ignore lint/suspicious/noExplicitAny: d3-sankey types are complex\n    (link: any) => {\n      try {\n        const pathGenerator = sankeyLinkHorizontal();\n        return pathGenerator(link) || \"\";\n      } catch {\n        return \"\";\n      }\n    },\n    []\n  );\n\n  const handleMouseMove = useCallback((event: React.MouseEvent) => {\n    const point = localPoint(event);\n    if (point) {\n      setMousePos({ x: point.x, y: point.y });\n    }\n  }, []);\n\n  const handleMouseLeave = useCallback(() => {\n    setHoveredNodeIndex(null);\n    setHoveredLinkIndex(null);\n    setTooltipData(null);\n    setMousePos(null);\n  }, [setHoveredNodeIndex]);\n\n  const contextValue = {\n    graph,\n    nodes: graph.nodes,\n    links: graph.links,\n    width,\n    height,\n    innerWidth,\n    innerHeight,\n    margin,\n    hoveredNodeIndex,\n    hoveredLinkIndex,\n    setHoveredNodeIndex,\n    setHoveredLinkIndex,\n    tooltipData,\n    setTooltipData,\n    containerRef,\n    isLoaded,\n    animationDuration,\n    enterTransition,\n    revealEpoch,\n    mousePos,\n    createPath,\n  };\n\n  return (\n    <SankeyProvider value={contextValue}>\n      <div className=\"relative h-full w-full\" ref={containerRef}>\n        <svg\n          aria-hidden=\"true\"\n          height={height}\n          onMouseLeave={handleMouseLeave}\n          onMouseMove={handleMouseMove}\n          width={width}\n        >\n          <g transform={`translate(${margin.left},${margin.top})`}>\n            {children}\n          </g>\n        </svg>\n      </div>\n    </SankeyProvider>\n  );\n});\n\nexport function SankeyChart({\n  data,\n  margin: marginProp,\n  animationDuration = 1100,\n  enterTransition,\n  revealSignature,\n  aspectRatio = \"2 / 1\",\n  nodeWidth = 16,\n  nodePadding = 24,\n  className = \"\",\n  children,\n  hoveredNodeIndex,\n  onNodeHoverChange,\n}: SankeyChartProps) {\n  const margin = { ...DEFAULT_MARGIN, ...marginProp };\n\n  return (\n    <div className={cn(\"relative w-full\", className)} style={{ aspectRatio }}>\n      <ParentSize>\n        {({ width, height }) => (\n          <SankeyChartInner\n            animationDuration={animationDuration}\n            data={data}\n            enterTransition={enterTransition}\n            height={height}\n            hoveredNodeIndexProp={hoveredNodeIndex}\n            margin={margin}\n            nodePadding={nodePadding}\n            nodeWidth={nodeWidth}\n            onNodeHoverChange={onNodeHoverChange}\n            revealSignature={revealSignature}\n            width={width}\n          >\n            {children}\n          </SankeyChartInner>\n        )}\n      </ParentSize>\n    </div>\n  );\n}\n\nSankeyChart.displayName = \"SankeyChart\";\n\nexport default SankeyChart;\n",
      "type": "registry:component",
      "target": "components/charts/sankey/sankey-chart.tsx"
    },
    {
      "path": "src/charts/sankey/sankey-context.tsx",
      "content": "\"use client\";\n\nimport type { SankeyGraph, SankeyLink, SankeyNode } from \"d3-sankey\";\nimport type { Transition } from \"motion/react\";\nimport {\n  createContext,\n  type Dispatch,\n  type RefObject,\n  type SetStateAction,\n  useContext,\n} from \"react\";\n\nexport interface Margin {\n  top: number;\n  right: number;\n  bottom: number;\n  left: number;\n}\n\nexport interface SankeyNodeDatum {\n  name: string;\n  category?: \"source\" | \"landing\" | \"outcome\";\n  [key: string]: unknown;\n}\n\nexport interface SankeyLinkDatum {\n  source: number;\n  target: number;\n  value: number;\n  [key: string]: unknown;\n}\n\nexport interface SankeyTooltipData {\n  type: \"node\" | \"link\";\n  nodeIndex?: number;\n  linkIndex?: number;\n  x: number;\n  y: number;\n  data: SankeyNodeDatum | SankeyLinkDatum;\n}\n\nexport interface SankeyContextValue {\n  // Layout data\n  graph: SankeyGraph<SankeyNodeDatum, SankeyLinkDatum>;\n  nodes: SankeyNode<SankeyNodeDatum, SankeyLinkDatum>[];\n  links: SankeyLink<SankeyNodeDatum, SankeyLinkDatum>[];\n\n  // Dimensions\n  width: number;\n  height: number;\n  innerWidth: number;\n  innerHeight: number;\n  margin: Margin;\n\n  // Hover state\n  hoveredNodeIndex: number | null;\n  hoveredLinkIndex: number | null;\n  setHoveredNodeIndex: (index: number | null) => void;\n  setHoveredLinkIndex: (index: number | null) => void;\n\n  // Tooltip\n  tooltipData: SankeyTooltipData | null;\n  setTooltipData: Dispatch<SetStateAction<SankeyTooltipData | null>>;\n  containerRef: RefObject<HTMLDivElement | null>;\n\n  // Animation\n  isLoaded: boolean;\n  animationDuration: number;\n  /** Motion enter transition (spring or cubic-bezier tween). */\n  enterTransition?: Transition;\n  /** Increments when enter animation should replay. */\n  revealEpoch: number;\n\n  // Mouse position for dynamic tooltips\n  mousePos: { x: number; y: number } | null;\n\n  // Link path generator\n  createPath: (link: SankeyLink<SankeyNodeDatum, SankeyLinkDatum>) => string;\n}\n\nconst SankeyContext = createContext<SankeyContextValue | null>(null);\n\nexport function SankeyProvider({\n  children,\n  value,\n}: {\n  children: React.ReactNode;\n  value: SankeyContextValue;\n}) {\n  return (\n    <SankeyContext.Provider value={value}>{children}</SankeyContext.Provider>\n  );\n}\n\nexport function useSankey(): SankeyContextValue {\n  const context = useContext(SankeyContext);\n  if (!context) {\n    throw new Error(\"useSankey must be used within a SankeyProvider\");\n  }\n  return context;\n}\n\n// CSS variables for sankey theming\nexport const sankeyCssVars = {\n  background: \"var(--chart-background)\",\n  foreground: \"var(--chart-foreground)\",\n  nodePrimary: \"var(--chart-line-primary)\",\n  nodeSecondary: \"var(--chart-line-secondary)\",\n  linkColor: \"var(--chart-foreground-muted, hsl(0, 0%, 50%))\",\n};\n",
      "type": "registry:component",
      "target": "components/charts/sankey/sankey-context.tsx"
    },
    {
      "path": "src/charts/sankey/sankey-node.tsx",
      "content": "\"use client\";\n\nimport type { SankeyNode as SankeyNodeType } from \"d3-sankey\";\nimport { motion, type Transition } from \"motion/react\";\nimport { type ReactNode, useCallback, useMemo } from \"react\";\nimport { intFmt } from \"../chart-formatters\";\nimport { transitionWithDelay } from \"../motion-utils\";\nimport {\n  type SankeyLinkDatum,\n  type SankeyNodeDatum,\n  useSankey,\n} from \"./sankey-context\";\n\n// Helper to get node index from link source/target\ntype NodeOrIndex = SankeyNodeType<SankeyNodeDatum, SankeyLinkDatum> | number;\n\nexport type SankeyLabelOrientation = \"horizontal\" | \"vertical\";\n\nfunction getNodeIndex(nodeOrIndex: NodeOrIndex): number | undefined {\n  if (typeof nodeOrIndex === \"number\") {\n    return nodeOrIndex;\n  }\n  return nodeOrIndex.index;\n}\n\nexport interface SankeyNodeProps {\n  /** Fill color for nodes. Default: uses theme colors */\n  fill?: string;\n  /** Corner radius for nodes. Default: 4 */\n  lineCap?: number;\n  /** Opacity when another node/link is hovered. Default: 0.4 */\n  fadedOpacity?: number;\n  /** Show node labels. Default: true */\n  showLabels?: boolean;\n  /** Show value labels under node names. Default: true */\n  showValueLabels?: boolean;\n  /**\n   * Label reading direction for outside node labels.\n   * - \"horizontal\": labels sit left/right of nodes (default).\n   * - \"vertical\": labels rotate 90° and read along the node edge.\n   */\n  labelOrientation?: SankeyLabelOrientation;\n  /** Custom node color function */\n  getNodeColor?: (\n    node: SankeyNodeType<SankeyNodeDatum, SankeyLinkDatum>,\n    index: number\n  ) => string;\n}\n\ntype TextAnchor = \"start\" | \"middle\" | \"end\";\n\ninterface NodeLabelLayout {\n  x: number;\n  y: number;\n  textAnchor: TextAnchor;\n  dy: string;\n  textLocalX: number;\n  rotate: number;\n  initialX: number;\n  initialY: number;\n}\n\nconst LABEL_OFFSET = 12;\nconst VALUE_LABEL_GAP = 16;\n\nfunction getNodeLabelLayouts({\n  labelOrientation,\n  isLeftSide,\n  x,\n  y,\n  width,\n  height,\n  showValueLabels,\n}: {\n  labelOrientation: SankeyLabelOrientation;\n  isLeftSide: boolean;\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n  showValueLabels: boolean;\n}): { name: NodeLabelLayout; value: NodeLabelLayout | null } {\n  const centerY = y + height / 2;\n  const initialX = isLeftSide ? x + 8 : x + width - 8;\n\n  if (labelOrientation === \"horizontal\") {\n    const labelX = isLeftSide ? x - LABEL_OFFSET : x + width + LABEL_OFFSET;\n    const textAnchor: TextAnchor = isLeftSide ? \"end\" : \"start\";\n\n    return {\n      name: {\n        x: labelX,\n        y: centerY,\n        textAnchor,\n        dy: \"0.35em\",\n        textLocalX: 0,\n        rotate: 0,\n        initialX,\n        initialY: centerY,\n      },\n      value: showValueLabels\n        ? {\n            x: labelX,\n            y: centerY + VALUE_LABEL_GAP,\n            textAnchor,\n            dy: \"0.35em\",\n            textLocalX: 0,\n            rotate: 0,\n            initialX,\n            initialY: centerY + VALUE_LABEL_GAP,\n          }\n        : null,\n    };\n  }\n\n  const labelX = isLeftSide ? x - LABEL_OFFSET : x + width + LABEL_OFFSET;\n  const rotate = isLeftSide ? -90 : 90;\n  const halfGap = VALUE_LABEL_GAP / 2;\n  let nameLocalX = 0;\n  if (showValueLabels) {\n    nameLocalX = isLeftSide ? halfGap : -halfGap;\n  }\n  const valueLocalX = isLeftSide ? -halfGap : halfGap;\n\n  return {\n    name: {\n      x: labelX,\n      y: centerY,\n      textAnchor: \"middle\",\n      dy: \"0.35em\",\n      textLocalX: nameLocalX,\n      rotate,\n      initialX,\n      initialY: centerY,\n    },\n    value: showValueLabels\n      ? {\n          x: labelX,\n          y: centerY,\n          textAnchor: \"middle\",\n          dy: \"0.35em\",\n          textLocalX: valueLocalX,\n          rotate,\n          initialX,\n          initialY: centerY,\n        }\n      : null,\n  };\n}\n\ninterface AnimatedNodeProps {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n  fill: string;\n  rx: number;\n  index: number;\n  totalNodes: number;\n  isFaded: boolean;\n  fadedOpacity: number;\n  animationDuration: number;\n  onMouseEnter: () => void;\n  onMouseLeave: () => void;\n  name: string;\n  value: number;\n  isLeftSide: boolean;\n  showLabels: boolean;\n  showValueLabels: boolean;\n  labelOrientation: SankeyLabelOrientation;\n}\n\nfunction NodeLabel({\n  layout,\n  opacity,\n  transition,\n  className,\n  children,\n}: {\n  layout: NodeLabelLayout;\n  opacity: number;\n  transition: Transition;\n  className: string;\n  children: ReactNode;\n}) {\n  return (\n    <motion.g\n      animate={{\n        opacity,\n        x: layout.x,\n        y: layout.y,\n        rotate: layout.rotate,\n      }}\n      initial={{\n        opacity: 0,\n        x: layout.initialX,\n        y: layout.initialY,\n        rotate: layout.rotate,\n      }}\n      transition={transition}\n    >\n      <text\n        className={className}\n        dy={layout.dy}\n        textAnchor={layout.textAnchor}\n        x={layout.textLocalX}\n      >\n        {children}\n      </text>\n    </motion.g>\n  );\n}\n\nfunction AnimatedNode({\n  x,\n  y,\n  width,\n  height,\n  fill,\n  rx,\n  index,\n  totalNodes,\n  isFaded,\n  fadedOpacity,\n  animationDuration,\n  onMouseEnter,\n  onMouseLeave,\n  name,\n  value,\n  isLeftSide,\n  showLabels,\n  showValueLabels,\n  labelOrientation,\n}: AnimatedNodeProps) {\n  const { enterTransition, revealEpoch } = useSankey();\n\n  const nodeAnimDuration = animationDuration * 0.6;\n  const staggerDelaySec =\n    ((index / totalNodes) * nodeAnimDuration * 0.4) / 1000;\n  const nameLabelDelaySec =\n    staggerDelaySec + (nodeAnimDuration * 0.6 * 0.3) / 1000;\n  const valueLabelDelaySec = nameLabelDelaySec + 0.06;\n\n  const nodeEnter = transitionWithDelay(enterTransition, staggerDelaySec);\n  const nameEnter = transitionWithDelay(enterTransition, nameLabelDelaySec);\n  const valueEnter = transitionWithDelay(enterTransition, valueLabelDelaySec);\n  const nodeOpacity = isFaded ? fadedOpacity : 1;\n  const nameOpacity = isFaded ? fadedOpacity : 1;\n  const valueOpacity = isFaded ? fadedOpacity * 0.8 : 0.6;\n  const labelLayouts = getNodeLabelLayouts({\n    labelOrientation,\n    isLeftSide,\n    x,\n    y,\n    width,\n    height,\n    showValueLabels,\n  });\n\n  return (\n    <motion.g\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n      style={{ cursor: \"pointer\" }}\n    >\n      <motion.rect\n        animate={{ opacity: nodeOpacity, scaleY: 1 }}\n        fill={fill}\n        height={height}\n        initial={{ opacity: 0, scaleY: 0 }}\n        key={`node-${index}-${revealEpoch}`}\n        rx={rx}\n        ry={rx}\n        style={{ originY: 0.5 }}\n        transition={nodeEnter}\n        width={width}\n        x={x}\n        y={y}\n      />\n      {showLabels ? (\n        <>\n          <NodeLabel\n            className=\"fill-foreground font-medium text-[13px]\"\n            key={`name-${index}-${revealEpoch}`}\n            layout={labelLayouts.name}\n            opacity={nameOpacity}\n            transition={nameEnter}\n          >\n            {name}\n          </NodeLabel>\n          {labelLayouts.value ? (\n            <NodeLabel\n              className=\"fill-foreground text-[11px]\"\n              key={`value-${index}-${revealEpoch}`}\n              layout={labelLayouts.value}\n              opacity={valueOpacity}\n              transition={valueEnter}\n            >\n              {intFmt(value)} sessions\n            </NodeLabel>\n          ) : null}\n        </>\n      ) : null}\n    </motion.g>\n  );\n}\n\nexport function SankeyNode({\n  fill,\n  lineCap = 4,\n  fadedOpacity = 0.4,\n  showLabels = true,\n  showValueLabels = true,\n  labelOrientation = \"horizontal\",\n  getNodeColor: getNodeColorProp,\n}: SankeyNodeProps) {\n  const {\n    nodes,\n    links,\n    width,\n    margin,\n    hoveredNodeIndex,\n    hoveredLinkIndex,\n    setHoveredNodeIndex,\n    setTooltipData,\n    animationDuration,\n  } = useSankey();\n\n  // Default colors using CSS variables\n  const defaultColors = useMemo(\n    () => [\n      \"var(--chart-1)\",\n      \"var(--chart-2)\",\n      \"var(--chart-3)\",\n      \"var(--chart-4)\",\n      \"var(--chart-5)\",\n    ],\n    []\n  );\n\n  // Get color for a node\n  const getColor = useCallback(\n    (\n      node: SankeyNodeType<SankeyNodeDatum, SankeyLinkDatum>,\n      index: number\n    ): string => {\n      if (fill) {\n        return fill;\n      }\n      if (getNodeColorProp) {\n        return getNodeColorProp(node, index);\n      }\n\n      return defaultColors[index % defaultColors.length] ?? \"var(--chart-1)\";\n    },\n    [fill, getNodeColorProp, defaultColors]\n  );\n\n  // Check if a node is connected to the hovered element\n  const isNodeConnected = useCallback(\n    (nodeIndex: number) => {\n      if (hoveredNodeIndex !== null) {\n        if (hoveredNodeIndex === nodeIndex) {\n          return true;\n        }\n        return links.some((link) => {\n          const sIdx = getNodeIndex(link.source as NodeOrIndex);\n          const tIdx = getNodeIndex(link.target as NodeOrIndex);\n          return (\n            (sIdx === hoveredNodeIndex && tIdx === nodeIndex) ||\n            (tIdx === hoveredNodeIndex && sIdx === nodeIndex)\n          );\n        });\n      }\n      if (hoveredLinkIndex !== null) {\n        const link = links[hoveredLinkIndex];\n        if (!link) {\n          return false;\n        }\n        const sIdx = getNodeIndex(link.source as NodeOrIndex);\n        const tIdx = getNodeIndex(link.target as NodeOrIndex);\n        return sIdx === nodeIndex || tIdx === nodeIndex;\n      }\n      return false;\n    },\n    [hoveredNodeIndex, hoveredLinkIndex, links]\n  );\n\n  const isAnyHovered = hoveredNodeIndex !== null || hoveredLinkIndex !== null;\n  const innerWidth = width - margin.left - margin.right;\n\n  return (\n    <g className=\"sankey-nodes\">\n      {nodes.map((node, index) => {\n        const nodeX = node.x0 ?? 0;\n        const nodeY = node.y0 ?? 0;\n        const nodeWidth = (node.x1 ?? 0) - nodeX;\n        const nodeHeight = (node.y1 ?? 0) - nodeY;\n\n        const isConnected = isNodeConnected(index);\n        const isFaded = isAnyHovered && !isConnected;\n        const isLeftSide = nodeX < innerWidth / 2;\n\n        let displayValue = 0;\n        for (const l of links) {\n          const sIdx = getNodeIndex(l.source as NodeOrIndex);\n          const tIdx = getNodeIndex(l.target as NodeOrIndex);\n          if (node.category === \"source\" && sIdx === index) {\n            displayValue += l.value;\n          } else if (node.category !== \"source\" && tIdx === index) {\n            displayValue += l.value;\n          }\n        }\n\n        const handleMouseEnter = () => {\n          setHoveredNodeIndex(index);\n          setTooltipData({\n            type: \"node\",\n            nodeIndex: index,\n            x: 0,\n            y: 0,\n            data: node,\n          });\n        };\n\n        const handleMouseLeave = () => {\n          setHoveredNodeIndex(null);\n          setTooltipData(null);\n        };\n\n        return (\n          <AnimatedNode\n            animationDuration={animationDuration}\n            fadedOpacity={fadedOpacity}\n            fill={getColor(node, index)}\n            height={nodeHeight}\n            index={index}\n            isFaded={isFaded}\n            isLeftSide={isLeftSide}\n            key={`node-${node.name}`}\n            labelOrientation={labelOrientation}\n            name={node.name}\n            onMouseEnter={handleMouseEnter}\n            onMouseLeave={handleMouseLeave}\n            rx={lineCap}\n            showLabels={showLabels}\n            showValueLabels={showValueLabels}\n            totalNodes={nodes.length}\n            value={displayValue}\n            width={nodeWidth}\n            x={nodeX}\n            y={nodeY}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\nSankeyNode.displayName = \"SankeyNode\";\n\nexport default SankeyNode;\n",
      "type": "registry:component",
      "target": "components/charts/sankey/sankey-node.tsx"
    },
    {
      "path": "src/charts/sankey/sankey-link.tsx",
      "content": "\"use client\";\n\nimport type {\n  SankeyLink as SankeyLinkType,\n  SankeyNode as SankeyNodeType,\n} from \"d3-sankey\";\nimport { motion, useTransform } from \"motion/react\";\nimport { useCallback, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport { useMountProgress } from \"../use-mount-progress\";\nimport {\n  type SankeyLinkDatum,\n  type SankeyNodeDatum,\n  useSankey,\n} from \"./sankey-context\";\n\n// Helper to get node index from link source/target\ntype NodeOrIndex = SankeyNodeType<SankeyNodeDatum, SankeyLinkDatum> | number;\n\nfunction getNodeIndex(nodeOrIndex: NodeOrIndex): number | undefined {\n  if (typeof nodeOrIndex === \"number\") {\n    return nodeOrIndex;\n  }\n  return nodeOrIndex.index;\n}\n\nfunction getNodeObject(\n  nodeOrIndex: NodeOrIndex\n): SankeyNodeType<SankeyNodeDatum, SankeyLinkDatum> | null {\n  if (typeof nodeOrIndex === \"number\") {\n    return null;\n  }\n  return nodeOrIndex;\n}\n\n// Default node color palette using CSS variables\nconst defaultColors = [\n  \"var(--chart-1)\",\n  \"var(--chart-2)\",\n  \"var(--chart-3)\",\n  \"var(--chart-4)\",\n  \"var(--chart-5)\",\n];\n\nfunction getDefaultNodeColor(\n  node: SankeyNodeType<SankeyNodeDatum, SankeyLinkDatum>\n): string {\n  const index = node.index ?? 0;\n  return defaultColors[index % defaultColors.length] ?? \"var(--chart-1)\";\n}\n\nexport interface SankeyLinkProps {\n  /** Stroke color for links (overrides gradient). Default: uses gradient */\n  stroke?: string;\n  /** Stroke opacity. Default: 0.5 */\n  strokeOpacity?: number;\n  /** Opacity when another link/node is hovered. Default: 0.1 */\n  fadedOpacity?: number;\n  /** Use gradient from source to target color. Default: true */\n  useGradient?: boolean;\n  /** Custom function to get node color (for gradient) */\n  getNodeColor?: (\n    node: SankeyNodeType<SankeyNodeDatum, SankeyLinkDatum>,\n    index: number\n  ) => string;\n  /** Custom link color function (overrides gradient) */\n  getLinkColor?: (\n    link: SankeyLinkType<SankeyNodeDatum, SankeyLinkDatum>,\n    index: number\n  ) => string;\n  /** Pattern definitions to render in defs. Use @visx/pattern components (PatternLines, PatternCircles, etc.) */\n  patterns?: React.ReactNode;\n  /** Return pattern ID for a link, or null/undefined to use gradient/solid color */\n  getLinkPattern?: (\n    link: SankeyLinkType<SankeyNodeDatum, SankeyLinkDatum>,\n    index: number\n  ) => string | null | undefined;\n}\n\ninterface AnimatedLinkProps {\n  path: string;\n  width: number;\n  stroke: string;\n  strokeOpacity: number;\n  index: number;\n  totalLinks: number;\n  isFaded: boolean;\n  isHighlighted: boolean;\n  fadedOpacity: number;\n  animationDuration: number;\n  onMouseEnter: () => void;\n  onMouseLeave: () => void;\n}\n\nfunction AnimatedLink({\n  path,\n  width,\n  stroke,\n  strokeOpacity,\n  index,\n  totalLinks,\n  isFaded,\n  isHighlighted,\n  fadedOpacity,\n  animationDuration,\n  onMouseEnter,\n  onMouseLeave,\n}: AnimatedLinkProps) {\n  const { enterTransition, revealEpoch } = useSankey();\n  const pathRef = useRef<SVGPathElement>(null);\n  const [pathLength, setPathLength] = useState(0);\n\n  // Links animate during the last 80% of total duration, starting at 20%\n  const linkStartDelay = animationDuration * 0.2;\n  const linkAnimDuration = animationDuration * 0.8;\n  const staggerDelaySeconds =\n    (linkStartDelay + (index / totalLinks) * linkAnimDuration * 0.4) / 1000;\n\n  useLayoutEffect(() => {\n    if (pathRef.current) {\n      const length = pathRef.current.getTotalLength();\n      setPathLength(length);\n    }\n  });\n\n  const progress = useMountProgress(\n    enterTransition,\n    staggerDelaySeconds,\n    `${revealEpoch}-${index}`\n  );\n  const strokeDashoffset = useTransform(progress, [0, 1], [pathLength, 0]);\n\n  // Calculate target opacity\n  const getTargetOpacity = () => {\n    if (isFaded) {\n      return fadedOpacity;\n    }\n    if (isHighlighted) {\n      return Math.min(1, strokeOpacity * 1.3);\n    }\n    return strokeOpacity;\n  };\n  const targetOpacity = getTargetOpacity();\n\n  // Dasharray for path reveal\n  const dashArray = pathLength > 0 ? `${pathLength} ${pathLength}` : \"none\";\n\n  // Ensure opacity values are always numbers\n  const initialOpacity = strokeOpacity ?? 0.5;\n  const animatedOpacity = targetOpacity ?? initialOpacity;\n\n  return (\n    <motion.path\n      animate={{ opacity: animatedOpacity }}\n      d={path}\n      fill=\"none\"\n      initial={{ opacity: initialOpacity }}\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n      ref={pathRef}\n      stroke={stroke}\n      strokeDasharray={dashArray}\n      strokeWidth={Math.max(1, width)}\n      style={{\n        cursor: \"pointer\",\n        strokeDashoffset,\n      }}\n      transition={{ duration: 0.18, ease: \"easeOut\" }}\n    />\n  );\n}\n\nexport function SankeyLink({\n  stroke,\n  strokeOpacity = 0.5,\n  fadedOpacity = 0.1,\n  useGradient = true,\n  getNodeColor,\n  getLinkColor,\n  patterns,\n  getLinkPattern,\n}: SankeyLinkProps) {\n  const {\n    links,\n    hoveredNodeIndex,\n    hoveredLinkIndex,\n    setHoveredLinkIndex,\n    setTooltipData,\n    animationDuration,\n    createPath,\n  } = useSankey();\n\n  // Get color for a node (for gradients)\n  const getNodeColorFn = useCallback(\n    (node: SankeyNodeType<SankeyNodeDatum, SankeyLinkDatum>): string => {\n      if (getNodeColor) {\n        return getNodeColor(node, node.index ?? 0);\n      }\n      return getDefaultNodeColor(node);\n    },\n    [getNodeColor]\n  );\n\n  // Get color for a link (solid color, when not using gradient)\n  const getLinkColorFn = useCallback(\n    (link: SankeyLinkType<SankeyNodeDatum, SankeyLinkDatum>, index: number) => {\n      if (getLinkColor) {\n        return getLinkColor(link, index);\n      }\n      return stroke || \"var(--chart-line-primary)\";\n    },\n    [getLinkColor, stroke]\n  );\n\n  // Check if any element is hovered\n  const isAnyHovered = hoveredNodeIndex !== null || hoveredLinkIndex !== null;\n\n  // Build gradient definitions for all links\n  const gradientDefs = useMemo(() => {\n    if (!useGradient || stroke || getLinkColor) {\n      return null;\n    }\n\n    return links.map((link, index) => {\n      const sourceNode = getNodeObject(link.source as NodeOrIndex);\n      const targetNode = getNodeObject(link.target as NodeOrIndex);\n\n      // Always define a gradient so `url(#...)` never points to a missing id.\n      // Use fallback colors if nodes can't be resolved\n      const sourceColor = sourceNode\n        ? getNodeColorFn(sourceNode)\n        : \"var(--chart-1)\";\n      const targetColor = targetNode\n        ? getNodeColorFn(targetNode)\n        : \"var(--chart-1)\";\n      const gradientId = `link-gradient-${index}`;\n\n      // Get absolute x positions for gradient\n      // Use userSpaceOnUse to avoid issues with horizontal links (where bounding box has zero height)\n      const x1 = sourceNode?.x1 ?? 0;\n      const x2 = targetNode?.x0 ?? 100;\n\n      return (\n        <linearGradient\n          gradientUnits=\"userSpaceOnUse\"\n          id={gradientId}\n          key={gradientId}\n          x1={x1}\n          x2={x2}\n          y1=\"0\"\n          y2=\"0\"\n        >\n          <stop offset=\"0%\" stopColor={sourceColor} stopOpacity={1} />\n          <stop offset=\"100%\" stopColor={targetColor} stopOpacity={1} />\n        </linearGradient>\n      );\n    });\n  }, [links, useGradient, stroke, getLinkColor, getNodeColorFn]);\n\n  return (\n    <g className=\"sankey-links\">\n      {/* Pattern and gradient definitions */}\n      <defs>\n        {patterns}\n        {gradientDefs}\n      </defs>\n\n      {/* Links */}\n      {links.map((link, index) => {\n        const path = createPath(link);\n        const linkWidth = link.width ?? 1;\n\n        // Skip if path is empty\n        if (!path || path.trim() === \"\") {\n          return null;\n        }\n\n        const sIdx = getNodeIndex(link.source as NodeOrIndex);\n        const tIdx = getNodeIndex(link.target as NodeOrIndex);\n\n        // Use fallback indices if we can't resolve\n        const sourceIdx =\n          sIdx ?? (typeof link.source === \"number\" ? link.source : -1);\n        const targetIdx =\n          tIdx ?? (typeof link.target === \"number\" ? link.target : -1);\n\n        const isHighlighted =\n          hoveredLinkIndex === index ||\n          hoveredNodeIndex === sourceIdx ||\n          hoveredNodeIndex === targetIdx;\n        const isFaded = isAnyHovered && !isHighlighted;\n\n        const handleMouseEnter = () => {\n          setHoveredLinkIndex(index);\n          setTooltipData({\n            type: \"link\",\n            linkIndex: index,\n            x: 0,\n            y: 0,\n            data: link,\n          });\n        };\n\n        const handleMouseLeave = () => {\n          setHoveredLinkIndex(null);\n          setTooltipData(null);\n        };\n\n        // Determine stroke color (pattern URL, gradient URL, or solid color)\n        let linkStroke: string;\n        const patternId = getLinkPattern?.(link, index);\n        if (patternId) {\n          // Use pattern fill\n          linkStroke = `url(#${patternId})`;\n        } else if (useGradient && !stroke && !getLinkColor) {\n          // Use gradient\n          linkStroke = `url(#link-gradient-${index})`;\n        } else {\n          linkStroke = getLinkColorFn(link, index);\n        }\n\n        return (\n          <AnimatedLink\n            animationDuration={animationDuration}\n            fadedOpacity={fadedOpacity}\n            index={index}\n            isFaded={isFaded}\n            isHighlighted={isHighlighted}\n            key={`link-${sourceIdx}-${targetIdx}-${link.width ?? link.value ?? \"\"}`}\n            onMouseEnter={handleMouseEnter}\n            onMouseLeave={handleMouseLeave}\n            path={path}\n            stroke={linkStroke}\n            strokeOpacity={strokeOpacity}\n            totalLinks={links.length}\n            width={linkWidth}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\nSankeyLink.displayName = \"SankeyLink\";\n\nexport default SankeyLink;\n",
      "type": "registry:component",
      "target": "components/charts/sankey/sankey-link.tsx"
    },
    {
      "path": "src/charts/sankey/sankey-tooltip.tsx",
      "content": "\"use client\";\n\nimport type { SankeyLink, SankeyNode } from \"d3-sankey\";\nimport { intFmt } from \"../chart-formatters\";\nimport { TooltipBox } from \"../tooltip/tooltip-box\";\nimport { TooltipContent, type TooltipRow } from \"../tooltip/tooltip-content\";\nimport {\n  type SankeyLinkDatum,\n  type SankeyNodeDatum,\n  useSankey,\n} from \"./sankey-context\";\n\n// Helper to get node name from link source/target\ntype NodeOrIndex = SankeyNode<SankeyNodeDatum, SankeyLinkDatum> | number;\n\nfunction getNodeName(nodeOrIndex: NodeOrIndex, fallbackIndex: number): string {\n  if (typeof nodeOrIndex === \"number\") {\n    return `Node ${nodeOrIndex}`;\n  }\n  return nodeOrIndex.name ?? `Node ${fallbackIndex}`;\n}\n\nexport interface SankeyTooltipProps {\n  /** Custom content renderer for node tooltips */\n  nodeContent?: (props: {\n    node: SankeyNode<SankeyNodeDatum, SankeyLinkDatum>;\n    index: number;\n  }) => React.ReactNode;\n  /** Custom content renderer for link tooltips */\n  linkContent?: (props: {\n    link: SankeyLink<SankeyNodeDatum, SankeyLinkDatum>;\n    index: number;\n  }) => React.ReactNode;\n  /** Value formatter function */\n  formatValue?: (value: number) => string;\n  /** Custom class name */\n  className?: string;\n}\n\nexport function SankeyTooltip({\n  nodeContent,\n  linkContent,\n  formatValue = intFmt,\n  className = \"\",\n}: SankeyTooltipProps) {\n  const {\n    tooltipData,\n    containerRef,\n    width,\n    height,\n    margin,\n    nodes,\n    links,\n    mousePos,\n  } = useSankey();\n\n  if (!tooltipData) {\n    return null;\n  }\n\n  // Use mouse position if available, otherwise fallback to anchor point\n  const x = mousePos ? mousePos.x : tooltipData.x + margin.left;\n  const y = mousePos ? mousePos.y : tooltipData.y + margin.top;\n\n  // Render node tooltip\n  if (tooltipData.type === \"node\" && tooltipData.nodeIndex !== undefined) {\n    const node = nodes[tooltipData.nodeIndex];\n    if (!node) {\n      return null;\n    }\n\n    // Calculate total value flowing through this node\n    const totalValue = node.value ?? 0;\n\n    // Custom content\n    if (nodeContent) {\n      return (\n        <TooltipBox\n          className={className}\n          containerHeight={height}\n          containerRef={containerRef}\n          containerWidth={width}\n          visible\n          x={x}\n          y={y}\n        >\n          {nodeContent({ node, index: tooltipData.nodeIndex })}\n        </TooltipBox>\n      );\n    }\n\n    // Default node tooltip\n    const rows: TooltipRow[] = [\n      {\n        color: \"var(--chart-line-primary)\",\n        label: \"Sessions\",\n        value: formatValue(totalValue),\n      },\n    ];\n\n    return (\n      <TooltipBox\n        className={className}\n        containerHeight={height}\n        containerRef={containerRef}\n        containerWidth={width}\n        visible\n        x={x}\n        y={y}\n      >\n        <TooltipContent rows={rows} title={node.name} />\n      </TooltipBox>\n    );\n  }\n\n  // Render link tooltip\n  if (tooltipData.type === \"link\" && tooltipData.linkIndex !== undefined) {\n    const link = links[tooltipData.linkIndex];\n    if (!link) {\n      return null;\n    }\n\n    // Get source and target names\n    const sourceName = getNodeName(\n      link.source as NodeOrIndex,\n      tooltipData.linkIndex\n    );\n    const targetName = getNodeName(\n      link.target as NodeOrIndex,\n      tooltipData.linkIndex\n    );\n\n    // Custom content\n    if (linkContent) {\n      return (\n        <TooltipBox\n          className={className}\n          containerHeight={height}\n          containerRef={containerRef}\n          containerWidth={width}\n          visible\n          x={x}\n          y={y}\n        >\n          {linkContent({ link, index: tooltipData.linkIndex })}\n        </TooltipBox>\n      );\n    }\n\n    // Default link tooltip\n    const rows: TooltipRow[] = [\n      {\n        color: \"var(--chart-foreground-muted)\",\n        label: \"Flow\",\n        value: formatValue(link.value),\n      },\n    ];\n\n    return (\n      <TooltipBox\n        className={className}\n        containerHeight={height}\n        containerRef={containerRef}\n        containerWidth={width}\n        visible\n        x={x}\n        y={y}\n      >\n        <TooltipContent rows={rows} title={`${sourceName} → ${targetName}`} />\n      </TooltipBox>\n    );\n  }\n\n  return null;\n}\n\nSankeyTooltip.displayName = \"SankeyTooltip\";\n\nexport default SankeyTooltip;\n",
      "type": "registry:component",
      "target": "components/charts/sankey/sankey-tooltip.tsx"
    },
    {
      "path": "src/charts/sankey/index.ts",
      "content": "export {\n  SankeyChart,\n  type SankeyChartProps,\n  type SankeyData,\n} from \"./sankey-chart\";\nexport {\n  type Margin,\n  type SankeyContextValue,\n  type SankeyLinkDatum,\n  type SankeyNodeDatum,\n  SankeyProvider,\n  type SankeyTooltipData,\n  sankeyCssVars,\n  useSankey,\n} from \"./sankey-context\";\nexport { SankeyLink, type SankeyLinkProps } from \"./sankey-link\";\nexport {\n  type SankeyLabelOrientation,\n  SankeyNode,\n  type SankeyNodeProps,\n} from \"./sankey-node\";\nexport { SankeyTooltip, type SankeyTooltipProps } from \"./sankey-tooltip\";\n",
      "type": "registry:component",
      "target": "components/charts/sankey/index.ts"
    }
  ]
}