{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "funnel-chart",
  "type": "registry:component",
  "title": "Funnel Chart",
  "description": "An animated funnel chart with multi-layer halo rings, hover interactions, and staggered entrance animations",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "@bklit/chart-animation",
    "@bklit/utils",
    "@bklit/chart-utils"
  ],
  "files": [
    {
      "path": "src/charts/funnel-chart.tsx",
      "content": "\"use client\";\n\nimport type { Transition } from \"motion/react\";\nimport { motion, useTransform } from \"motion/react\";\nimport {\n  type CSSProperties,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useEnterComplete } from \"./use-enter-complete\";\nimport { useMountProgress } from \"./use-mount-progress\";\n\n// ─── Public types ───────────────────────────────────────────────────\n\nexport interface FunnelGradientStop {\n  offset: string | number;\n  color: string;\n}\n\nexport interface FunnelStage {\n  label: string;\n  value: number;\n  displayValue?: string;\n  /** Override the chart-level color for this segment */\n  color?: string;\n  /**\n   * Apply a linear gradient to this segment.\n   * Provide an array of color stops, e.g. `[{ offset: \"0%\", color: \"#8B5CF6\" }, { offset: \"100%\", color: \"#3B82F6\" }]`.\n   * When set, this takes priority over the segment and chart-level `color` for the innermost ring.\n   * Outer halo rings use the first stop color as their solid color.\n   */\n  gradient?: FunnelGradientStop[];\n}\n\nexport interface FunnelChartProps {\n  data: FunnelStage[];\n  orientation?: \"horizontal\" | \"vertical\";\n  color?: string;\n  layers?: number;\n  className?: string;\n  style?: CSSProperties;\n  showPercentage?: boolean;\n  showValues?: boolean;\n  showLabels?: boolean;\n  /** Controlled hover state — index of the hovered segment */\n  hoveredIndex?: number | null;\n  /** Callback when hover state changes */\n  onHoverChange?: (index: number | null) => void;\n  formatPercentage?: (pct: number) => string;\n  formatValue?: (value: number) => string;\n  /** Stagger delay between segments in seconds. Default 0.12 */\n  staggerDelay?: number;\n  /** Framer Motion transition for segment enter animation */\n  enterTransition?: Transition;\n  /** Gap between segments in pixels. Default 4 */\n  gap?: number;\n  /**\n   * Render a visx pattern definition. Receives a unique `id` string per segment\n   * and the resolved `color`. Return a `<PatternLines>` (or any visx pattern)\n   * inside an SVG `<defs>`. The component will use `fill=\"url(#id)\"` on the\n   * innermost ring while keeping outer halo rings as solid color.\n   */\n  renderPattern?: (id: string, color: string) => ReactNode;\n  /** Edge style for the funnel segments. Default \"curved\" */\n  edges?: \"curved\" | \"straight\";\n  /**\n   * Controls how segment labels (value, percentage, stage name) are arranged.\n   * - \"spread\": Value/percentage/label are spread apart (top/center/bottom for horizontal,\n   *   left/center/right for vertical). This is the default.\n   * - \"grouped\": All label items stack together in a tight group.\n   *\n   * When \"grouped\", use `labelOrientation` and `labelAlign` for full control.\n   */\n  labelLayout?: \"spread\" | \"grouped\";\n  /**\n   * Stack direction of the label group. Only applies when `labelLayout=\"grouped\"`.\n   * - \"vertical\": Items stack top-to-bottom. Default for horizontal funnels.\n   * - \"horizontal\": Items stack left-to-right. Default for vertical funnels.\n   */\n  labelOrientation?: \"vertical\" | \"horizontal\";\n  /**\n   * Where the label group sits within the segment cell.\n   * - \"center\" (default), \"start\", \"end\"\n   * For horizontal funnel: start=top, end=bottom.\n   * For vertical funnel: start=left, end=right.\n   */\n  labelAlign?: \"center\" | \"start\" | \"end\";\n  /** Grid configuration. Pass `true` for default bands + lines, or an object for fine control. */\n  grid?:\n    | boolean\n    | {\n        /** Show alternating background bands behind each segment. Default true */\n        bands?: boolean;\n        /** Color of the background bands. Default \"var(--color-muted)\" */\n        bandColor?: string;\n        /** Show grid lines at each gap between segments. Default true */\n        lines?: boolean;\n        /** Color of the grid lines. Default \"var(--chart-grid)\" */\n        lineColor?: string;\n        /** Opacity of the grid lines. Default 1 */\n        lineOpacity?: number;\n        /** Width of the grid lines in pixels. Default 1 */\n        lineWidth?: number;\n      };\n}\n\n// ─── Defaults ───────────────────────────────────────────────────────\n\nimport { intFmt } from \"./chart-formatters\";\n\nconst fmtPct = (p: number) => `${Math.round(p)}%`;\nconst fmtVal = intFmt;\n\n// ─── SVG helpers ────────────────────────────────────────────────────\n\n/**\n * Builds a single segment path for one stage in the funnel.\n * Each segment is a smooth trapezoid-like shape transitioning from\n * the height of the current norm to the next norm.\n */\nfunction hSegmentPath(\n  normStart: number,\n  normEnd: number,\n  segW: number,\n  H: number,\n  layerScale: number,\n  straight = false\n) {\n  const my = H / 2;\n  const h0 = normStart * H * 0.44 * layerScale;\n  const h1 = normEnd * H * 0.44 * layerScale;\n\n  if (straight) {\n    return `M 0 ${my - h0} L ${segW} ${my - h1} L ${segW} ${my + h1} L 0 ${my + h0} Z`;\n  }\n\n  const cx = segW * 0.55;\n  const top = `M 0 ${my - h0} C ${cx} ${my - h0}, ${segW - cx} ${my - h1}, ${segW} ${my - h1}`;\n  const bot = `L ${segW} ${my + h1} C ${segW - cx} ${my + h1}, ${cx} ${my + h0}, 0 ${my + h0}`;\n  return `${top} ${bot} Z`;\n}\n\nfunction vSegmentPath(\n  normStart: number,\n  normEnd: number,\n  segH: number,\n  W: number,\n  layerScale: number,\n  straight = false\n) {\n  const mx = W / 2;\n  const w0 = normStart * W * 0.44 * layerScale;\n  const w1 = normEnd * W * 0.44 * layerScale;\n\n  if (straight) {\n    return `M ${mx - w0} 0 L ${mx - w1} ${segH} L ${mx + w1} ${segH} L ${mx + w0} 0 Z`;\n  }\n\n  const cy = segH * 0.55;\n  const left = `M ${mx - w0} 0 C ${mx - w0} ${cy}, ${mx - w1} ${segH - cy}, ${mx - w1} ${segH}`;\n  const right = `L ${mx + w1} ${segH} C ${mx + w1} ${segH - cy}, ${mx + w0} ${cy}, ${mx + w0} 0`;\n  return `${left} ${right} Z`;\n}\n\n// ─── Animated Segment ───────────────────────────────────────────────\n\nfunction HRing({\n  d,\n  color,\n  fill,\n  opacity,\n  hovered,\n  ringIndex,\n  totalRings,\n}: {\n  d: string;\n  color: string;\n  fill?: string;\n  opacity: number;\n  hovered: boolean;\n  ringIndex: number;\n  totalRings: number;\n}) {\n  const extraScale = 1 + (ringIndex / Math.max(totalRings - 1, 1)) * 0.12;\n\n  return (\n    <motion.path\n      animate={{ scaleY: hovered ? extraScale : 1 }}\n      d={d}\n      fill={fill ?? color}\n      opacity={opacity}\n      style={{ transformOrigin: \"center center\" }}\n      transition={{\n        type: \"spring\",\n        stiffness: 300 - ringIndex * 60,\n        damping: 24 - ringIndex * 3,\n      }}\n    />\n  );\n}\n\nfunction HSegment({\n  index,\n  normStart,\n  normEnd,\n  segW,\n  fullH,\n  color,\n  layers,\n  staggerDelay,\n  enterTransition,\n  hovered,\n  dimmed,\n  renderPattern,\n  straight,\n  gradientStops,\n}: {\n  index: number;\n  normStart: number;\n  normEnd: number;\n  segW: number;\n  fullH: number;\n  color: string;\n  layers: number;\n  staggerDelay: number;\n  enterTransition?: Transition;\n  hovered: boolean;\n  dimmed: boolean;\n  renderPattern?: (id: string, color: string) => ReactNode;\n  straight: boolean;\n  gradientStops?: FunnelGradientStop[];\n}) {\n  const patternId = `funnel-h-pattern-${index}`;\n  const gradientId = `funnel-h-grad-${index}`;\n  const mountProgress = useMountProgress(\n    enterTransition,\n    index * staggerDelay,\n    index\n  );\n  const enterComplete = useEnterComplete(mountProgress);\n  const entranceScaleX = useTransform(mountProgress, [0, 1], [0, 1]);\n  const entranceScaleY = useTransform(mountProgress, [0, 1], [0, 1]);\n\n  const rings = Array.from({ length: layers }, (_, l) => {\n    const scale = 1 - (l / layers) * 0.35;\n    const opacity = 0.18 + (l / (layers - 1 || 1)) * 0.65;\n    return {\n      d: hSegmentPath(normStart, normEnd, segW, fullH, scale, straight),\n      opacity,\n    };\n  });\n\n  return (\n    <motion.div\n      animate={{ opacity: dimmed ? 0.4 : 1 }}\n      className=\"pointer-events-none relative shrink-0 overflow-visible\"\n      style={{\n        width: segW,\n        height: fullH,\n        zIndex: hovered ? 10 : 1,\n      }}\n      transition={{ opacity: { duration: 0.15 } }}\n    >\n      {enterComplete ? (\n        <div className=\"absolute inset-0 overflow-visible\">\n          <svg\n            aria-hidden=\"true\"\n            className=\"absolute inset-0 h-full w-full overflow-visible\"\n            preserveAspectRatio=\"none\"\n            role=\"presentation\"\n            viewBox={`0 0 ${segW} ${fullH}`}\n          >\n            <defs>\n              {gradientStops && (\n                <linearGradient id={gradientId} x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\">\n                  {gradientStops.map((stop) => (\n                    <stop\n                      key={`${stop.offset}-${stop.color}`}\n                      offset={\n                        typeof stop.offset === \"number\"\n                          ? `${stop.offset * 100}%`\n                          : stop.offset\n                      }\n                      stopColor={stop.color}\n                    />\n                  ))}\n                </linearGradient>\n              )}\n              {renderPattern?.(patternId, color)}\n            </defs>\n            {rings.map((r, i) => {\n              const isInnermost = i === rings.length - 1;\n              let ringFill: string | undefined;\n              if (isInnermost && renderPattern) {\n                ringFill = `url(#${patternId})`;\n              } else if (isInnermost && gradientStops) {\n                ringFill = `url(#${gradientId})`;\n              }\n              const ringKey = `h-ring-${r.opacity.toFixed(2)}`;\n              return (\n                <HRing\n                  color={color}\n                  d={r.d}\n                  fill={ringFill}\n                  hovered={hovered}\n                  key={ringKey}\n                  opacity={r.opacity}\n                  ringIndex={i}\n                  totalRings={layers}\n                />\n              );\n            })}\n          </svg>\n        </div>\n      ) : (\n        <motion.div\n          className=\"absolute inset-0 overflow-visible\"\n          style={{\n            scaleX: entranceScaleX,\n            scaleY: entranceScaleY,\n            transformOrigin: \"left center\",\n          }}\n        >\n          <svg\n            aria-hidden=\"true\"\n            className=\"absolute inset-0 h-full w-full overflow-visible\"\n            preserveAspectRatio=\"none\"\n            role=\"presentation\"\n            viewBox={`0 0 ${segW} ${fullH}`}\n          >\n            <defs>\n              {gradientStops && (\n                <linearGradient id={gradientId} x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\">\n                  {gradientStops.map((stop) => (\n                    <stop\n                      key={`${stop.offset}-${stop.color}`}\n                      offset={\n                        typeof stop.offset === \"number\"\n                          ? `${stop.offset * 100}%`\n                          : stop.offset\n                      }\n                      stopColor={stop.color}\n                    />\n                  ))}\n                </linearGradient>\n              )}\n              {renderPattern?.(patternId, color)}\n            </defs>\n            {rings.map((r, i) => {\n              const isInnermost = i === rings.length - 1;\n              let ringFill: string | undefined;\n              if (isInnermost && renderPattern) {\n                ringFill = `url(#${patternId})`;\n              } else if (isInnermost && gradientStops) {\n                ringFill = `url(#${gradientId})`;\n              }\n              const ringKey = `h-ring-${r.opacity.toFixed(2)}`;\n              return (\n                <HRing\n                  color={color}\n                  d={r.d}\n                  fill={ringFill}\n                  hovered={hovered}\n                  key={ringKey}\n                  opacity={r.opacity}\n                  ringIndex={i}\n                  totalRings={layers}\n                />\n              );\n            })}\n          </svg>\n        </motion.div>\n      )}\n    </motion.div>\n  );\n}\n\nfunction VRing({\n  d,\n  color,\n  fill,\n  opacity,\n  hovered,\n  ringIndex,\n  totalRings,\n}: {\n  d: string;\n  color: string;\n  fill?: string;\n  opacity: number;\n  hovered: boolean;\n  ringIndex: number;\n  totalRings: number;\n}) {\n  const extraScale = 1 + (ringIndex / Math.max(totalRings - 1, 1)) * 0.12;\n\n  return (\n    <motion.path\n      animate={{ scaleX: hovered ? extraScale : 1 }}\n      d={d}\n      fill={fill ?? color}\n      opacity={opacity}\n      style={{ transformOrigin: \"center center\" }}\n      transition={{\n        type: \"spring\",\n        stiffness: 300 - ringIndex * 60,\n        damping: 24 - ringIndex * 3,\n      }}\n    />\n  );\n}\n\nfunction VSegment({\n  index,\n  normStart,\n  normEnd,\n  segH,\n  fullW,\n  color,\n  layers,\n  staggerDelay,\n  enterTransition,\n  hovered,\n  dimmed,\n  renderPattern,\n  straight,\n  gradientStops,\n}: {\n  index: number;\n  normStart: number;\n  normEnd: number;\n  segH: number;\n  fullW: number;\n  color: string;\n  layers: number;\n  staggerDelay: number;\n  enterTransition?: Transition;\n  hovered: boolean;\n  dimmed: boolean;\n  renderPattern?: (id: string, color: string) => ReactNode;\n  straight: boolean;\n  gradientStops?: FunnelGradientStop[];\n}) {\n  const patternId = `funnel-v-pattern-${index}`;\n  const gradientId = `funnel-v-grad-${index}`;\n  const mountProgress = useMountProgress(\n    enterTransition,\n    index * staggerDelay,\n    index\n  );\n  const enterComplete = useEnterComplete(mountProgress);\n  const entranceScaleY = useTransform(mountProgress, [0, 1], [0, 1]);\n  const entranceScaleX = useTransform(mountProgress, [0, 1], [0, 1]);\n\n  const rings = Array.from({ length: layers }, (_, l) => {\n    const scale = 1 - (l / layers) * 0.35;\n    const opacity = 0.18 + (l / (layers - 1 || 1)) * 0.65;\n    return {\n      d: vSegmentPath(normStart, normEnd, segH, fullW, scale, straight),\n      opacity,\n    };\n  });\n\n  return (\n    <motion.div\n      animate={{ opacity: dimmed ? 0.4 : 1 }}\n      className=\"pointer-events-none relative shrink-0 overflow-visible\"\n      style={{\n        width: fullW,\n        height: segH,\n        zIndex: hovered ? 10 : 1,\n      }}\n      transition={{ opacity: { duration: 0.15 } }}\n    >\n      {enterComplete ? (\n        <div className=\"absolute inset-0 overflow-visible\">\n          <svg\n            aria-hidden=\"true\"\n            className=\"absolute inset-0 h-full w-full overflow-visible\"\n            preserveAspectRatio=\"none\"\n            role=\"presentation\"\n            viewBox={`0 0 ${fullW} ${segH}`}\n          >\n            <defs>\n              {gradientStops && (\n                <linearGradient id={gradientId} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n                  {gradientStops.map((stop) => (\n                    <stop\n                      key={`${stop.offset}-${stop.color}`}\n                      offset={\n                        typeof stop.offset === \"number\"\n                          ? `${stop.offset * 100}%`\n                          : stop.offset\n                      }\n                      stopColor={stop.color}\n                    />\n                  ))}\n                </linearGradient>\n              )}\n              {renderPattern?.(patternId, color)}\n            </defs>\n            {rings.map((r, i) => {\n              const isInnermost = i === rings.length - 1;\n              let ringFill: string | undefined;\n              if (isInnermost && renderPattern) {\n                ringFill = `url(#${patternId})`;\n              } else if (isInnermost && gradientStops) {\n                ringFill = `url(#${gradientId})`;\n              }\n              const ringKey = `v-ring-${r.opacity.toFixed(2)}`;\n              return (\n                <VRing\n                  color={color}\n                  d={r.d}\n                  fill={ringFill}\n                  hovered={hovered}\n                  key={ringKey}\n                  opacity={r.opacity}\n                  ringIndex={i}\n                  totalRings={layers}\n                />\n              );\n            })}\n          </svg>\n        </div>\n      ) : (\n        <motion.div\n          className=\"absolute inset-0 overflow-visible\"\n          style={{\n            scaleY: entranceScaleY,\n            scaleX: entranceScaleX,\n            transformOrigin: \"center top\",\n          }}\n        >\n          <svg\n            aria-hidden=\"true\"\n            className=\"absolute inset-0 h-full w-full overflow-visible\"\n            preserveAspectRatio=\"none\"\n            role=\"presentation\"\n            viewBox={`0 0 ${fullW} ${segH}`}\n          >\n            <defs>\n              {gradientStops && (\n                <linearGradient id={gradientId} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n                  {gradientStops.map((stop) => (\n                    <stop\n                      key={`${stop.offset}-${stop.color}`}\n                      offset={\n                        typeof stop.offset === \"number\"\n                          ? `${stop.offset * 100}%`\n                          : stop.offset\n                      }\n                      stopColor={stop.color}\n                    />\n                  ))}\n                </linearGradient>\n              )}\n              {renderPattern?.(patternId, color)}\n            </defs>\n            {rings.map((r, i) => {\n              const isInnermost = i === rings.length - 1;\n              let ringFill: string | undefined;\n              if (isInnermost && renderPattern) {\n                ringFill = `url(#${patternId})`;\n              } else if (isInnermost && gradientStops) {\n                ringFill = `url(#${gradientId})`;\n              }\n              const ringKey = `v-ring-${r.opacity.toFixed(2)}`;\n              return (\n                <VRing\n                  color={color}\n                  d={r.d}\n                  fill={ringFill}\n                  hovered={hovered}\n                  key={ringKey}\n                  opacity={r.opacity}\n                  ringIndex={i}\n                  totalRings={layers}\n                />\n              );\n            })}\n          </svg>\n        </motion.div>\n      )}\n    </motion.div>\n  );\n}\n\n// ─── Label overlay ──────────────────────────────────────────────────\n\nfunction SegmentLabel({\n  stage,\n  pct,\n  isHorizontal,\n  showValues,\n  showPercentage,\n  showLabels,\n  formatPercentage,\n  formatValue,\n  index,\n  staggerDelay,\n  layout = \"spread\",\n  orientation,\n  align = \"center\",\n}: {\n  stage: FunnelStage;\n  pct: number;\n  isHorizontal: boolean;\n  showValues: boolean;\n  showPercentage: boolean;\n  showLabels: boolean;\n  formatPercentage: (p: number) => string;\n  formatValue: (v: number) => string;\n  index: number;\n  staggerDelay: number;\n  layout?: \"spread\" | \"grouped\";\n  orientation?: \"vertical\" | \"horizontal\";\n  align?: \"center\" | \"start\" | \"end\";\n}) {\n  const display = stage.displayValue ?? formatValue(stage.value);\n\n  const valueEl = showValues && (\n    <span className=\"whitespace-nowrap font-semibold text-foreground text-sm\">\n      {display}\n    </span>\n  );\n  const pctEl = showPercentage && (\n    <span className=\"rounded-full bg-foreground px-3 py-1 font-bold text-background text-xs shadow-sm\">\n      {formatPercentage(pct)}\n    </span>\n  );\n  const labelEl = showLabels && (\n    <span className=\"whitespace-nowrap font-medium text-muted-foreground text-xs\">\n      {stage.label}\n    </span>\n  );\n\n  // ── Spread layout (default): items pushed to edges with center element ──\n  if (layout === \"spread\") {\n    return (\n      <motion.div\n        animate={{ opacity: 1 }}\n        className={cn(\n          \"absolute inset-0 flex\",\n          isHorizontal ? \"flex-col items-center\" : \"flex-row items-center\"\n        )}\n        initial={{ opacity: 0 }}\n        transition={{\n          delay: index * staggerDelay + 0.25,\n          duration: 0.35,\n          ease: \"easeOut\",\n        }}\n      >\n        {isHorizontal ? (\n          <>\n            <div className=\"flex h-[16%] items-end justify-center pb-1\">\n              {valueEl}\n            </div>\n            <div className=\"flex flex-1 items-center justify-center\">\n              {pctEl}\n            </div>\n            <div className=\"flex h-[16%] items-start justify-center pt-1\">\n              {labelEl}\n            </div>\n          </>\n        ) : (\n          <>\n            <div className=\"flex w-[16%] items-center justify-end pr-2\">\n              {valueEl}\n            </div>\n            <div className=\"flex flex-1 items-center justify-center\">\n              {pctEl}\n            </div>\n            <div className=\"flex w-[16%] items-center justify-start pl-2\">\n              {labelEl}\n            </div>\n          </>\n        )}\n      </motion.div>\n    );\n  }\n\n  // ── Grouped layout: items stacked tightly together ──\n  const resolvedOrientation =\n    orientation ?? (isHorizontal ? \"vertical\" : \"horizontal\");\n  const isVerticalStack = resolvedOrientation === \"vertical\";\n\n  // Map align to flexbox alignment on the cross axes\n  const justifyMap = {\n    start: \"justify-start\",\n    center: \"justify-center\",\n    end: \"justify-end\",\n  } as const;\n  const itemsMap = {\n    start: \"items-start\",\n    center: \"items-center\",\n    end: \"items-end\",\n  } as const;\n\n  // The outer container uses the chart orientation to position the group,\n  // and the inner group uses the label orientation for stacking.\n  return (\n    <motion.div\n      animate={{ opacity: 1 }}\n      className={cn(\n        \"absolute inset-0 flex\",\n        // For horizontal funnel, align controls vertical placement\n        // For vertical funnel, align controls horizontal placement\n        isHorizontal\n          ? cn(\"flex-col items-center\", justifyMap[align])\n          : cn(\"flex-row items-center\", justifyMap[align])\n      )}\n      initial={{ opacity: 0 }}\n      style={{\n        padding: isHorizontal ? \"8% 0\" : \"0 8%\",\n      }}\n      transition={{\n        delay: index * staggerDelay + 0.25,\n        duration: 0.35,\n        ease: \"easeOut\",\n      }}\n    >\n      <div\n        className={cn(\n          \"flex gap-1.5\",\n          isVerticalStack\n            ? cn(\"flex-col\", itemsMap[isHorizontal ? \"center\" : align])\n            : cn(\"flex-row\", itemsMap.center)\n        )}\n      >\n        {valueEl}\n        {pctEl}\n        {labelEl}\n      </div>\n    </motion.div>\n  );\n}\n\n// ─── Main Component ─────────────────────────────────────────────────\n\nexport function FunnelChart({\n  data,\n  orientation = \"horizontal\",\n  color = \"var(--chart-1)\",\n  layers = 3,\n  className,\n  style,\n  showPercentage = true,\n  showValues = true,\n  showLabels = true,\n  hoveredIndex: hoveredIndexProp,\n  onHoverChange,\n  formatPercentage = fmtPct,\n  formatValue = fmtVal,\n  staggerDelay = 0.12,\n  enterTransition,\n  gap = 4,\n  renderPattern,\n  edges = \"curved\",\n  labelLayout = \"spread\",\n  labelOrientation,\n  labelAlign = \"center\",\n  grid: gridProp = false,\n}: FunnelChartProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const [sz, setSz] = useState({ w: 0, h: 0 });\n  const [internalHoveredIndex, setInternalHoveredIndex] = useState<\n    number | null\n  >(null);\n\n  const isControlled = hoveredIndexProp !== undefined;\n  const hoveredIndex = isControlled ? hoveredIndexProp : internalHoveredIndex;\n  const setHoveredIndex = useCallback(\n    (index: number | null) => {\n      if (isControlled) {\n        onHoverChange?.(index);\n      } else {\n        setInternalHoveredIndex(index);\n      }\n    },\n    [isControlled, onHoverChange]\n  );\n\n  const measure = useCallback(() => {\n    if (!ref.current) {\n      return;\n    }\n    const { width: w, height: h } = ref.current.getBoundingClientRect();\n    if (w > 0 && h > 0) {\n      setSz({ w, h });\n    }\n  }, []);\n\n  useEffect(() => {\n    measure();\n    const ro = new ResizeObserver(measure);\n    if (ref.current) {\n      ro.observe(ref.current);\n    }\n    return () => ro.disconnect();\n  }, [measure]);\n\n  if (!data.length) {\n    return null;\n  }\n\n  const first = data[0];\n  if (!first) {\n    return null;\n  }\n  const max = first.value;\n  const n = data.length;\n  const norms = data.map((d) => d.value / max);\n  const horiz = orientation === \"horizontal\";\n  const { w: W, h: H } = sz;\n\n  const totalGap = gap * (n - 1);\n  const segW = (W - (horiz ? totalGap : 0)) / n;\n  const segH = (H - (horiz ? 0 : totalGap)) / n;\n\n  // Resolve grid config\n  const gridEnabled = gridProp !== false;\n  const gridCfg = typeof gridProp === \"object\" ? gridProp : {};\n  const showBands = gridEnabled && (gridCfg.bands ?? true);\n  const bandColor = gridCfg.bandColor ?? \"var(--color-muted)\";\n  const showGridLines = gridEnabled && (gridCfg.lines ?? true);\n  const gridLineColor = gridCfg.lineColor ?? \"var(--chart-grid)\";\n  const gridLineOpacity = gridCfg.lineOpacity ?? 1;\n  const gridLineWidth = gridCfg.lineWidth ?? 1;\n\n  return (\n    <div\n      className={cn(\"relative w-full select-none overflow-visible\", className)}\n      ref={ref}\n      style={{\n        aspectRatio: horiz ? \"2.2 / 1\" : \"1 / 1.8\",\n        ...style,\n      }}\n    >\n      {W > 0 && H > 0 && (\n        <>\n          {/* Grid layer: background bands + grid lines */}\n          {gridEnabled && (\n            <svg\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute inset-0 h-full w-full\"\n              preserveAspectRatio=\"none\"\n              role=\"presentation\"\n              viewBox={`0 0 ${W} ${H}`}\n            >\n              {/* Background bands — alternating on even segments */}\n              {showBands &&\n                data.map((stage, i) => {\n                  if (i % 2 !== 0) {\n                    return null;\n                  }\n                  if (horiz) {\n                    const x = (segW + gap) * i;\n                    return (\n                      <rect\n                        fill={bandColor}\n                        height={H}\n                        key={`band-${stage.label}`}\n                        width={segW}\n                        x={x}\n                        y={0}\n                      />\n                    );\n                  }\n                  const y = (segH + gap) * i;\n                  return (\n                    <rect\n                      fill={bandColor}\n                      height={segH}\n                      key={`band-${stage.label}`}\n                      width={W}\n                      x={0}\n                      y={y}\n                    />\n                  );\n                })}\n            </svg>\n          )}\n\n          {/* Segments container — overflow-visible so hover scale is not clipped */}\n          <div\n            className={cn(\n              \"absolute inset-0 flex overflow-visible\",\n              horiz ? \"flex-row\" : \"flex-col\"\n            )}\n            style={{ gap }}\n          >\n            {data.map((stage, i) => {\n              const normStart = norms[i] ?? 0;\n              const normEnd = norms[Math.min(i + 1, n - 1)] ?? 0;\n              const firstStop = stage.gradient?.[0];\n              const segColor = firstStop\n                ? firstStop.color\n                : (stage.color ?? color);\n\n              return horiz ? (\n                <HSegment\n                  color={segColor}\n                  dimmed={hoveredIndex !== null && hoveredIndex !== i}\n                  enterTransition={enterTransition}\n                  fullH={H}\n                  gradientStops={stage.gradient}\n                  hovered={hoveredIndex === i}\n                  index={i}\n                  key={stage.label}\n                  layers={layers}\n                  normEnd={normEnd}\n                  normStart={normStart}\n                  renderPattern={renderPattern}\n                  segW={segW}\n                  staggerDelay={staggerDelay}\n                  straight={edges === \"straight\"}\n                />\n              ) : (\n                <VSegment\n                  color={segColor}\n                  dimmed={hoveredIndex !== null && hoveredIndex !== i}\n                  enterTransition={enterTransition}\n                  fullW={W}\n                  gradientStops={stage.gradient}\n                  hovered={hoveredIndex === i}\n                  index={i}\n                  key={stage.label}\n                  layers={layers}\n                  normEnd={normEnd}\n                  normStart={normStart}\n                  renderPattern={renderPattern}\n                  segH={segH}\n                  staggerDelay={staggerDelay}\n                  straight={edges === \"straight\"}\n                />\n              );\n            })}\n          </div>\n\n          {/* Grid lines — rendered above segments so they're visible */}\n          {gridEnabled && showGridLines && (\n            <svg\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute inset-0 h-full w-full\"\n              preserveAspectRatio=\"none\"\n              role=\"presentation\"\n              viewBox={`0 0 ${W} ${H}`}\n            >\n              {Array.from({ length: n - 1 }, (_, i) => {\n                const idx = i + 1;\n                const gridKey = `grid-${idx}`;\n                if (horiz) {\n                  const x = segW * idx + gap * i + gap / 2;\n                  return (\n                    <line\n                      key={gridKey}\n                      stroke={gridLineColor}\n                      strokeOpacity={gridLineOpacity}\n                      strokeWidth={gridLineWidth}\n                      x1={x}\n                      x2={x}\n                      y1={0}\n                      y2={H}\n                    />\n                  );\n                }\n                const y = segH * idx + gap * i + gap / 2;\n                return (\n                  <line\n                    key={gridKey}\n                    stroke={gridLineColor}\n                    strokeOpacity={gridLineOpacity}\n                    strokeWidth={gridLineWidth}\n                    x1={0}\n                    x2={W}\n                    y1={y}\n                    y2={y}\n                  />\n                );\n              })}\n            </svg>\n          )}\n\n          {/* Label overlays — one per segment, positioned over each segment cell.\n              These are the hover triggers for each segment. */}\n          {data.map((stage, i) => {\n            const pct = (stage.value / max) * 100;\n            const posStyle: CSSProperties = horiz\n              ? {\n                  left: (segW + gap) * i,\n                  width: segW,\n                  top: 0,\n                  height: H,\n                }\n              : {\n                  top: (segH + gap) * i,\n                  height: segH,\n                  left: 0,\n                  width: W,\n                };\n\n            const isDimmed = hoveredIndex !== null && hoveredIndex !== i;\n\n            return (\n              <motion.div\n                animate={{ opacity: isDimmed ? 0.4 : 1 }}\n                className=\"absolute cursor-pointer\"\n                key={`lbl-${stage.label}`}\n                onMouseEnter={() => setHoveredIndex(i)}\n                onMouseLeave={() => setHoveredIndex(null)}\n                style={{ ...posStyle, zIndex: 20 }}\n                transition={{ type: \"spring\", stiffness: 300, damping: 24 }}\n              >\n                <SegmentLabel\n                  align={labelAlign}\n                  formatPercentage={formatPercentage}\n                  formatValue={formatValue}\n                  index={i}\n                  isHorizontal={horiz}\n                  layout={labelLayout}\n                  orientation={labelOrientation}\n                  pct={pct}\n                  showLabels={showLabels}\n                  showPercentage={showPercentage}\n                  showValues={showValues}\n                  stage={stage}\n                  staggerDelay={staggerDelay}\n                />\n              </motion.div>\n            );\n          })}\n        </>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/funnel-chart.tsx"
    }
  ]
}