{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sankey-chart",
  "title": "Sankey Chart",
  "description": "A styled EvilCharts sankey chart for weighted flows between stages.",
  "dependencies": [
    "motion",
    "recharts"
  ],
  "registryDependencies": [
    "https://ui.corr.sh/r/chart-empty-loading-error.json"
  ],
  "files": [
    {
      "path": "src/components/evilcharts/charts/sankey-chart.tsx",
      "content": "\"use client\";\n\nimport {\n  ChartTooltip,\n  ChartTooltipContent,\n  type TooltipRoundness,\n  type TooltipVariant,\n} from \"@/components/evilcharts/ui/tooltip\";\nimport {\n  type ChartConfig,\n  ChartContainer,\n  getColorsCount,\n  LoadingIndicator,\n} from \"@/components/evilcharts/ui/chart\";\nimport type {\n  SankeyProps,\n  SankeyNodeProps,\n  SankeyLinkProps,\n  SankeyData,\n  SankeyNode,\n} from \"recharts\";\nimport { ChartBackground, type BackgroundVariant } from \"@/components/evilcharts/ui/background\";\nimport { useCallback, useId, useState, type ReactNode } from \"react\";\nimport { Sankey, Layer } from \"recharts\";\nimport { motion } from \"motion/react\";\n\n// Loading animation constants\nconst LOADING_ANIMATION_DURATION = 2000; // Full cycle duration in ms\n\n// Constants\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\";\n\n// Node label position type\ntype NodeLabelPosition = \"inside\" | \"outside\" | \"none\";\n\ntype EvilSankeyChartProps = {\n  // Data\n  data: SankeyData;\n  chartConfig: ChartConfig;\n  className?: string;\n  sankeyProps?: Omit<SankeyProps, \"data\">;\n\n  // Layout\n  nodeWidth?: number;\n  nodePadding?: number;\n  linkCurvature?: number;\n  iterations?: number;\n  sort?: boolean;\n  align?: \"left\" | \"justify\";\n  verticalAlign?: \"justify\" | \"top\";\n\n  // Styling\n  linkVariant?: LinkVariant;\n  nodeRadius?: number;\n  linkVerticalPadding?: number;\n\n  // Node Labels\n  showNodeLabels?: NodeLabelPosition;\n  showNodeValues?: boolean;\n  nodeValueFormatter?: (value: number) => string;\n\n  // Hide Stuffs\n  hideTooltip?: boolean;\n  // Tooltip\n  tooltipRoundness?: TooltipRoundness;\n  tooltipVariant?: TooltipVariant;\n  tooltipDefaultIndex?: number;\n\n  // Interactive Stuffs\n  isLoading?: boolean;\n\n  // Glow Effects\n  glowingNodes?: string[];\n  glowingLinks?: number[];\n  // Background\n  backgroundVariant?: BackgroundVariant;\n};\n\ntype EvilSankeyChartClickable = {\n  isClickable: true;\n  onSelectionChange?: (selection: { dataKey: string; value: number } | null) => void;\n};\n\ntype EvilSankeyChartNotClickable = {\n  isClickable?: false;\n  onSelectionChange?: never;\n};\n\ntype EvilSankeyChartPropsWithCallback = EvilSankeyChartProps &\n  (EvilSankeyChartClickable | EvilSankeyChartNotClickable);\n\nexport function EvilSankeyChart({\n  data,\n  chartConfig,\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  linkVariant = \"gradient\",\n  nodeRadius = 0,\n  linkVerticalPadding = 0,\n  showNodeLabels = \"none\",\n  showNodeValues = false,\n  nodeValueFormatter = (value: number) => value.toLocaleString(),\n  hideTooltip = false,\n  tooltipRoundness,\n  tooltipVariant,\n  tooltipDefaultIndex,\n  isClickable = false,\n  isLoading = false,\n  glowingNodes = [],\n  glowingLinks = [],\n  onSelectionChange,\n  backgroundVariant,\n}: EvilSankeyChartPropsWithCallback) {\n  const [selectedNode, setSelectedNode] = useState<string | null>(null);\n  const chartId = useId().replace(/:/g, \"\");\n\n  // Handler to update selection and call callback\n  const handleNodeClick = useCallback(\n    (nodeName: string | null) => {\n      setSelectedNode(nodeName);\n      if (isClickable && onSelectionChange) {\n        if (nodeName === null) {\n          onSelectionChange(null);\n        } else {\n          // Calculate node value: sum of all outgoing links (or incoming if no outgoing)\n          const nodeIndex = data.nodes.findIndex((node) => node.name === nodeName);\n          if (nodeIndex !== -1) {\n            // Sum of outgoing links\n            const outgoingValue = data.links\n              .filter((link) => link.source === nodeIndex)\n              .reduce((sum, link) => sum + link.value, 0);\n            // Sum of incoming links (if no outgoing)\n            const incomingValue = data.links\n              .filter((link) => link.target === nodeIndex)\n              .reduce((sum, link) => sum + link.value, 0);\n            // Use outgoing value if available, otherwise incoming\n            const value = outgoingValue > 0 ? outgoingValue : incomingValue;\n            onSelectionChange({ dataKey: nodeName, value });\n          }\n        }\n      }\n    },\n    [isClickable, onSelectionChange, data],\n  );\n\n  return (\n    <ChartContainer className={className} config={chartConfig}>\n      <LoadingIndicator isLoading={isLoading} />\n      {backgroundVariant && <ChartBackground variant={backgroundVariant} />}\n      {!isLoading && (\n        <Sankey\n          id=\"evil-charts-sankey-chart\"\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          node={(props: SankeyNodeProps) => (\n            <CustomNode\n              {...props}\n              chartId={chartId}\n              chartConfig={chartConfig}\n              data={data}\n              selectedNode={selectedNode}\n              isClickable={isClickable}\n              nodeRadius={nodeRadius}\n              showNodeLabels={showNodeLabels}\n              showNodeValues={showNodeValues}\n              nodeValueFormatter={nodeValueFormatter}\n              glowingNodes={glowingNodes}\n              onNodeClick={(name: string) => {\n                if (!isClickable) return;\n                handleNodeClick(selectedNode === name ? null : name);\n              }}\n            />\n          )}\n          link={(props: SankeyLinkProps) => (\n            <CustomLink\n              {...props}\n              chartId={chartId}\n              chartConfig={chartConfig}\n              selectedNode={selectedNode}\n              linkVariant={linkVariant}\n              linkVerticalPadding={linkVerticalPadding}\n              glowingLinks={glowingLinks}\n            />\n          )}\n          {...sankeyProps}\n        >\n          {!hideTooltip && (\n            <ChartTooltip\n              defaultIndex={tooltipDefaultIndex}\n              content={\n                <ChartTooltipContent\n                  nameKey=\"name\"\n                  hideLabel\n                  roundness={tooltipRoundness}\n                  variant={tooltipVariant}\n                />\n              }\n            />\n          )}\n          {/* ======== CHART STYLES ======== */}\n          <defs>\n            {/* Color gradients for nodes */}\n            <NodeColorGradientStyle chartConfig={chartConfig} chartId={chartId} />\n\n            {/* Glow filters for nodes */}\n            {glowingNodes.length > 0 && (\n              <GlowFilterStyle chartId={chartId} glowingNodes={glowingNodes} type=\"node\" />\n            )}\n\n            {/* Glow filters for links */}\n            {glowingLinks.length > 0 && (\n              <GlowFilterStyle\n                chartId={chartId}\n                glowingNodes={glowingLinks.map(String)}\n                type=\"link\"\n              />\n            )}\n          </defs>\n        </Sankey>\n      )}\n\n      {/* Loading state */}\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  );\n}\n\n// Custom node component with labels, icons, and glow effects\ntype CustomNodeProps = SankeyNodeProps & {\n  chartId: string;\n  chartConfig: ChartConfig;\n  data: SankeyData;\n  selectedNode: string | null;\n  isClickable: boolean;\n  nodeRadius: number;\n  showNodeLabels: NodeLabelPosition;\n  showNodeValues: boolean;\n  nodeValueFormatter: (value: number) => string;\n  glowingNodes: string[];\n  onNodeClick: (name: string) => void;\n};\n\nconst CustomNode = ({\n  x,\n  y,\n  width,\n  height,\n  payload,\n  chartId,\n  chartConfig,\n  data,\n  selectedNode,\n  isClickable,\n  nodeRadius,\n  showNodeLabels,\n  showNodeValues,\n  nodeValueFormatter,\n  glowingNodes,\n  onNodeClick,\n}: CustomNodeProps) => {\n  const nodeName = payload.name;\n  const nodeValue = payload.value;\n  const nodeIcon = (payload as SankeyNode & { icon?: ReactNode }).icon;\n\n  // Check if this node is the selected one, or connected to the selected one via a link\n  const isConnectedToSelected = (() => {\n    if (selectedNode === null) return true;\n    if (selectedNode === nodeName) return true;\n    const selectedIdx = data.nodes.findIndex((n) => n.name === selectedNode);\n    const thisIdx = data.nodes.findIndex((n) => n.name === nodeName);\n    return data.links.some(\n      (link) =>\n        (link.source === selectedIdx && link.target === thisIdx) ||\n        (link.source === thisIdx && link.target === selectedIdx),\n    );\n  })();\n  const isSelected = isConnectedToSelected;\n  const isGlowing = glowingNodes.includes(nodeName);\n\n  const hasConfigColor = nodeName in chartConfig;\n  const configLabel = chartConfig[nodeName]?.label ?? nodeName;\n\n  const getFilter = () => {\n    if (isGlowing) return `url(#${chartId}-node-glow-${nodeName})`;\n    return undefined;\n  };\n\n  // Calculate positions for inside labels\n  const labelX = x + width / 2;\n  const labelY = showNodeValues ? y + height / 2 - 8 : y + height / 2;\n  const valueY = y + height / 2 + 8;\n\n  // Calculate positions for outside labels (to the right of the node)\n  const outsideLabelX = x + width + 8;\n  const outsideLabelY = y + height / 2;\n\n  return (\n    <Layer>\n      {/* Main node rectangle - using native rect for proper rx/ry support */}\n      <rect\n        x={x}\n        y={y}\n        width={width}\n        height={height}\n        rx={nodeRadius}\n        ry={nodeRadius}\n        fill={hasConfigColor ? `url(#${chartId}-sankey-colors-${nodeName})` : \"currentColor\"}\n        fillOpacity={isClickable && !isSelected ? 0.3 : 0.9}\n        filter={getFilter()}\n        className=\"transition-opacity duration-200\"\n        style={isClickable ? { cursor: \"pointer\" } : undefined}\n        onClick={() => onNodeClick(nodeName)}\n      />\n\n      {/* Inside labels */}\n      {showNodeLabels === \"inside\" && (\n        <>\n          {/* Background overlay for label readability */}\n          <rect\n            x={x + 1}\n            y={y + 1}\n            width={width - 2}\n            height={height - 2}\n            rx={Math.max(0, nodeRadius - 1)}\n            ry={Math.max(0, nodeRadius - 1)}\n            opacity={isClickable && !isSelected ? 0.3 : 1}\n            className=\"fill-white/50 transition-opacity duration-200 dark:fill-black/60\"\n            style={{ pointerEvents: \"none\" }}\n          />\n\n          {/* Icon if provided */}\n          {nodeIcon && (\n            <foreignObject\n              x={labelX - 8}\n              y={labelY - 30}\n              width={16}\n              height={16}\n              opacity={isClickable && !isSelected ? 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\n          {/* Label text */}\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={isClickable && !isSelected ? 0.3 : 1}\n            style={{ pointerEvents: \"none\" }}\n          >\n            {configLabel}\n          </text>\n\n          {/* Value text */}\n          {showNodeValues && (\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={isClickable && !isSelected ? 0.3 : 0.6}\n              style={{ pointerEvents: \"none\" }}\n            >\n              {nodeValueFormatter(nodeValue)}\n            </text>\n          )}\n        </>\n      )}\n\n      {/* Outside labels (to the side of nodes) */}\n      {showNodeLabels === \"outside\" && (\n        <>\n          <text\n            x={outsideLabelX}\n            y={outsideLabelY - (showNodeValues ? 8 : 0)}\n            textAnchor=\"start\"\n            dominantBaseline=\"middle\"\n            className=\"fill-foreground text-xs\"\n            style={{ pointerEvents: \"none\" }}\n          >\n            {configLabel}\n          </text>\n\n          {showNodeValues && (\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              {nodeValueFormatter(nodeValue)}\n            </text>\n          )}\n        </>\n      )}\n    </Layer>\n  );\n};\n\n// Custom link component with gradient variants and connection highlighting\ntype CustomLinkProps = SankeyLinkProps & {\n  chartId: string;\n  chartConfig: ChartConfig;\n  selectedNode: string | null;\n  linkVariant: LinkVariant;\n  linkVerticalPadding: number;\n  glowingLinks: number[];\n};\n\nconst CustomLink = ({\n  sourceX,\n  targetX,\n  sourceY,\n  targetY,\n  sourceControlX,\n  targetControlX,\n  linkWidth,\n  index,\n  payload,\n  chartId,\n  chartConfig,\n  selectedNode,\n  linkVariant,\n  linkVerticalPadding,\n  glowingLinks,\n}: CustomLinkProps) => {\n  const sourceName = payload.source.name;\n  const targetName = payload.target.name;\n\n  // Check if either source or target is selected\n  const isConnected =\n    selectedNode === null || selectedNode === sourceName || selectedNode === targetName;\n\n  const isGlowing = glowingLinks.includes(index);\n\n  const getFilter = () => {\n    if (isGlowing) return `url(#${chartId}-link-glow-${index})`;\n    return undefined;\n  };\n\n  // Calculate link fill based on variant\n  const getLinkFill = () => {\n    const hasSourceColor = sourceName in chartConfig;\n    const hasTargetColor = targetName in chartConfig;\n\n    switch (linkVariant) {\n      case \"gradient\":\n        // Create a unique gradient for this link\n        return `url(#${chartId}-link-gradient-${index})`;\n      case \"source\":\n        return hasSourceColor ? `url(#${chartId}-sankey-colors-${sourceName})` : \"currentColor\";\n      case \"target\":\n        return hasTargetColor ? `url(#${chartId}-sankey-colors-${targetName})` : \"currentColor\";\n      case \"solid\":\n      default:\n        return \"currentColor\";\n    }\n  };\n\n  // Apply vertical padding to the link width (reduces stroke width to create padding effect)\n  const paddedLinkWidth = Math.max(1, linkWidth - linkVerticalPadding);\n  const halfWidth = paddedLinkWidth / 2;\n\n  // Build a closed area path for the link band (top edge forward, bottom edge backward)\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      {/* Define gradient for this specific link if using gradient variant */}\n      <defs>\n        {/* Gradient fill for links */}\n        {linkVariant === \"gradient\" && (\n          <linearGradient\n            id={`${chartId}-link-gradient-${index}`}\n            x1=\"0%\"\n            y1=\"0%\"\n            x2=\"100%\"\n            y2=\"0%\"\n          >\n            <stop\n              offset=\"0%\"\n              stopColor={\n                sourceName in chartConfig ? `var(--color-${sourceName}-0)` : \"currentColor\"\n              }\n              stopOpacity={0.2}\n            />\n            <stop\n              offset=\"50%\"\n              stopColor={\n                sourceName in chartConfig ? `var(--color-${sourceName}-0)` : \"currentColor\"\n              }\n              stopOpacity={0.5}\n            />\n            <stop\n              offset=\"100%\"\n              stopColor={\n                targetName in chartConfig ? `var(--color-${targetName}-0)` : \"currentColor\"\n              }\n              stopOpacity={0.2}\n            />\n          </linearGradient>\n        )}\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      </defs>\n\n      <path\n        d={linkAreaPath}\n        fill={getLinkFill()}\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={0.3}\n        filter={getFilter()}\n        className=\"transition-opacity duration-200\"\n      />\n    </Layer>\n  );\n};\n\n// Animated skeleton loading state with nodes and links\nconst LoadingSankey = () => {\n  // Full-width loading skeleton with 3 columns of nodes (with padding from edges)\n  const nodes = [\n    // Column 1 (left side with padding)\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    // Column 2 (center)\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    // Column 3 (right side with padding)\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  // Links with unique delays for varied animation timing\n  const links = [\n    // Column 1 -> Column 2\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    // Column 2 -> Column 3\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  // Generate bezier path between two nodes\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      {/* Loading links */}\n      {links.map((link, i) => (\n        <motion.path\n          key={`loading-link-${i}`}\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\n      {/* Loading nodes */}\n      {nodes.map((node, i) => (\n        <motion.rect\n          key={`loading-node-${i}`}\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// Create vertical color gradient for sankey nodes\nconst NodeColorGradientStyle = ({\n  chartConfig,\n  chartId,\n}: {\n  chartConfig: ChartConfig;\n  chartId: string;\n}) => {\n  return (\n    <>\n      {Object.entries(chartConfig).map(([dataKey, config]) => {\n        const colorsCount = getColorsCount(config);\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                <stop\n                  key={index}\n                  offset={`${(index / (colorsCount - 1)) * 100}%`}\n                  stopColor={`var(--color-${dataKey}-${index}, var(--color-${dataKey}-0))`}\n                />\n              ))\n            )}\n          </linearGradient>\n        );\n      })}\n    </>\n  );\n};\n\n// Apply soft glow filter effect to nodes or links using SVG filters\nconst GlowFilterStyle = ({\n  chartId,\n  glowingNodes,\n  type,\n}: {\n  chartId: string;\n  glowingNodes: string[];\n  type: \"node\" | \"link\";\n}) => {\n  return (\n    <>\n      {glowingNodes.map((nodeName) => (\n        <filter\n          key={`${chartId}-${type}-glow-${nodeName}`}\n          id={`${chartId}-${type}-glow-${nodeName}`}\n          x=\"-200%\"\n          y=\"-200%\"\n          width=\"400%\"\n          height=\"400%\"\n        >\n          <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"6\" 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.6 0\"\n            result=\"glow\"\n          />\n          <feMerge>\n            <feMergeNode in=\"glow\" />\n            <feMergeNode in=\"SourceGraphic\" />\n          </feMerge>\n        </filter>\n      ))}\n    </>\n  );\n};\n",
      "type": "registry:component"
    },
    {
      "path": "src/components/evilcharts/ui/chart.tsx",
      "content": "\"use client\";\n\nimport * as RechartsPrimitive from \"recharts\";\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const;\n\ntype ThemeKey = keyof typeof THEMES;\n\n// All Keys are optional at first\ntype ThemeColorsBase = {\n  [K in ThemeKey]?: string[];\n};\n\n// Require at least one theme key\ntype AtLeastOneThemeColor = {\n  [K in ThemeKey]: Required<Pick<ThemeColorsBase, K>> & Partial<Omit<ThemeColorsBase, K>>;\n}[ThemeKey];\n\nconst VALID_THEME_KEYS = Object.keys(THEMES) as ThemeKey[];\n\n// Validation for chart config colors at runtime\nfunction validateChartConfigColors(config: ChartConfig): void {\n  for (const [key, value] of Object.entries(config)) {\n    if (value.colors) {\n      const hasValidThemeKey = VALID_THEME_KEYS.some(\n        (themeKey) => value.colors?.[themeKey] !== undefined,\n      );\n\n      if (!hasValidThemeKey) {\n        throw new Error(\n          `[EvilCharts] Invalid chart config for \"${key}\": colors object must have at least one theme key (${VALID_THEME_KEYS.join(\", \")}). Received empty object or invalid keys.`,\n        );\n      }\n    }\n  }\n}\n\nexport type ChartConfig = Record<\n  string,\n  {\n    label?: React.ReactNode;\n    icon?: React.ComponentType;\n    colors?: AtLeastOneThemeColor;\n  }\n>;\n\ninterface ChartContextProps {\n  config: ChartConfig;\n}\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null);\n\nexport function useChart() {\n  const context = React.useContext(ChartContext);\n\n  if (!context) {\n    throw new Error(\"useChart must be used within a <ChartContainer />\");\n  }\n\n  return context;\n}\n\ninterface ChartContainerProps\n  extends\n    Omit<React.ComponentProps<\"div\">, \"children\">,\n    Pick<\n      React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>,\n      | \"initialDimension\"\n      | \"aspect\"\n      | \"debounce\"\n      | \"minHeight\"\n      | \"minWidth\"\n      | \"maxHeight\"\n      | \"height\"\n      | \"width\"\n      | \"onResize\"\n      | \"children\"\n    > {\n  config: ChartConfig;\n  innerResponsiveContainerStyle?: React.ComponentProps<\n    typeof RechartsPrimitive.ResponsiveContainer\n  >[\"style\"];\n  /** Optional content rendered below the chart (e.g. EvilBrush) */\n  footer?: React.ReactNode;\n}\n\nfunction ChartContainer({\n  id,\n  config,\n  initialDimension = { width: 320, height: 200 },\n  className,\n  children,\n  footer,\n  ...props\n}: Readonly<ChartContainerProps>) {\n  const uniqueId = React.useId();\n  const chartId = `chart-${id ?? uniqueId.replace(/:/g, \"\")}`;\n\n  // Validate chart config at runtime\n  validateChartConfigColors(config);\n\n  return (\n    <ChartContext.Provider value={{ config }}>\n      <div\n        data-slot=\"chart\"\n        data-chart={chartId}\n        className={cn(\n          \"min-h-0 w-full flex-1\",\n          \"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border relative flex flex-col justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden\",\n          !footer && \"aspect-video\",\n          className,\n        )}\n        {...props}\n      >\n        <ChartStyle id={chartId} config={config} />\n        <RechartsPrimitive.ResponsiveContainer\n          className=\"min-h-0 w-full flex-1\"\n          initialDimension={initialDimension}\n        >\n          {children}\n        </RechartsPrimitive.ResponsiveContainer>\n        {footer}\n      </div>\n    </ChartContext.Provider>\n  );\n}\n\nfunction LoadingIndicator({ isLoading }: { isLoading: boolean }) {\n  if (!isLoading) {\n    return null;\n  }\n\n  return (\n    <div className=\"pointer-events-none absolute inset-0 z-20 flex items-center justify-center\">\n      <div className=\"text-primary bg-background flex items-center justify-center gap-2 rounded-md border px-2 py-0.5 text-sm\">\n        <div className=\"border-border border-t-primary h-3 w-3 animate-spin rounded-full border\" />\n        <span>Loading</span>\n      </div>\n    </div>\n  );\n}\n\n// Distribute colors evenly across slots, extra slots go to last color(s)\n// Example: 2 colors for 4 slots → [red, red, pink, pink]\n// Example: 3 colors for 4 slots → [red, pink, blue, blue]\nfunction distributeColors(colorsArray: string[], maxCount: number): string[] {\n  const availableCount = colorsArray.length;\n  if (availableCount >= maxCount) {\n    return colorsArray.slice(0, maxCount);\n  }\n\n  const result: string[] = [];\n  const baseSlots = Math.floor(maxCount / availableCount);\n  const extraSlots = maxCount % availableCount;\n\n  // First (availableCount - extraSlots) colors get baseSlots each\n  // Last extraSlots colors get (baseSlots + 1) each\n  for (let colorIdx = 0; colorIdx < availableCount; colorIdx++) {\n    const isExtraColor = colorIdx >= availableCount - extraSlots;\n    const slotsForThisColor = baseSlots + (isExtraColor ? 1 : 0);\n    for (let j = 0; j < slotsForThisColor; j++) {\n      result.push(colorsArray[colorIdx]);\n    }\n  }\n\n  return result;\n}\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n  const colorConfig = Object.entries(config).filter(([, config]) => config.colors);\n\n  if (!colorConfig.length) {\n    return null;\n  }\n\n  const generateCssVars = (theme: keyof typeof THEMES) =>\n    colorConfig\n      .flatMap(([key, itemConfig]) => {\n        const colorsArray = itemConfig.colors?.[theme];\n        if (!colorsArray || !Array.isArray(colorsArray) || colorsArray.length === 0) {\n          return [];\n        }\n\n        // Get max count across all themes for this key\n        const maxCount = getColorsCount(itemConfig);\n\n        // Distribute colors evenly across all required slots\n        const distributedColors = distributeColors(colorsArray, maxCount);\n\n        return distributedColors.map((color, index) => `  --color-${key}-${index}: ${color};`);\n      })\n      .filter(Boolean)\n      .join(\"\\n\");\n\n  const css = Object.entries(THEMES)\n    .map(\n      ([theme, prefix]) =>\n        `${prefix} [data-chart=${id}] {\\n${generateCssVars(theme as keyof typeof THEMES)}\\n}`,\n    )\n    .join(\"\\n\");\n\n  return <style dangerouslySetInnerHTML={{ __html: css }} />;\n};\n\n// Helper to extract item config from a payload.\nexport function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {\n  if (typeof payload !== \"object\" || payload === null) {\n    return undefined;\n  }\n\n  const payloadPayload =\n    \"payload\" in payload && typeof payload.payload === \"object\" && payload.payload !== null\n      ? payload.payload\n      : undefined;\n\n  let configLabelKey: string = key;\n\n  if (key in payload && typeof payload[key as keyof typeof payload] === \"string\") {\n    configLabelKey = payload[key as keyof typeof payload] as string;\n  } else if (\n    payloadPayload &&\n    key in payloadPayload &&\n    typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n  ) {\n    configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;\n  }\n\n  return configLabelKey in config ? config[configLabelKey] : config[key];\n}\n\n// Format values to percent for expanded charts\nfunction axisValueToPercentFormatter(value: number) {\n  return `${Math.round(value * 100).toFixed(0)}%`;\n}\n\n// Get max colors count across all themes for a config entry\nfunction getColorsCount(config: ChartConfig[string]): number {\n  if (!config.colors) return 1;\n  const counts = VALID_THEME_KEYS.map((theme) => config.colors?.[theme]?.length ?? 0);\n  return Math.max(...counts, 1);\n}\n\n// Generate random loading data for skeleton/loading state\n// min/max represent percentage of the range (0-100), defaults to 20-80 for realistic look\nexport const getLoadingData = (points: number = 10, min: number = 0, max: number = 70) => {\n  const range = max - min;\n  return Array.from({ length: points }, () => ({\n    loading: Math.floor(Math.random() * range) + min,\n  }));\n};\n\nexport {\n  ChartContainer,\n  ChartStyle,\n  axisValueToPercentFormatter,\n  LoadingIndicator,\n  getColorsCount,\n};\n",
      "type": "registry:component"
    },
    {
      "path": "src/components/evilcharts/ui/tooltip.tsx",
      "content": "import { getPayloadConfigFromPayload, getColorsCount, useChart } from \"@/components/evilcharts/ui/chart\";\nimport type { NameType, ValueType } from \"recharts/types/component/DefaultTooltipContent\";\nimport * as RechartsPrimitive from \"recharts\";\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\ntype TooltipRoundness = \"sm\" | \"md\" | \"lg\" | \"xl\";\ntype TooltipVariant = \"default\" | \"frosted-glass\";\n\nconst roundnessMap: Record<TooltipRoundness, string> = {\n  sm: \"rounded-sm\",\n  md: \"rounded-md\",\n  lg: \"rounded-lg\",\n  xl: \"rounded-xl\",\n};\n\nconst variantMap: Record<TooltipVariant, string> = {\n  default: \"bg-background\",\n  \"frosted-glass\": \"bg-background/70 backdrop-blur-sm\",\n};\n\nfunction ChartTooltipContent({\n  active,\n  payload,\n  className,\n  indicator = \"dot\",\n  hideLabel = false,\n  hideIndicator = false,\n  label,\n  labelFormatter,\n  labelClassName,\n  formatter,\n  nameKey,\n  labelKey,\n  selected,\n  roundness = \"lg\",\n  variant = \"default\",\n}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n  React.ComponentProps<\"div\"> & {\n    hideLabel?: boolean;\n    hideIndicator?: boolean;\n    indicator?: \"line\" | \"dot\" | \"dashed\";\n    nameKey?: string;\n    labelKey?: string;\n    selected?: string | null;\n    roundness?: TooltipRoundness;\n    variant?: TooltipVariant;\n  } & Omit<\n    RechartsPrimitive.DefaultTooltipContentProps<ValueType, NameType>,\n    \"accessibilityLayer\"\n  >) {\n  const { config } = useChart();\n\n  const tooltipLabel = React.useMemo(() => {\n    if (hideLabel || !payload?.length) {\n      return null;\n    }\n\n    const [item] = payload;\n    const key = `${labelKey ?? item?.dataKey ?? item?.name ?? \"value\"}`;\n    const itemConfig = getPayloadConfigFromPayload(config, item, key);\n    const value =\n      !labelKey && typeof label === \"string\" ? (config[label]?.label ?? label) : itemConfig?.label;\n\n    if (labelFormatter) {\n      return (\n        <div className={cn(\"font-medium\", labelClassName)}>{labelFormatter(value, payload)}</div>\n      );\n    }\n\n    if (!value) {\n      return null;\n    }\n\n    return <div className={cn(\"font-medium\", labelClassName)}>{value}</div>;\n  }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);\n\n  if (!active || !payload?.length) {\n    // Empty tooltip - to prevent position getting 0.0 so it doesnt animate tooltip every time from 0.0 origin\n    return <span className=\"p-4\" />;\n  }\n\n  const nestLabel = payload.length === 1 && indicator !== \"dot\";\n\n  return (\n    <div\n      className={cn(\n        \"border-border/50 grid min-w-32 items-start gap-1.5 border px-2.5 py-1.5 text-xs shadow-xl\",\n        roundnessMap[roundness],\n        variantMap[variant],\n        className,\n      )}\n    >\n      {!nestLabel ? tooltipLabel : null}\n      <div className=\"grid gap-1.5\">\n        {payload\n          .filter((item) => item.type !== \"none\")\n          .map((item, index) => {\n            // For pie charts, item.name contains the sector name (e.g., \"chrome\")\n            // For radial charts, the name is in item.payload[nameKey]\n            // For other charts, item.name or item.dataKey contains the series name\n            const payloadName =\n              nameKey && item.payload\n                ? (item.payload as Record<string, unknown>)[nameKey]\n                : undefined;\n            const key = `${payloadName ?? item.name ?? item.dataKey ?? \"value\"}`;\n            const itemConfig = getPayloadConfigFromPayload(config, item, key);\n\n            // Get colors count for this item to determine gradient vs solid\n            const colorsCount = itemConfig ? getColorsCount(itemConfig) : 1;\n\n            return (\n              <div\n                key={index}\n                className={cn(\n                  \"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5\",\n                  indicator === \"dot\" && \"items-center\",\n                  selected != null && selected !== item.dataKey && \"opacity-30\",\n                )}\n              >\n                {formatter && item?.value !== undefined && item.name ? (\n                  formatter(item.value, item.name, item, index, item.payload)\n                ) : (\n                  <>\n                    {itemConfig?.icon ? (\n                      <itemConfig.icon />\n                    ) : (\n                      !hideIndicator && (\n                        <div\n                          className={cn(\"shrink-0 rounded-[2px]\", {\n                            \"h-2.5 w-2.5\": indicator === \"dot\",\n                            \"w-1\": indicator === \"line\",\n                            \"w-0 border-[1.5px] border-dashed bg-transparent!\":\n                              indicator === \"dashed\",\n                            \"my-0.5\": nestLabel && indicator === \"dashed\",\n                          })}\n                          style={getIndicatorColorStyle(key, colorsCount)}\n                        />\n                      )\n                    )}\n                    <div\n                      className={cn(\n                        \"flex flex-1 justify-between gap-4 leading-none\",\n                        nestLabel ? \"items-end\" : \"items-center\",\n                      )}\n                    >\n                      <div className=\"grid gap-1.5\">\n                        {nestLabel ? tooltipLabel : null}\n                        <span className=\"text-muted-foreground\">\n                          {itemConfig?.label ?? item.name}\n                        </span>\n                      </div>\n                      {item.value != null && (\n                        <span className=\"text-foreground font-mono font-medium tabular-nums\">\n                          {typeof item.value === \"number\"\n                            ? item.value.toLocaleString()\n                            : String(item.value)}\n                        </span>\n                      )}\n                    </div>\n                  </>\n                )}\n              </div>\n            );\n          })}\n      </div>\n    </div>\n  );\n}\n\nfunction getIndicatorColorStyle(dataKey: string, colorsCount: number): React.CSSProperties {\n  if (colorsCount <= 1) {\n    return { background: `var(--color-${dataKey}-0)` };\n  }\n\n  // Multiple colors: create linear gradient with evenly distributed stops\n  const stops = Array.from({ length: colorsCount }, (_, index) => {\n    const offset = (index / (colorsCount - 1)) * 100;\n    return `var(--color-${dataKey}-${index}) ${offset}%`;\n  }).join(\", \");\n\n  return { background: `linear-gradient(to right, ${stops})` };\n}\n\nconst ChartTooltip = ({\n  animationDuration = 200,\n  ...props\n}: React.ComponentProps<typeof RechartsPrimitive.Tooltip>) => (\n  <RechartsPrimitive.Tooltip animationDuration={animationDuration} {...props} />\n);\n\nexport { ChartTooltip, ChartTooltipContent };\nexport type { TooltipRoundness, TooltipVariant };\n",
      "type": "registry:component"
    },
    {
      "path": "src/components/evilcharts/ui/legend.tsx",
      "content": "import { getPayloadConfigFromPayload, getColorsCount, useChart } from \"@/components/evilcharts/ui/chart\";\nimport * as RechartsPrimitive from \"recharts\";\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\ntype ChartLegendVariant =\n  | \"square\"\n  | \"circle\"\n  | \"circle-outline\"\n  | \"rounded-square\"\n  | \"rounded-square-outline\"\n  | \"vertical-bar\"\n  | \"horizontal-bar\";\n\nfunction ChartLegendContent({\n  className,\n  hideIcon = false,\n  nameKey,\n  payload,\n  verticalAlign,\n  align = \"right\",\n  selected,\n  onSelectChange,\n  isClickable,\n  variant = \"rounded-square\",\n}: React.ComponentProps<\"div\"> & {\n  hideIcon?: boolean;\n  nameKey?: string;\n  selected?: string | null;\n  isClickable?: boolean;\n  onSelectChange?: (selected: string | null) => void;\n  variant?: ChartLegendVariant;\n} & RechartsPrimitive.DefaultLegendContentProps) {\n  const { config } = useChart();\n\n  if (!payload?.length) {\n    return null;\n  }\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-4 select-none\",\n        align === \"left\" && \"justify-start\",\n        align === \"center\" && \"justify-center\",\n        align === \"right\" && \"justify-end\",\n        verticalAlign === \"top\" ? \"pb-4\" : \"pt-4\",\n        className,\n      )}\n    >\n      {payload\n        .filter((item) => item.type !== \"none\")\n        .map((item) => {\n          // For pie charts, item.value contains the sector name (e.g., \"chrome\")\n          // For radial charts, the name is in item.payload[nameKey]\n          // For other charts, item.dataKey contains the series name (e.g., \"desktop\")\n          const payloadName =\n            nameKey && item.payload\n              ? (item.payload as Record<string, unknown>)[nameKey]\n              : undefined;\n          const key = `${payloadName ?? item.value ?? item.dataKey ?? \"value\"}`;\n          const itemConfig = getPayloadConfigFromPayload(config, item, key);\n          const isSelected = selected === null || selected === key;\n\n          // Get colors count for this item to determine gradient vs solid\n          const colorsCount = itemConfig ? getColorsCount(itemConfig) : 1;\n\n          return (\n            <div\n              key={key}\n              className={cn(\n                \"[&>svg]:text-muted-foreground flex items-center gap-1.5 transition-opacity [&>svg]:h-3 [&>svg]:w-3\",\n                !isSelected && \"opacity-30\",\n                isClickable && \"cursor-pointer\",\n              )}\n              onClick={() => {\n                if (!isClickable) return;\n\n                onSelectChange?.(selected === key ? null : key);\n              }}\n            >\n              {itemConfig?.icon && !hideIcon ? (\n                <itemConfig.icon />\n              ) : (\n                <LegendIndicator\n                  variant={variant}\n                  dataKey={key}\n                  colorsCount={colorsCount}\n                />\n              )}\n              {itemConfig?.label}\n            </div>\n          );\n        })}\n    </div>\n  );\n}\n\n// ---------------------------------------------------------------------------\n// Legend indicator — each variant gets its own branch so future variants\n// can diverge freely in markup & style.\n// ---------------------------------------------------------------------------\n\nfunction LegendIndicator({\n  variant,\n  dataKey,\n  colorsCount,\n}: {\n  variant: ChartLegendVariant;\n  dataKey: string;\n  colorsCount: number;\n}) {\n  const fillStyle = getLegendFillStyle(dataKey, colorsCount);\n  const outlineStyle = getLegendOutlineStyle(dataKey, colorsCount);\n\n  switch (variant) {\n    case \"square\":\n      return <div className=\"h-2 w-2 shrink-0\" style={fillStyle} />;\n\n    case \"circle\":\n      return <div className=\"h-2 w-2 shrink-0 rounded-full\" style={fillStyle} />;\n\n    case \"circle-outline\":\n      return (\n        <div\n          className=\"h-2.5 w-2.5 shrink-0 rounded-full p-[1.5px]\"\n          style={outlineStyle}\n        />\n      );\n\n    case \"vertical-bar\":\n      return <div className=\"h-3 w-1 shrink-0 rounded-[2px]\" style={fillStyle} />;\n\n    case \"horizontal-bar\":\n      return <div className=\"h-1 w-3 shrink-0 rounded-[2px]\" style={fillStyle} />;\n\n    case \"rounded-square-outline\":\n      return (\n        <div\n          className=\"h-2.5 w-2.5 shrink-0 rounded-[3px] p-[1.5px]\"\n          style={outlineStyle}\n        />\n      );\n\n    case \"rounded-square\":\n    default:\n      return <div className=\"h-2 w-2 shrink-0 rounded-[2px]\" style={fillStyle} />;\n  }\n}\n\n// ---------------------------------------------------------------------------\n// Style helpers\n// ---------------------------------------------------------------------------\n\n/** Solid fill / gradient background for filled variants. */\nfunction getLegendFillStyle(dataKey: string, colorsCount: number): React.CSSProperties {\n  if (colorsCount <= 1) {\n    return { backgroundColor: `var(--color-${dataKey}-0)` };\n  }\n\n  const stops = Array.from({ length: colorsCount }, (_, i) => {\n    const offset = (i / (colorsCount - 1)) * 100;\n    return `var(--color-${dataKey}-${i}) ${offset}%`;\n  }).join(\", \");\n\n  return { background: `linear-gradient(to right, ${stops})` };\n}\n\n/**\n * Outline style for stroke variants.\n * Uses background + mask-composite to punch out the center, leaving only the\n * \"border\" visible. Works with both solid colors and gradients, and respects\n * border-radius — unlike plain `border-color`.\n */\nfunction getLegendOutlineStyle(dataKey: string, colorsCount: number): React.CSSProperties {\n  const maskStyle: React.CSSProperties = {\n    WebkitMask:\n      \"linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)\",\n    WebkitMaskComposite: \"xor\",\n    mask: \"linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)\",\n    maskComposite: \"exclude\",\n  };\n\n  if (colorsCount <= 1) {\n    return {\n      backgroundColor: `var(--color-${dataKey}-0)`,\n      ...maskStyle,\n    };\n  }\n\n  const stops = Array.from({ length: colorsCount }, (_, i) => {\n    const offset = (i / (colorsCount - 1)) * 100;\n    return `var(--color-${dataKey}-${i}) ${offset}%`;\n  }).join(\", \");\n\n  return {\n    background: `linear-gradient(to right, ${stops})`,\n    ...maskStyle,\n  };\n}\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\nexport { ChartLegend, ChartLegendContent, type ChartLegendVariant };\n",
      "type": "registry:component"
    },
    {
      "path": "src/components/evilcharts/ui/background.tsx",
      "content": "\"use client\";\n\nimport { ZIndexLayer } from \"recharts\";\nimport { useId } from \"react\";\n\n// ── Background Variant Types ─────────────────────────────────────────────────\n// To add a new variant:\n// 1. Add its name to the BackgroundVariant union type below\n// 2. Create a pattern component with PatternProps\n// 3. Register it in PATTERN_MAP\n\nexport type BackgroundVariant =\n  | \"dots\"\n  | \"grid\"\n  | \"cross-hatch\"\n  | \"diagonal-lines\"\n  | \"plus\"\n  | \"falling-triangles\"\n  | \"4-pointed-star\"\n  | \"tiny-checkers\"\n  | \"overlapping-circles\"\n  | \"wiggle-lines\"\n  | \"bubbles\";\n\n// ── Pattern Components ───────────────────────────────────────────────────────\n\ntype PatternProps = { id: string };\n\nconst DotsPattern = ({ id }: PatternProps) => (\n  <pattern id={id} x=\"0\" y=\"0\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\">\n    <circle className=\"text-border dark:text-border\" cx=\"2\" cy=\"2\" r=\"1\" fill=\"currentColor\" />\n  </pattern>\n);\n\nconst GridPattern = ({ id }: PatternProps) => (\n  <pattern id={id} x=\"0\" y=\"0\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\">\n    <path\n      className=\"text-border dark:text-border\"\n      d=\"M 20 0 L 0 0 0 20\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"0.5\"\n    />\n  </pattern>\n);\n\nconst CrossHatchPattern = ({ id }: PatternProps) => (\n  <pattern id={id} x=\"0\" y=\"0\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\">\n    <path\n      className=\"text-border/60 dark:text-border/50\"\n      d=\"M 0 0 L 20 20 M 20 0 L 0 20\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"0.5\"\n    />\n  </pattern>\n);\n\nconst DiagonalLinesPattern = ({ id }: PatternProps) => (\n  <pattern\n    id={id}\n    x=\"0\"\n    y=\"0\"\n    width=\"6\"\n    height=\"6\"\n    patternUnits=\"userSpaceOnUse\"\n    patternTransform=\"rotate(45)\"\n  >\n    <line\n      className=\"text-border dark:text-border\"\n      x1=\"0\"\n      y1=\"0\"\n      x2=\"0\"\n      y2=\"6\"\n      stroke=\"currentColor\"\n      strokeWidth=\"0.5\"\n    />\n  </pattern>\n);\n\nconst PlusPattern = ({ id }: PatternProps) => (\n  <pattern id={id} x=\"0\" y=\"0\" width=\"16\" height=\"16\" patternUnits=\"userSpaceOnUse\">\n    <path\n      className=\"text-border dark:text-border\"\n      d=\"M 8 4 L 8 12 M 4 8 L 12 8\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"0.5\"\n      strokeLinecap=\"round\"\n    />\n  </pattern>\n);\n\nconst FallingTrianglesPattern = ({ id }: PatternProps) => (\n  <pattern id={id} x=\"0\" y=\"0\" width=\"18\" height=\"36\" patternUnits=\"userSpaceOnUse\">\n    <path\n      className=\"text-border dark:text-border\"\n      d=\"M2 6h12L8 18 2 6zm18 36h12l-6 12-6-12z\"\n      transform=\"scale(0.5)\"\n      fill=\"currentColor\"\n      fillOpacity=\"0.4\"\n    />\n  </pattern>\n);\n\nconst FourPointedStarPattern = ({ id }: PatternProps) => (\n  <pattern id={id} x=\"0\" y=\"0\" width=\"16\" height=\"16\" patternUnits=\"userSpaceOnUse\">\n    <polygon\n      className=\"text-border dark:text-border\"\n      fillRule=\"evenodd\"\n      points=\"5 3 8 4 5 5 4 8 3 5 0 4 3 3 4 0 5 3\"\n      fill=\"currentColor\"\n      fillOpacity=\"0.4\"\n    />\n  </pattern>\n);\n\nconst TinyCheckersPattern = ({ id }: PatternProps) => (\n  <pattern id={id} x=\"0\" y=\"0\" width=\"8\" height=\"8\" patternUnits=\"userSpaceOnUse\">\n    <path\n      className=\"text-border dark:text-border\"\n      fillRule=\"evenodd\"\n      d=\"M0 0h4v4H0V0zm4 4h4v4H4V4z\"\n      fill=\"currentColor\"\n      fillOpacity=\"0.2\"\n    />\n  </pattern>\n);\n\nconst OverlappingCirclesPattern = ({ id }: PatternProps) => (\n  <pattern id={id} x=\"0\" y=\"0\" width=\"40\" height=\"40\" patternUnits=\"userSpaceOnUse\">\n    <path\n      className=\"text-border dark:text-border\"\n      fillRule=\"evenodd\"\n      d=\"M25 25c0-2.762 2.238-5 5-5s5 2.238 5 5-2.238 5-5 5c0 2.762-2.238 5-5 5s-5-2.238-5-5 2.238-5 5-5zM5 5c0-2.762 2.238-5 5-5s5 2.238 5 5-2.238 5-5 5c0 2.762-2.238 5-5 5S0 12.762 0 10s2.238-5 5-5zm5 4c2.209 0 4-1.791 4-4s-1.791-4-4-4-4 1.791-4 4 1.791 4 4 4zm20 20c2.209 0 4-1.791 4-4s-1.791-4-4-4-4 1.791-4 4 1.791 4 4 4z\"\n      fill=\"currentColor\"\n      fillOpacity=\"0.4\"\n    />\n  </pattern>\n);\n\nconst WiggleLinesPattern = ({ id }: PatternProps) => (\n  <pattern\n    id={id}\n    x=\"0\"\n    y=\"0\"\n    width=\"52\"\n    height=\"26\"\n    patternUnits=\"userSpaceOnUse\"\n    patternTransform=\"scale(0.6)\"\n  >\n    <path\n      className=\"text-border dark:text-border\"\n      d=\"M10 10c0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6h2c0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4v2c-3.314 0-6-2.686-6-6 0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6zm25.464-1.95l8.486 8.486-1.414 1.414-8.486-8.486 1.414-1.414z\"\n      fill=\"currentColor\"\n      fillOpacity=\"0.4\"\n    />\n  </pattern>\n);\n\nconst BubblesPattern = ({ id }: PatternProps) => (\n  <pattern\n    id={id}\n    x=\"0\"\n    y=\"0\"\n    width=\"100\"\n    height=\"100\"\n    patternUnits=\"userSpaceOnUse\"\n    patternTransform=\"scale(0.6667)\"\n  >\n    <path\n      className=\"text-border dark:text-border\"\n      d=\"M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z\"\n      fill=\"currentColor\"\n      fillOpacity=\"0.4\"\n      fillRule=\"evenodd\"\n    />\n  </pattern>\n);\n\n// ── Pattern Registry ─────────────────────────────────────────────────────────\n// Map variant names to pattern components\n\nconst PATTERN_MAP: Record<BackgroundVariant, React.FC<PatternProps>> = {\n  dots: DotsPattern,\n  grid: GridPattern,\n  plus: PlusPattern,\n  bubbles: BubblesPattern,\n  \"cross-hatch\": CrossHatchPattern,\n  \"diagonal-lines\": DiagonalLinesPattern,\n  \"falling-triangles\": FallingTrianglesPattern,\n  \"4-pointed-star\": FourPointedStarPattern,\n  \"tiny-checkers\": TinyCheckersPattern,\n  \"overlapping-circles\": OverlappingCirclesPattern,\n  \"wiggle-lines\": WiggleLinesPattern,\n};\n\n// ── Main Component ───────────────────────────────────────────────────────────\n// Usage: Place <ChartBackground variant=\"dots\" /> inside any Recharts chart component.\n// ZIndexLayer with zIndex={-1} ensures the background renders behind all chart content.\n\ninterface ChartBackgroundProps {\n  variant: BackgroundVariant;\n}\n\nexport function ChartBackground({ variant }: ChartBackgroundProps) {\n  const baseId = useId().replace(/:/g, \"\");\n  const patternId = `${baseId}-bg-${variant}`;\n  const maskId = `${baseId}-bg-edge-fade`;\n  const filterId = `${baseId}-bg-blur`;\n  const PatternComponent = PATTERN_MAP[variant];\n\n  return (\n    <ZIndexLayer zIndex={-1}>\n      <defs>\n        <PatternComponent id={patternId} />\n        {/* Gaussian blur filter for soft edge fade */}\n        <filter id={filterId}>\n          <feGaussianBlur stdDeviation=\"25\" />\n        </filter>\n        {/* Mask: a slightly inset white rect with blur creates smooth transparent edges */}\n        <mask id={maskId} maskUnits=\"userSpaceOnUse\">\n          <rect x=\"8%\" y=\"20%\" width=\"85%\" height=\"60%\" fill=\"white\" filter={`url(#${filterId})`} />\n        </mask>\n      </defs>\n      <rect width=\"100%\" height=\"100%\" fill={`url(#${patternId})`} mask={`url(#${maskId})`} />\n    </ZIndexLayer>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "src/components/evilcharts/ui/dot.tsx",
      "content": "import { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\nexport type DotVariant = \"default\" | \"border\" | \"colored-border\";\n\ntype ChartDotProps = {\n  cx?: number;\n  cy?: number;\n  dataKey: string;\n  chartId: string;\n  className?: string;\n  fillOpacity?: number;\n  type?: DotVariant;\n};\n\nconst ChartDot = React.memo(function ChartDot({\n  cx,\n  cy,\n  dataKey,\n  chartId,\n  className,\n  fillOpacity = 1,\n  type = \"default\",\n}: ChartDotProps) {\n  const dotId = React.useId().replace(/:/g, \"\");\n  const gradientUrl = `url(#${chartId}-colors-${String(dataKey)})`;\n\n  if (cx === undefined || cy === undefined) return null;\n\n  switch (type) {\n    case \"border\":\n      return (\n        <PrimaryBorderDot\n          cx={cx}\n          cy={cy}\n          dotId={dotId}\n          fillOpacity={fillOpacity}\n          gradientUrl={gradientUrl}\n          className={className}\n        />\n      );\n    case \"colored-border\":\n      return (\n        <ColoredBorderDot\n          cx={cx}\n          cy={cy}\n          dotId={dotId}\n          fillOpacity={fillOpacity}\n          gradientUrl={gradientUrl}\n          className={className}\n        />\n      );\n    default:\n      return (\n        <DefaultDot\n          cx={cx}\n          cy={cy}\n          dotId={dotId}\n          fillOpacity={fillOpacity}\n          gradientUrl={gradientUrl}\n          className={className}\n        />\n      );\n  }\n});\n\ntype DotVariantProps = {\n  cx: number;\n  cy: number;\n  dotId: string;\n  fillOpacity: number;\n  gradientUrl: string;\n  className?: string;\n};\n\nconst DefaultDot = React.memo(\n  ({ cx, cy, dotId, fillOpacity, gradientUrl, className }: DotVariantProps) => {\n    const r = 3;\n    return (\n      <g className={className}>\n        <defs>\n          <clipPath id={`dot-clip-${dotId}`}>\n            <circle cx={cx} cy={cy} r={r} />\n          </clipPath>\n        </defs>\n        {/* Full-width gradient rectangle clipped to dot shape */}\n        <rect\n          x=\"0\"\n          y={cy - r}\n          width=\"100%\"\n          height={r * 2}\n          fill={gradientUrl}\n          fillOpacity={fillOpacity}\n          clipPath={`url(#dot-clip-${dotId})`}\n        />\n      </g>\n    );\n  },\n);\n\nDefaultDot.displayName = \"DefaultDot\";\n\nconst PrimaryBorderDot = React.memo(\n  ({ cx, cy, dotId, fillOpacity, gradientUrl, className }: DotVariantProps) => {\n    const r = 6;\n    const strokeWidth = 5;\n    return (\n      <g className={cn(className, \"text-background\")}>\n        <defs>\n          <clipPath id={`dot-clip-${dotId}`}>\n            <circle cx={cx} cy={cy} r={r} />\n          </clipPath>\n        </defs>\n        {/* Background stroke (border) */}\n        <circle cx={cx} cy={cy} r={r} fill=\"currentColor\" />\n        {/* Inner gradient circle clipped */}\n        <rect\n          x=\"0\"\n          y={cy - (r - strokeWidth / 2)}\n          width=\"100%\"\n          height={(r - strokeWidth / 2) * 2}\n          fill={gradientUrl}\n          fillOpacity={fillOpacity}\n          clipPath={`url(#dot-clip-inner-${dotId})`}\n        />\n        <defs>\n          <clipPath id={`dot-clip-inner-${dotId}`}>\n            <circle cx={cx} cy={cy} r={r - strokeWidth / 2} />\n          </clipPath>\n        </defs>\n      </g>\n    );\n  },\n);\n\nPrimaryBorderDot.displayName = \"PrimaryBorderDot\";\n\nconst ColoredBorderDot = React.memo(\n  ({ cx, cy, dotId, fillOpacity, gradientUrl, className }: DotVariantProps) => {\n    const r = 3;\n    const strokeWidth = 1;\n    return (\n      <g className={cn(className, \"text-background\")}>\n        <defs>\n          <clipPath id={`dot-clip-${dotId}`}>\n            <circle cx={cx} cy={cy} r={r + strokeWidth / 2} />\n          </clipPath>\n        </defs>\n        {/* Gradient stroke (border) via clipped rect */}\n        <rect\n          x=\"0\"\n          y={cy - r - strokeWidth / 2}\n          width=\"100%\"\n          height={(r + strokeWidth / 2) * 2}\n          fill={gradientUrl}\n          fillOpacity={fillOpacity}\n          clipPath={`url(#dot-clip-${dotId})`}\n        />\n        {/* Inner solid fill */}\n        <circle cx={cx} cy={cy} r={r - strokeWidth / 2} fill=\"currentColor\" />\n      </g>\n    );\n  },\n);\n\nColoredBorderDot.displayName = \"ColoredBorderDot\";\n\nexport { ChartDot };\n",
      "type": "registry:component"
    },
    {
      "path": "src/components/evilcharts/ui/evil-brush.tsx",
      "content": "\"use client\";\n\nimport { motion, useMotionValue, useMotionValueEvent, useSpring, useTransform } from \"motion/react\";\nimport { ResponsiveContainer, AreaChart, Area, LineChart, Line, BarChart, Bar } from \"recharts\";\nimport { ChartStyle, getColorsCount, type ChartConfig } from \"@/components/evilcharts/ui/chart\";\nimport type { MotionValue } from \"motion/react\";\nimport { useCallback, useEffect, type ComponentProps } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\ntype EvilBrushVariant = \"line\" | \"area\" | \"bar\";\ntype CurveType = ComponentProps<typeof Area>[\"type\"];\n\ninterface EvilBrushRange {\n  startIndex: number;\n  endIndex: number;\n}\n\ninterface EvilBrushProps {\n  /** Full dataset – always rendered in the miniature chart */\n  data: Record<string, unknown>[];\n  /** Chart config with colour definitions */\n  chartConfig: ChartConfig;\n  /** Data keys to plot (default: all keys from chartConfig) */\n  dataKeys?: string[];\n  /** X-axis data key – used for handle labels */\n  xDataKey?: string;\n  /** Visual variant of the mini chart */\n  variant?: EvilBrushVariant;\n  /** Pixel height of the brush */\n  height?: number;\n  /** Extra className */\n  className?: string;\n  /** Whether areas/bars should be stacked in the mini chart */\n  stacked?: boolean;\n  /** Stroke variant for line / area strokes in the mini chart */\n  strokeVariant?: \"solid\" | \"dashed\" | \"animated-dashed\";\n  /** Whether to connect null data points in line / area variants */\n  connectNulls?: boolean;\n  /** Radius for bar corners in the bar variant */\n  barRadius?: number;\n\n  // ── Controlled mode ──────────────────────────────────────────────────\n  /** Controlled start index */\n  startIndex?: number;\n  /** Controlled end index */\n  endIndex?: number;\n\n  // ── Uncontrolled mode ────────────────────────────────────────────────\n  /** Initial start index (uncontrolled) */\n  defaultStartIndex?: number;\n  /** Initial end index (uncontrolled) */\n  defaultEndIndex?: number;\n\n  /** Fired whenever the visible range changes */\n  onChange?: (range: EvilBrushRange) => void;\n  /** Format the handle label from the xDataKey value */\n  formatLabel?: (value: unknown, index: number) => string;\n  /** Curve type for line / area variants */\n  curveType?: CurveType;\n  /** Minimum number of data points that must remain selected */\n  minSpan?: number;\n  /** Whether to render labels on the handles */\n  showLabels?: boolean;\n  /** Skip rendering own ChartStyle (when inside a ChartContainer that already provides CSS vars) */\n  skipStyle?: boolean;\n}\n\n// ─── Spring config ──────────────────────────────────────────────────────────\n\nconst SPRING_CONFIG = { stiffness: 300, damping: 35, mass: 0.8 };\n\n// ─── Pointer-capture drag hook ──────────────────────────────────────────────\n// Replaces raw addEventListener with the modern Pointer Events API.\n// setPointerCapture routes all pointer events to the originating element,\n// so we get mouse + touch + pen support with zero global listeners.\n\ntype DragType = \"left\" | \"right\" | \"middle\";\n\ninterface DragState {\n  type: DragType;\n  originX: number;\n  originRange: EvilBrushRange;\n}\n\nfunction useBrushDrag({\n  range,\n  totalPoints,\n  containerRef,\n  commit,\n}: {\n  range: EvilBrushRange;\n  totalPoints: number;\n  containerRef: React.RefObject<HTMLDivElement | null>;\n  commit: (next: EvilBrushRange, mode?: DragType) => void;\n}) {\n  const dragRef = React.useRef<DragState | null>(null);\n  const [isDragging, setIsDragging] = React.useState(false);\n\n  const toIndexDelta = useCallback(\n    (px: number) => {\n      if (!containerRef.current || totalPoints <= 1) return 0;\n      return Math.round(\n        (px / containerRef.current.getBoundingClientRect().width) * (totalPoints - 1),\n      );\n    },\n    [totalPoints, containerRef],\n  );\n\n  const onPointerDown = useCallback(\n    (e: React.PointerEvent, type: DragType) => {\n      e.preventDefault();\n      (e.target as HTMLElement).setPointerCapture(e.pointerId);\n      dragRef.current = { type, originX: e.clientX, originRange: { ...range } };\n      setIsDragging(true);\n    },\n    [range],\n  );\n\n  const onPointerMove = useCallback(\n    (e: React.PointerEvent) => {\n      const d = dragRef.current;\n      if (!d) return;\n\n      const delta = toIndexDelta(e.clientX - d.originX);\n      const { type, originRange: o } = d;\n\n      if (type === \"left\") {\n        commit({ startIndex: o.startIndex + delta, endIndex: o.endIndex }, \"left\");\n      } else if (type === \"right\") {\n        commit({ startIndex: o.startIndex, endIndex: o.endIndex + delta }, \"right\");\n      } else {\n        const span = o.endIndex - o.startIndex;\n        let s = o.startIndex + delta;\n        let e2 = s + span;\n        if (s < 0) {\n          s = 0;\n          e2 = span;\n        }\n        if (e2 > totalPoints - 1) {\n          e2 = totalPoints - 1;\n          s = Math.max(0, e2 - span);\n        }\n        commit({ startIndex: s, endIndex: e2 }, \"middle\");\n      }\n    },\n    [toIndexDelta, totalPoints, commit],\n  );\n\n  const onPointerUp = useCallback((e: React.PointerEvent) => {\n    (e.target as HTMLElement).releasePointerCapture(e.pointerId);\n    dragRef.current = null;\n    setIsDragging(false);\n  }, []);\n\n  // Helper to bind all three pointer handlers for a given drag type\n  const bind = useCallback(\n    (type: DragType) => ({\n      onPointerDown: (e: React.PointerEvent) => onPointerDown(e, type),\n      onPointerMove,\n      onPointerUp,\n    }),\n    [onPointerDown, onPointerMove, onPointerUp],\n  );\n\n  return { isDragging, bind };\n}\n\n// ─── EvilBrush ────────────────────────────────────────────────────────────\n\nfunction EvilBrush({\n  data,\n  chartConfig,\n  dataKeys,\n  xDataKey,\n  variant = \"area\",\n  height = 56,\n  className,\n  stacked = false,\n  strokeVariant = \"solid\",\n  connectNulls = false,\n  barRadius,\n  startIndex: controlledStart,\n  endIndex: controlledEnd,\n  defaultStartIndex = 0,\n  defaultEndIndex,\n  onChange,\n  formatLabel,\n  curveType = \"monotone\",\n  minSpan = 2,\n  showLabels = true,\n  skipStyle = false,\n}: EvilBrushProps) {\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const keys = React.useMemo(() => dataKeys ?? Object.keys(chartConfig), [dataKeys, chartConfig]);\n  const totalPoints = data.length;\n  const chartId = React.useId().replace(/:/g, \"\");\n\n  // ── Controlled vs uncontrolled ──────────────────────────────────────────\n\n  const isControlled = controlledStart !== undefined && controlledEnd !== undefined;\n\n  const [internalRange, setInternalRange] = React.useState<EvilBrushRange>(() => ({\n    startIndex: Math.max(0, Math.min(defaultStartIndex, totalPoints - 1)),\n    endIndex: Math.max(0, Math.min(defaultEndIndex ?? totalPoints - 1, totalPoints - 1)),\n  }));\n\n  // Track the last committed range to avoid duplicate updates when small\n  // mouse movements don't produce index changes (e.g., at boundaries)\n  const lastCommittedRef = React.useRef<EvilBrushRange>(internalRange);\n\n  useEffect(() => {\n    if (!isControlled) {\n      setInternalRange((prev) => {\n        const adjusted = {\n          startIndex: Math.min(prev.startIndex, Math.max(0, totalPoints - 1)),\n          endIndex: Math.min(prev.endIndex, Math.max(0, totalPoints - 1)),\n        };\n        lastCommittedRef.current = adjusted;\n        return adjusted;\n      });\n    }\n  }, [totalPoints, isControlled]);\n\n  // ── Clamping & committing ───────────────────────────────────────────────\n\n  const clampRange = useCallback(\n    (range: EvilBrushRange, mode?: DragType): EvilBrushRange => {\n      let { startIndex, endIndex } = range;\n      const maxIndex = Math.max(0, totalPoints - 1);\n\n      startIndex = Math.max(0, Math.min(startIndex, maxIndex));\n      endIndex = Math.max(0, Math.min(endIndex, maxIndex));\n\n      if (mode === \"left\") {\n        const maxStart = Math.max(0, endIndex - minSpan);\n        startIndex = Math.min(startIndex, maxStart);\n        return { startIndex, endIndex };\n      }\n\n      if (mode === \"right\") {\n        const minEnd = Math.min(maxIndex, startIndex + minSpan);\n        endIndex = Math.max(endIndex, minEnd);\n        return { startIndex, endIndex };\n      }\n\n      if (endIndex - startIndex < minSpan) {\n        endIndex = Math.min(startIndex + minSpan, maxIndex);\n        if (endIndex - startIndex < minSpan) {\n          startIndex = Math.max(0, endIndex - minSpan);\n        }\n      }\n      return { startIndex, endIndex };\n    },\n    [totalPoints, minSpan],\n  );\n\n  const commit = useCallback(\n    (next: EvilBrushRange, mode?: DragType) => {\n      const clamped = clampRange(next, mode);\n      const last = lastCommittedRef.current;\n\n      // Only update if the range has actually changed — avoids unnecessary\n      // re-renders when the brush is at a boundary and small mouse movements\n      // don't produce index changes\n      if (last.startIndex === clamped.startIndex && last.endIndex === clamped.endIndex) {\n        return;\n      }\n\n      lastCommittedRef.current = clamped;\n      setInternalRange(clamped);\n      // Defer the parent callback — chart re-render happens at lower priority,\n      // React can skip intermediate frames during fast drags\n      React.startTransition(() => {\n        onChange?.(clamped);\n      });\n    },\n    [clampRange, onChange],\n  );\n\n  // ── Drag ────────────────────────────────────────────────────────────────\n\n  const { isDragging, bind } = useBrushDrag({\n    range: internalRange,\n    totalPoints,\n    containerRef,\n    commit,\n  });\n\n  // Position always driven by internalRange (never lags behind controlled props)\n  const range = internalRange;\n\n  // Sync internalRange with controlled props when not dragging\n  useEffect(() => {\n    if (isControlled && !isDragging) {\n      const syncedRange = { startIndex: controlledStart, endIndex: controlledEnd };\n      // eslint-disable-next-line react-hooks/set-state-in-effect\n      setInternalRange(syncedRange);\n      lastCommittedRef.current = syncedRange;\n    }\n  }, [isControlled, controlledStart, controlledEnd, isDragging]);\n\n  // ── Computed positions (%) ──────────────────────────────────────────────\n\n  const leftPct = totalPoints > 1 ? (range.startIndex / (totalPoints - 1)) * 100 : 0;\n  const rightPct = totalPoints > 1 ? (range.endIndex / (totalPoints - 1)) * 100 : 100;\n\n  // Drive all moving brush UI from the same springed edge values.\n  const leftTarget = useMotionValue(leftPct);\n  const rightTarget = useMotionValue(rightPct);\n  if (leftTarget.get() !== leftPct) leftTarget.set(leftPct);\n  if (rightTarget.get() !== rightPct) rightTarget.set(rightPct);\n\n  const leftSpring = useSpring(leftTarget, SPRING_CONFIG);\n  const rightSpring = useSpring(rightTarget, SPRING_CONFIG);\n  const leftPosition = useTransform(leftSpring, (v) => `${v}%`);\n  const rightPosition = useTransform(rightSpring, (v) => `${v}%`);\n  const leftOverlayWidth = useTransform(leftSpring, (v) => `${v}%`);\n  const rightOverlayWidth = useTransform(rightSpring, (v) => `${Math.max(0, 100 - v)}%`);\n  const selectedWidth = useMotionValue(`${Math.max(0, rightPct - leftPct)}%`);\n\n  const updateSelectedWidth = useCallback(() => {\n    selectedWidth.set(`${Math.max(0, rightSpring.get() - leftSpring.get())}%`);\n  }, [leftSpring, rightSpring, selectedWidth]);\n\n  useMotionValueEvent(leftSpring, \"change\", updateSelectedWidth);\n  useMotionValueEvent(rightSpring, \"change\", updateSelectedWidth);\n\n  const getLabel = useCallback(\n    (idx: number) => {\n      if (!xDataKey) return String(idx);\n      const v = data[idx]?.[xDataKey];\n      return formatLabel ? formatLabel(v, idx) : String(v ?? idx);\n    },\n    [data, xDataKey, formatLabel],\n  );\n\n  // ── Render ──────────────────────────────────────────────────────────────\n\n  if (totalPoints === 0) return null;\n\n  return (\n    <div\n      ref={containerRef}\n      data-chart={skipStyle ? undefined : chartId}\n      className={cn(\"group relative select-none\", className)}\n      style={{ height }}\n    >\n      {!skipStyle && <ChartStyle id={chartId} config={chartConfig} />}\n\n      {/* Mini chart – always shows all data */}\n      <div className=\"absolute inset-0 overflow-hidden rounded-md\">\n        <MiniChart\n          data={data}\n          keys={keys}\n          chartConfig={chartConfig}\n          variant={variant}\n          curveType={curveType}\n          chartId={chartId}\n          stacked={stacked}\n          strokeVariant={strokeVariant === \"animated-dashed\" ? \"dashed\" : strokeVariant}\n          connectNulls={connectNulls}\n          barRadius={barRadius}\n        />\n      </div>\n\n      {/* Dim overlay – left */}\n      <motion.div\n        className=\"bg-background/70 pointer-events-none absolute inset-y-0 left-0 rounded-l-md\"\n        style={{ width: leftOverlayWidth }}\n      />\n      {/* Dim overlay – right */}\n      <motion.div\n        className=\"bg-background/70 pointer-events-none absolute inset-y-0 right-0 rounded-r-md\"\n        style={{ width: rightOverlayWidth }}\n      />\n\n      {/* Selected region – draggable to pan */}\n      <motion.div\n        className=\"absolute inset-y-0 cursor-grab touch-none rounded-sm border active:cursor-grabbing\"\n        style={{ left: leftPosition, width: selectedWidth }}\n        {...bind(\"middle\")}\n      />\n\n      {/* Left handle */}\n      <BrushHandle\n        side=\"left\"\n        position={leftPosition}\n        label={showLabels ? getLabel(range.startIndex) : undefined}\n        bind={bind(\"left\")}\n      />\n\n      {/* Right handle */}\n      <BrushHandle\n        side=\"right\"\n        position={rightPosition}\n        label={showLabels ? getLabel(range.endIndex) : undefined}\n        bind={bind(\"right\")}\n      />\n    </div>\n  );\n}\n\n// ─── Brush Handle ───────────────────────────────────────────────────────────\n\nfunction BrushHandle({\n  side,\n  position,\n  label,\n  bind,\n}: {\n  side: \"left\" | \"right\";\n  position: MotionValue<string>;\n  label?: string;\n  bind: {\n    onPointerDown: (e: React.PointerEvent) => void;\n    onPointerMove: (e: React.PointerEvent) => void;\n    onPointerUp: (e: React.PointerEvent) => void;\n  };\n}) {\n  const isLeft = side === \"left\";\n\n  return (\n    <motion.div className=\"absolute inset-y-0 z-10\" style={{ left: position }}>\n      <div\n        className={cn(\n          \"group absolute inset-y-0 flex w-3 cursor-ew-resize touch-none items-center justify-center after:absolute after:inset-y-0 after:-left-4 after:w-11 after:content-['']\",\n          isLeft ? \"\" : \"-translate-x-full\",\n        )}\n        {...bind}\n      >\n        <div\n          className={cn(\n            \"bg-muted-foreground group-hover:bg-foreground relative flex h-4 w-1.5 items-center justify-center rounded-md transition-colors\",\n            isLeft ? \"-left-[5.5px]\" : \"-right-[5.5px]\",\n          )}\n        >\n          <div className=\"flex flex-col gap-[2px]\">\n            <div className=\"bg-background/70 h-[2px] w-[2px] rounded-full\" />\n            <div className=\"bg-background/70 h-[2px] w-[2px] rounded-full\" />\n            <div className=\"bg-background/70 h-[2px] w-[2px] rounded-full\" />\n          </div>\n        </div>\n      </div>\n\n      {label && (\n        <div\n          className={cn(\n            \"bg-foreground text-background pointer-events-none absolute -bottom-3 -translate-y-1/2 rounded-[3px] px-1 py-px text-[8px] leading-tight font-medium whitespace-nowrap opacity-0 group-hover:opacity-100\",\n            isLeft ? \"left-1.5\" : \"right-1.5\",\n          )}\n        >\n          {label}\n        </div>\n      )}\n    </motion.div>\n  );\n}\n\n// ─── Mini Chart ─────────────────────────────────────────────────────────────\n\nfunction MiniChart({\n  data,\n  keys,\n  chartConfig,\n  variant,\n  curveType,\n  chartId,\n  stacked,\n  strokeVariant = \"solid\",\n  connectNulls = false,\n  barRadius,\n}: {\n  data: Record<string, unknown>[];\n  keys: string[];\n  chartConfig: ChartConfig;\n  variant: EvilBrushVariant;\n  curveType: CurveType;\n  chartId: string;\n  stacked: boolean;\n  strokeVariant?: \"solid\" | \"dashed\" | \"animated-dashed\";\n  connectNulls?: boolean;\n  barRadius?: number;\n}) {\n  const gradients = React.useMemo(\n    () =>\n      Object.entries(chartConfig)\n        .filter(([key]) => keys.includes(key))\n        .map(([dataKey, config]) => ({\n          dataKey,\n          colorsCount: getColorsCount(config),\n        })),\n    [chartConfig, keys],\n  );\n\n  const dashArray =\n    strokeVariant === \"dashed\" || strokeVariant === \"animated-dashed\" ? \"4 4\" : undefined;\n\n  const defsContent = (\n    <>\n      {/* Vertical fade gradient for area fill mask */}\n      {variant === \"area\" && (\n        <linearGradient id={`${chartId}-zm-vertical-fade`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n          <stop offset=\"0%\" stopColor=\"white\" stopOpacity={0.15} />\n          <stop offset=\"100%\" stopColor=\"white\" stopOpacity={0} />\n        </linearGradient>\n      )}\n      {gradients.map(({ dataKey, colorsCount }) => {\n        const colorStops =\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 }, (_, i) => (\n              <stop\n                key={i}\n                offset={`${(i / (colorsCount - 1)) * 100}%`}\n                stopColor={`var(--color-${dataKey}-${i}, var(--color-${dataKey}-0))`}\n              />\n            ))\n          );\n\n        return (\n          <React.Fragment key={dataKey}>\n            {/* Vertical color gradient (stroke + bar fill) */}\n            <linearGradient id={`${chartId}-zm-${dataKey}`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n              {colorStops}\n            </linearGradient>\n\n            {/* Area fill: color gradient masked with vertical fade */}\n            {variant === \"area\" && (\n              <>\n                <mask id={`${chartId}-zm-fill-mask-${dataKey}`}>\n                  <rect width=\"100%\" height=\"100%\" fill={`url(#${chartId}-zm-vertical-fade)`} />\n                </mask>\n                <pattern\n                  id={`${chartId}-zm-fill-${dataKey}`}\n                  patternUnits=\"userSpaceOnUse\"\n                  width=\"100%\"\n                  height=\"100%\"\n                >\n                  <rect\n                    width=\"100%\"\n                    height=\"100%\"\n                    fill={`url(#${chartId}-zm-${dataKey})`}\n                    mask={`url(#${chartId}-zm-fill-mask-${dataKey})`}\n                  />\n                </pattern>\n              </>\n            )}\n          </React.Fragment>\n        );\n      })}\n    </>\n  );\n\n  if (variant === \"line\") {\n    return (\n      <ResponsiveContainer width=\"100%\" height=\"100%\">\n        <LineChart data={data} margin={{ top: 4, right: 0, bottom: 0, left: 0 }}>\n          <defs>{defsContent}</defs>\n          {keys.map((dk) => (\n            <Line\n              key={dk}\n              type={curveType}\n              dataKey={dk}\n              stroke={`url(#${chartId}-zm-${dk})`}\n              strokeWidth={1}\n              strokeOpacity={0.5}\n              strokeDasharray={dashArray}\n              connectNulls={connectNulls}\n              dot={false}\n              activeDot={false}\n              isAnimationActive={false}\n            />\n          ))}\n        </LineChart>\n      </ResponsiveContainer>\n    );\n  }\n\n  if (variant === \"bar\") {\n    const r = barRadius ?? 3;\n    return (\n      <ResponsiveContainer width=\"100%\" height=\"100%\">\n        <BarChart\n          data={data}\n          margin={{ top: 2, right: 0, bottom: 0, left: 0 }}\n          barGap={2}\n          barSize={14}\n        >\n          <defs>{defsContent}</defs>\n          {keys.map((dk) => (\n            <Bar\n              key={dk}\n              dataKey={dk}\n              fill={`url(#${chartId}-zm-${dk})`}\n              fillOpacity={0.35}\n              stackId={stacked ? \"zm-stack\" : undefined}\n              isAnimationActive={false}\n              radius={[r, r, r, r]}\n            />\n          ))}\n        </BarChart>\n      </ResponsiveContainer>\n    );\n  }\n\n  // Default: area\n  return (\n    <ResponsiveContainer width=\"100%\" height=\"100%\">\n      <AreaChart data={data} margin={{ top: 4, right: 0, bottom: 0, left: 0 }}>\n        <defs>{defsContent}</defs>\n        {keys.map((dk) => (\n          <Area\n            key={dk}\n            type={curveType}\n            dataKey={dk}\n            stroke={`url(#${chartId}-zm-${dk})`}\n            fill={`url(#${chartId}-zm-fill-${dk})`}\n            strokeWidth={1}\n            strokeOpacity={0.5}\n            strokeDasharray={dashArray}\n            connectNulls={connectNulls}\n            fillOpacity={1}\n            stackId={stacked ? \"zm-stack\" : undefined}\n            dot={false}\n            activeDot={false}\n            isAnimationActive={false}\n          />\n        ))}\n      </AreaChart>\n    </ResponsiveContainer>\n  );\n}\n\n// ─── useEvilBrush Hook ──────────────────────────────────────────────────────\n\nfunction useEvilBrush<TData extends Record<string, unknown>>({\n  data,\n  defaultStartIndex = 0,\n  defaultEndIndex,\n}: {\n  data: TData[];\n  defaultStartIndex?: number;\n  defaultEndIndex?: number;\n}) {\n  const [range, setRange] = React.useState<EvilBrushRange>({\n    startIndex: defaultStartIndex,\n    endIndex: defaultEndIndex ?? Math.max(0, data.length - 1),\n  });\n\n  // Defer the range used for data slicing — the brush handles move at the\n\n  // immediate `range` cadence while the expensive chart re-render uses the\n  // deferred value.  React can skip intermediate slices during fast drags.\n  const deferredRange = React.useDeferredValue(range);\n\n  useEffect(() => {\n    // eslint-disable-next-line react-hooks/set-state-in-effect\n    setRange({\n      startIndex: 0,\n      endIndex: Math.max(0, data.length - 1),\n    });\n  }, [data.length]);\n\n  const visibleData = React.useMemo(\n    () => data.slice(deferredRange.startIndex, deferredRange.endIndex + 1),\n    [data, deferredRange.startIndex, deferredRange.endIndex],\n  );\n\n  return {\n    range,\n    visibleData,\n    brushProps: {\n      startIndex: range.startIndex,\n      endIndex: range.endIndex,\n      onChange: setRange,\n    } satisfies Pick<EvilBrushProps, \"startIndex\" | \"endIndex\" | \"onChange\">,\n  };\n}\n\nexport { EvilBrush, useEvilBrush, type EvilBrushProps, type EvilBrushRange, type EvilBrushVariant };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}