{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bar-chart",
  "type": "registry:component",
  "title": "Bar Chart",
  "description": "A composable bar chart with horizontal/vertical orientations and animations",
  "dependencies": [
    "@visx/gradient@4.0.1-alpha.0",
    "@visx/pattern@4.0.1-alpha.0",
    "@visx/shape@4.0.1-alpha.0",
    "motion"
  ],
  "registryDependencies": [
    "@bklit/chart-context",
    "@bklit/chart-animation",
    "@bklit/grid",
    "@bklit/chart-tooltip",
    "@bklit/utils"
  ],
  "files": [
    {
      "path": "src/charts/bar-chart.tsx",
      "content": "\"use client\";\n\nimport { localPoint } from \"@visx/event\";\nimport { ParentSize } from \"@visx/responsive\";\nimport { scaleBand, scaleLinear } from \"@visx/scale\";\nimport type { Transition } from \"motion/react\";\nimport {\n  memo,\n  type ReactElement,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { DEFAULT_ANIMATION_EASING } from \"./animation\";\nimport type { BarProps } from \"./bar\";\nimport { topSquareCenterY } from \"./bar-squares-layout\";\nimport {\n  forEachChartChild,\n  isChartClipPassthrough,\n  isClipExcludedComponent,\n  isPostOverlayComponent,\n  isUnderlayComponent,\n  renderKeyedChartLayers,\n  resolveChartChildElement,\n} from \"./chart-child-passthrough\";\nimport {\n  ChartProvider,\n  type LineConfig,\n  type Margin,\n  type TooltipData,\n} from \"./chart-context\";\nimport { isGradientDefComponent, isPatternDefComponent } from \"./chart-defs\";\nimport { shortDateFmt } from \"./chart-formatters\";\nimport {\n  type ChartPhase,\n  type ChartStatus,\n  DEFAULT_CHART_LIFECYCLE,\n  resolveRestingChartPhase,\n} from \"./chart-phase\";\nimport { BarLoadingSkeleton } from \"./loading-sweep\";\nimport { extractReferenceAreaConfigs } from \"./reference-area-config\";\nimport { useScheduledTooltip } from \"./use-scheduled-tooltip\";\nimport {\n  buildYScalesForLines,\n  getPrimaryYScale,\n  normalizeYAxisId,\n  wrapSingleYScale,\n} from \"./y-axis-scales\";\n\n/** Skeleton bars to show when `status=\"loading\"` and `data` is empty. */\nconst FALLBACK_LOADING_BARS = 12;\n\nexport type BarOrientation = \"vertical\" | \"horizontal\";\n\nexport interface BarChartProps {\n  /** Data array - each item should have an x-axis key and numeric values */\n  data: Record<string, unknown>[];\n  /** Key in data for the categorical axis. Default: \"name\" */\n  xDataKey?: string;\n  /** Chart margins */\n  margin?: Partial<Margin>;\n  /** Animation duration in milliseconds. Default: 1100 */\n  animationDuration?: number;\n  /** CSS easing for bar grow transitions. */\n  animationEasing?: string;\n  /** Motion enter transition (spring or cubic-bezier tween). */\n  enterTransition?: Transition;\n  /** Signature of motion URL state — triggers enter replay when it changes. */\n  revealSignature?: string;\n  /** Aspect ratio as \"width / height\". Default: \"2 / 1\" */\n  aspectRatio?: string;\n  /** Additional class name for the container */\n  className?: string;\n  /** Gap between bar groups as a fraction of band width (0-1). Default: 0.2 */\n  barGap?: number;\n  /** Fixed bar width in pixels. If not set, bars auto-size to fill the band. */\n  barWidth?: number;\n  /** Bar chart orientation. Default: \"vertical\" */\n  orientation?: BarOrientation;\n  /** Whether to stack bars instead of grouping them. Default: false */\n  stacked?: boolean;\n  /** Gap between stacked bar segments in pixels. Default: 0 */\n  stackGap?: number;\n  /** When set, tooltip Y positions snap to the top square center (shape variant). */\n  squareSnap?: { squareGap: number; groupGap?: number; fit?: boolean };\n  /** Child components (Bar, Grid, ChartTooltip, etc.). Optional — omit for a\n   * pure `status=\"loading\"` skeleton. */\n  children?: ReactNode;\n  /** Reports reveal lifecycle for OG screenshots and loading orchestration. */\n  onPhaseChange?: (phase: ChartPhase) => void;\n  /** Fetch / display status. When `\"loading\"`, a shimmer skeleton replaces the\n   * bars (no chart data required). Default: `\"ready\"`. */\n  status?: ChartStatus;\n}\n\nconst DEFAULT_MARGIN: Margin = { top: 40, right: 40, bottom: 40, left: 40 };\n\n// Extract bar configs from children synchronously\nfunction extractBarConfigs(children: ReactNode): LineConfig[] {\n  const configs: LineConfig[] = [];\n\n  forEachChartChild(children, (child) => {\n    const childType = child.type as {\n      displayName?: string;\n      name?: string;\n      __isBarDepthLayer?: boolean;\n    };\n    // Bar-depth surface layers (BarDepthBack/Front, BarPulse) carry a\n    // `dataKey` to pair with a Bar but are not series themselves — skip them\n    // so they don't inflate the series count and shrink the real bars.\n    if (childType.__isBarDepthLayer) {\n      return;\n    }\n    const componentName =\n      typeof child.type === \"function\"\n        ? childType.displayName || childType.name || \"\"\n        : \"\";\n\n    const props = child.props as BarProps | undefined;\n    const isBarComponent =\n      componentName === \"Bar\" ||\n      componentName === \"BarSquares\" ||\n      (props && typeof props.dataKey === \"string\" && props.dataKey.length > 0);\n\n    if (isBarComponent && props?.dataKey) {\n      // Use stroke for tooltip dot color if provided, otherwise fall back to fill\n      // This allows gradient/pattern fills to have a solid dot color\n      const dotColor =\n        props.stroke || props.fill || \"var(--chart-line-primary)\";\n      configs.push({\n        dataKey: props.dataKey,\n        stroke: dotColor,\n        strokeWidth: 0,\n        yAxisId: props.yAxisId,\n      });\n    }\n  });\n\n  return configs;\n}\n\ninterface ChartInnerProps {\n  width: number;\n  height: number;\n  data: Record<string, unknown>[];\n  xDataKey: string;\n  margin: Margin;\n  animationDuration: number;\n  animationEasing: string;\n  enterTransition?: Transition;\n  revealSignature?: string;\n  barGap: number;\n  barWidthProp?: number;\n  orientation: BarOrientation;\n  stacked: boolean;\n  stackGap: number;\n  squareSnap?: { squareGap: number; groupGap?: number; fit?: boolean };\n  children: ReactNode;\n  containerRef: React.RefObject<HTMLDivElement | null>;\n  onPhaseChange?: (phase: ChartPhase) => void;\n  status: ChartStatus;\n}\n\nfunction ChartInner(props: ChartInnerProps) {\n  const { width, height } = props;\n  if (width < 10 || height < 10) {\n    return null;\n  }\n  return <ChartCore {...props} />;\n}\n\nconst ChartCore = memo(function ChartCore({\n  width,\n  height,\n  data,\n  xDataKey,\n  margin,\n  animationDuration,\n  animationEasing,\n  enterTransition,\n  revealSignature = \"\",\n  barGap,\n  barWidthProp,\n  orientation,\n  stacked,\n  stackGap,\n  squareSnap,\n  children,\n  containerRef,\n  onPhaseChange,\n  status,\n}: ChartInnerProps) {\n  const { tooltipData, setTooltipData, scheduleTooltip, clearTooltip } =\n    useScheduledTooltip<TooltipData>();\n  const [isLoaded, setIsLoaded] = useState(false);\n  const [revealEpoch, setRevealEpoch] = useState(0);\n  const hoveredBarIndex = tooltipData?.index ?? null;\n\n  const isHorizontal = orientation === \"horizontal\";\n\n  // Extract bar configs synchronously from children\n  const lines = useMemo(() => extractBarConfigs(children), [children]);\n\n  const innerWidth = width - margin.left - margin.right;\n  const innerHeight = height - margin.top - margin.bottom;\n\n  // Category accessor function - returns string for categorical scale\n  const categoryAccessor = useCallback(\n    (d: Record<string, unknown>): string => {\n      const value = d[xDataKey];\n      if (value instanceof Date) {\n        return shortDateFmt.format(value);\n      }\n      return String(value ?? \"\");\n    },\n    [xDataKey]\n  );\n\n  // For compatibility with ChartContext, provide a Date-based xAccessor\n  const xAccessorDate = useCallback(\n    (d: Record<string, unknown>): Date => {\n      const value = d[xDataKey];\n      if (value instanceof Date) {\n        return value;\n      }\n      return new Date();\n    },\n    [xDataKey]\n  );\n\n  // Category scale (band) - for the categorical axis\n  const categoryScale = useMemo(() => {\n    const domain = data.map((d) => categoryAccessor(d));\n    const range: [number, number] = isHorizontal\n      ? [0, innerHeight]\n      : [0, innerWidth];\n    return scaleBand<string>({\n      range,\n      domain,\n      padding: barGap,\n    });\n  }, [innerWidth, innerHeight, data, categoryAccessor, barGap, isHorizontal]);\n\n  // Band width for bars - use prop if provided, otherwise use scale's bandwidth\n  const bandWidth = barWidthProp ?? categoryScale.bandwidth();\n\n  // Compute max value considering stacking\n  const maxValue = useMemo(() => {\n    if (stacked) {\n      // For stacked bars, sum all values at each data point\n      let max = 0;\n      for (const d of data) {\n        let sum = 0;\n        for (const line of lines) {\n          const value = d[line.dataKey];\n          if (typeof value === \"number\") {\n            sum += value;\n          }\n        }\n        if (sum > max) {\n          max = sum;\n        }\n      }\n      return max || 100;\n    }\n    // For grouped bars, find max single value\n    let max = 0;\n    for (const line of lines) {\n      for (const d of data) {\n        const value = d[line.dataKey];\n        if (typeof value === \"number\" && value > max) {\n          max = value;\n        }\n      }\n    }\n    return max || 100;\n  }, [data, lines, stacked]);\n\n  // Value scale (linear) - for the value axis\n  const valueScale = useMemo(() => {\n    const range = isHorizontal ? [0, innerWidth] : [innerHeight, 0];\n    return scaleLinear({\n      range,\n      domain: [0, maxValue * 1.1],\n      nice: true,\n    });\n  }, [innerWidth, innerHeight, maxValue, isHorizontal]);\n\n  const yScales = useMemo(() => {\n    if (isHorizontal) {\n      return wrapSingleYScale(valueScale);\n    }\n    return buildYScalesForLines({\n      lines,\n      data,\n      innerHeight,\n      resolveDomain: (dataKeys) => {\n        let max = 0;\n        for (const d of data) {\n          for (const key of dataKeys) {\n            const value = d[key];\n            if (typeof value === \"number\" && value > max) {\n              max = value;\n            }\n          }\n        }\n        return [0, (max || 100) * 1.1];\n      },\n    });\n  }, [data, innerHeight, isHorizontal, lines, valueScale]);\n\n  const primaryYScale = getPrimaryYScale(yScales, valueScale);\n\n  // Compute stack offsets for stacked bars\n  const stackOffsets = useMemo(() => {\n    if (!stacked) {\n      return undefined;\n    }\n    const offsets = new Map<number, Map<string, number>>();\n    for (let i = 0; i < data.length; i++) {\n      const d = data[i];\n      if (!d) {\n        continue;\n      }\n      const pointOffsets = new Map<string, number>();\n      let cumulative = 0;\n      for (const line of lines) {\n        pointOffsets.set(line.dataKey, cumulative);\n        const value = d[line.dataKey];\n        if (typeof value === \"number\") {\n          cumulative += value;\n        }\n      }\n      offsets.set(i, pointOffsets);\n    }\n    return offsets;\n  }, [data, lines, stacked]);\n\n  // Column width for tooltip indicator\n  const columnWidth = useMemo(() => {\n    if (data.length < 1) {\n      return 0;\n    }\n    return isHorizontal ? innerHeight / data.length : innerWidth / data.length;\n  }, [innerWidth, innerHeight, data.length, isHorizontal]);\n\n  // Pre-compute labels for ticker animation\n  const dateLabels = useMemo(\n    () => data.map((d) => categoryAccessor(d)),\n    [data, categoryAccessor]\n  );\n\n  // Create a fake time scale for compatibility with ChartContext\n  const fakeTimeScale = useMemo(() => {\n    const now = Date.now();\n    const start = now - data.length * 24 * 60 * 60 * 1000;\n    const scale = {\n      ...categoryScale,\n      domain: () => [new Date(start), new Date(now)],\n      range: () => [0, innerWidth] as [number, number],\n      invert: (x: number) => new Date(start + (x / innerWidth) * (now - start)),\n      copy: () => scale,\n    };\n    return scale;\n  }, [categoryScale, innerWidth, data.length]);\n\n  // Animation timing — replay when motion settings change\n  // biome-ignore lint/correctness/useExhaustiveDependencies: revealSignature\n  useEffect(() => {\n    setRevealEpoch((n) => n + 1);\n    setIsLoaded(false);\n    // While loading, hold the skeleton (no reveal, no interaction). When\n    // status flips to \"ready\" this effect re-runs and plays the grow reveal.\n    if (status === \"loading\") {\n      return;\n    }\n    const staggerMs = data.length > 1 ? animationDuration * 0.4 : 0;\n    const timer = setTimeout(() => {\n      setIsLoaded(true);\n    }, animationDuration + staggerMs);\n    return () => clearTimeout(timer);\n  }, [animationDuration, revealSignature, status]);\n\n  useEffect(() => {\n    onPhaseChange?.(isLoaded ? \"ready\" : \"revealing\");\n  }, [isLoaded, onPhaseChange]);\n\n  // Mouse move handler\n  const handleMouseMove = useCallback(\n    (event: React.MouseEvent<SVGGElement>) => {\n      const point = localPoint(event);\n      if (!point) {\n        return;\n      }\n\n      const pos = isHorizontal ? point.y - margin.top : point.x - margin.left;\n\n      // Find which band the mouse is over\n      const bandIndex = Math.floor(pos / columnWidth);\n      const clampedIndex = Math.max(0, Math.min(data.length - 1, bandIndex));\n      const d = data[clampedIndex];\n\n      if (!d) {\n        return;\n      }\n\n      // Calculate positions for each bar\n      const yPositions: Record<string, number> = {};\n      const xPositions: Record<string, number> = {};\n      const barPos = categoryScale(categoryAccessor(d)) ?? 0;\n\n      if (isHorizontal) {\n        // Horizontal bars: dots at end of bar (x = value), centered vertically in band\n        const seriesCount = lines.length;\n        const groupGap = seriesCount > 1 ? 4 : 0;\n        const individualBarHeight =\n          seriesCount > 0\n            ? (bandWidth - groupGap * (seriesCount - 1)) / seriesCount\n            : bandWidth;\n\n        if (stacked) {\n          // Stacked horizontal: all bars same y, x at cumulative end\n          let cumulative = 0;\n          for (const line of lines) {\n            const value = d[line.dataKey];\n            if (typeof value === \"number\") {\n              cumulative += value;\n              const axisScale =\n                yScales[normalizeYAxisId(line.yAxisId)] ?? valueScale;\n              xPositions[line.dataKey] = axisScale(cumulative) ?? 0;\n              yPositions[line.dataKey] = barPos + bandWidth / 2;\n            }\n          }\n        } else {\n          // Grouped horizontal: each bar at its own y position\n          lines.forEach((line, idx) => {\n            const value = d[line.dataKey];\n            if (typeof value === \"number\") {\n              const axisScale =\n                yScales[normalizeYAxisId(line.yAxisId)] ?? valueScale;\n              xPositions[line.dataKey] = axisScale(value) ?? 0;\n              yPositions[line.dataKey] =\n                barPos +\n                idx * (individualBarHeight + groupGap) +\n                individualBarHeight / 2;\n            }\n          });\n        }\n      } else if (stacked) {\n        // Vertical stacked bars\n        let cumulative = 0;\n        let seriesIdx = 0;\n        for (const line of lines) {\n          const value = d[line.dataKey];\n          if (typeof value === \"number\") {\n            cumulative += value;\n            const axisScale =\n              yScales[normalizeYAxisId(line.yAxisId)] ?? primaryYScale;\n            const gapOffset = seriesIdx * stackGap;\n            yPositions[line.dataKey] = (axisScale(cumulative) ?? 0) - gapOffset;\n            seriesIdx++;\n          }\n        }\n      } else {\n        // Vertical grouped bars\n        const seriesCount = lines.length;\n        const groupGap = seriesCount > 1 ? 4 : 0;\n        const individualBarWidth =\n          seriesCount > 0\n            ? (bandWidth - groupGap * (seriesCount - 1)) / seriesCount\n            : bandWidth;\n\n        lines.forEach((line, idx) => {\n          const value = d[line.dataKey];\n          if (typeof value === \"number\") {\n            const axisScale =\n              yScales[normalizeYAxisId(line.yAxisId)] ?? primaryYScale;\n            const baselineY = axisScale(0) ?? innerHeight;\n            const valueY = axisScale(value) ?? 0;\n            const barLengthPx = baselineY - valueY;\n\n            if (squareSnap && !isHorizontal && value > 0) {\n              yPositions[line.dataKey] = topSquareCenterY({\n                baselineY,\n                barLengthPx,\n                squareSize: individualBarWidth,\n                gap: squareSnap.squareGap,\n                fit: squareSnap.fit,\n              });\n            } else {\n              yPositions[line.dataKey] = valueY;\n            }\n\n            xPositions[line.dataKey] =\n              barPos +\n              idx * (individualBarWidth + groupGap) +\n              individualBarWidth / 2;\n          }\n        });\n      }\n\n      // Tooltip position: for horizontal, position at max bar end; for vertical, center of band\n      let tooltipX: number;\n      if (isHorizontal) {\n        // Position tooltip at the end of the longest bar\n        const maxX = Math.max(...Object.values(xPositions), 0);\n        tooltipX = maxX;\n      } else {\n        tooltipX = barPos + bandWidth / 2;\n      }\n\n      scheduleTooltip({\n        point: d,\n        index: clampedIndex,\n        x: tooltipX,\n        yPositions,\n        xPositions: Object.keys(xPositions).length > 0 ? xPositions : undefined,\n      });\n    },\n    [\n      categoryScale,\n      valueScale,\n      data,\n      lines,\n      margin.left,\n      margin.top,\n      categoryAccessor,\n      columnWidth,\n      bandWidth,\n      isHorizontal,\n      stacked,\n      stackGap,\n      scheduleTooltip,\n      yScales,\n      primaryYScale,\n      squareSnap,\n      innerHeight,\n    ]\n  );\n\n  const handleMouseLeave = useCallback(() => {\n    clearTooltip();\n  }, [clearTooltip]);\n\n  const canInteract = isLoaded;\n\n  // Separate children into defs, pre-overlay, and post-overlay\n  const defsChildren: ReactElement[] = [];\n  const clipExcludedChildren: ReactElement[] = [];\n  const underlayChildren: ReactElement[] = [];\n  const preOverlayChildren: ReactElement[] = [];\n  const postOverlayChildren: ReactElement[] = [];\n\n  forEachChartChild(children, (child) => {\n    const resolvedChild = resolveChartChildElement(child);\n\n    if (isGradientDefComponent(child)) {\n      defsChildren.push(child);\n    } else if (isPatternDefComponent(child)) {\n      preOverlayChildren.push(child);\n    } else if (isPostOverlayComponent(resolvedChild)) {\n      postOverlayChildren.push(resolvedChild);\n    } else if (isClipExcludedComponent(resolvedChild)) {\n      clipExcludedChildren.push(\n        isChartClipPassthrough(child.type) ? resolvedChild : child\n      );\n    } else if (isUnderlayComponent(resolvedChild)) {\n      underlayChildren.push(resolvedChild);\n    } else {\n      preOverlayChildren.push(child);\n    }\n  });\n\n  const referenceAreas = useMemo(\n    () => extractReferenceAreaConfigs(children),\n    [children]\n  );\n\n  const contextValue = {\n    ...DEFAULT_CHART_LIFECYCLE,\n    chartPhase: resolveRestingChartPhase(status),\n    chartStatus: status,\n    data,\n    renderData: data,\n    xScale: fakeTimeScale as unknown as ReturnType<\n      typeof import(\"@visx/scale\").scaleTime<number>\n    >,\n    yScale: isHorizontal ? valueScale : primaryYScale,\n    yScales,\n    width,\n    height,\n    innerWidth,\n    innerHeight,\n    margin,\n    columnWidth,\n    tooltipData,\n    setTooltipData,\n    containerRef,\n    lines,\n    referenceAreas,\n    isLoaded,\n    animationDuration,\n    animationEasing,\n    enterTransition,\n    revealEpoch,\n    xAccessor: xAccessorDate,\n    dateLabels,\n    // Bar-specific properties\n    barScale: categoryScale,\n    bandWidth,\n    hoveredBarIndex,\n    barXAccessor: categoryAccessor,\n    orientation,\n    stacked,\n    stackOffsets,\n    squareSnap,\n  };\n\n  return (\n    <ChartProvider value={contextValue}>\n      <svg\n        aria-hidden=\"true\"\n        className=\"overflow-visible\"\n        height={height}\n        width={width}\n      >\n        {/* Gradient and pattern definitions */}\n        {defsChildren.length > 0 && <defs>{defsChildren}</defs>}\n\n        <rect fill=\"transparent\" height={height} width={width} x={0} y={0} />\n\n        {/* biome-ignore lint/a11y/noStaticElementInteractions: Chart interaction area */}\n        <g\n          onMouseLeave={canInteract ? handleMouseLeave : undefined}\n          onMouseMove={canInteract ? handleMouseMove : undefined}\n          style={{ cursor: canInteract ? \"crosshair\" : \"default\" }}\n          transform={`translate(${margin.left},${margin.top})`}\n        >\n          {/* Background rect for mouse event detection */}\n          <rect\n            fill=\"transparent\"\n            height={innerHeight}\n            width={innerWidth}\n            x={0}\n            y={0}\n          />\n\n          {renderKeyedChartLayers(clipExcludedChildren)}\n          {renderKeyedChartLayers(underlayChildren)}\n          {status === \"loading\" ? (\n            <BarLoadingSkeleton\n              barCount={data.length || FALLBACK_LOADING_BARS}\n              innerHeight={innerHeight}\n              innerWidth={innerWidth}\n            />\n          ) : (\n            renderKeyedChartLayers(preOverlayChildren)\n          )}\n\n          {/* Markers rendered last so they're on top for interaction */}\n          {renderKeyedChartLayers(postOverlayChildren)}\n        </g>\n      </svg>\n    </ChartProvider>\n  );\n});\n\nexport function BarChart({\n  data,\n  xDataKey = \"name\",\n  margin: marginProp,\n  animationDuration = 1100,\n  animationEasing = DEFAULT_ANIMATION_EASING,\n  enterTransition,\n  revealSignature,\n  aspectRatio = \"2 / 1\",\n  className = \"\",\n  barGap = 0.2,\n  barWidth,\n  orientation = \"vertical\",\n  stacked = false,\n  stackGap = 0,\n  squareSnap,\n  children,\n  onPhaseChange,\n  status = \"ready\",\n}: BarChartProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const margin = { ...DEFAULT_MARGIN, ...marginProp };\n\n  return (\n    <div\n      className={cn(\"relative w-full overflow-visible\", className)}\n      ref={containerRef}\n      style={{ aspectRatio }}\n    >\n      <ParentSize debounceTime={10}>\n        {({ width, height }) => (\n          <ChartInner\n            animationDuration={animationDuration}\n            animationEasing={animationEasing}\n            barGap={barGap}\n            barWidthProp={barWidth}\n            containerRef={containerRef}\n            data={data}\n            enterTransition={enterTransition}\n            height={height}\n            margin={margin}\n            onPhaseChange={onPhaseChange}\n            orientation={orientation}\n            revealSignature={revealSignature}\n            squareSnap={squareSnap}\n            stacked={stacked}\n            stackGap={stackGap}\n            status={status}\n            width={width}\n            xDataKey={xDataKey}\n          >\n            {children}\n          </ChartInner>\n        )}\n      </ParentSize>\n    </div>\n  );\n}\n\nBarChart.displayName = \"BarChart\";\n\nexport default BarChart;\n",
      "type": "registry:component",
      "target": "components/charts/bar-chart.tsx"
    },
    {
      "path": "src/charts/chart-child-passthrough.ts",
      "content": "import {\n  Children,\n  cloneElement,\n  Fragment,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\n\n/** Marker on wrapper components whose single child should inherit clip classification. */\nexport const CHART_CLIP_PASSTHROUGH = \"__chartClipPassthrough\" as const;\n\nexport function isChartClipPassthrough(type: unknown): boolean {\n  return (\n    typeof type === \"function\" &&\n    (type as { [CHART_CLIP_PASSTHROUGH]?: boolean })[CHART_CLIP_PASSTHROUGH] ===\n      true\n  );\n}\n\n/** Unwrap visibility wrappers so `Grid` / axes stay outside the series clip. */\nexport function resolveChartChildElement(child: ReactElement): ReactElement {\n  if (isChartClipPassthrough(child.type)) {\n    const inner = (child.props as { children?: unknown }).children;\n    if (isValidElement(inner)) {\n      return resolveChartChildElement(inner);\n    }\n  }\n  return child;\n}\n\n/** Walk chart children, flattening React fragments (studio often groups layers in `<>...</>`). */\nexport function forEachChartChild(\n  children: ReactNode,\n  callback: (child: ReactElement, index: number) => void\n) {\n  let index = 0;\n  const visit = (nodes: ReactNode) => {\n    Children.forEach(nodes, (child) => {\n      if (!isValidElement(child)) {\n        return;\n      }\n      if (child.type === Fragment) {\n        visit((child.props as { children?: ReactNode }).children);\n        return;\n      }\n      callback(child, index);\n      index += 1;\n    });\n  };\n  visit(children);\n}\n\nconst CLIP_EXCLUDED_COMPONENT_NAMES = new Set([\n  \"Background\",\n  \"Grid\",\n  \"XAxis\",\n  \"YAxis\",\n  \"BarXAxis\",\n  \"BarYAxis\",\n  \"LiveXAxis\",\n  \"LiveYAxis\",\n]);\n\nconst UNDERLAY_COMPONENT_NAMES = new Set([\"ReferenceArea\", \"BarColumnTrack\"]);\n\n/** Markers render after the interaction overlay so they stay clickable. */\nexport function isPostOverlayComponent(child: ReactElement): boolean {\n  const childType = child.type as {\n    displayName?: string;\n    name?: string;\n    __isChartMarkers?: boolean;\n    __isPostOverlay?: boolean;\n  };\n\n  if (childType.__isChartMarkers || childType.__isPostOverlay) {\n    return true;\n  }\n\n  const componentName =\n    typeof child.type === \"function\"\n      ? childType.displayName || childType.name || \"\"\n      : \"\";\n\n  return (\n    componentName === \"ChartMarkers\" ||\n    componentName === \"MarkerGroup\" ||\n    componentName === \"ChartBrush\"\n  );\n}\n\n/** Renders above grid/axes but below series; excluded from grow-clip reveal. */\nexport function isUnderlayComponent(child: ReactElement): boolean {\n  const childType = child.type as { displayName?: string; name?: string };\n  const componentName =\n    typeof child.type === \"function\"\n      ? childType.displayName || childType.name || \"\"\n      : \"\";\n  return UNDERLAY_COMPONENT_NAMES.has(componentName);\n}\n\n/** Grid and axes stay visible during series clip reveal (e.g. loading → ready). */\nexport function isClipExcludedComponent(child: ReactElement): boolean {\n  const childType = child.type as { displayName?: string; name?: string };\n  const componentName =\n    typeof child.type === \"function\"\n      ? childType.displayName || childType.name || \"\"\n      : \"\";\n  return CLIP_EXCLUDED_COMPONENT_NAMES.has(componentName);\n}\n\n/** SVG layer lists from chart shells need stable keys when rendered as arrays. */\nexport function renderKeyedChartLayers(children: ReactElement[]) {\n  return children.map((child, index) =>\n    cloneElement(child, { key: child.key ?? `chart-layer-${index}` })\n  );\n}\n",
      "type": "registry:lib",
      "target": "components/charts/chart-child-passthrough.ts"
    },
    {
      "path": "src/charts/chart-legend-hover.tsx",
      "content": "\"use client\";\n\nimport { createContext, type ReactNode, useContext, useMemo } from \"react\";\n\ninterface ChartLegendHoverContextValue {\n  hoveredIndex: number | null;\n  setHoveredIndex: (index: number | null) => void;\n}\n\nconst ChartLegendHoverContext =\n  createContext<ChartLegendHoverContextValue | null>(null);\n\nexport function ChartLegendHoverProvider({\n  hoveredIndex,\n  onHoverChange,\n  children,\n}: {\n  hoveredIndex: number | null;\n  onHoverChange: (index: number | null) => void;\n  children: ReactNode;\n}) {\n  const value = useMemo(\n    () => ({ hoveredIndex, setHoveredIndex: onHoverChange }),\n    [hoveredIndex, onHoverChange]\n  );\n\n  return (\n    <ChartLegendHoverContext.Provider value={value}>\n      {children}\n    </ChartLegendHoverContext.Provider>\n  );\n}\n\nexport function useChartLegendHover(): ChartLegendHoverContextValue {\n  const context = useContext(ChartLegendHoverContext);\n  return (\n    context ?? {\n      hoveredIndex: null,\n      setHoveredIndex: () => {\n        /* noop outside ChartLegendHoverProvider */\n      },\n    }\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/chart-legend-hover.tsx"
    },
    {
      "path": "src/charts/bar.tsx",
      "content": "\"use client\";\n\nimport type { scaleBand } from \"@visx/scale\";\nimport type { Transition } from \"motion/react\";\nimport { motion } from \"motion/react\";\nimport { memo, useId, useMemo } from \"react\";\nimport { barDepthAndRise, barDepthMaxDepth } from \"./bar-depth-geometry\";\nimport {\n  chartCssVars,\n  useChart,\n  useChartStable,\n  useYScale,\n} from \"./chart-context\";\nimport { useChartLegendHover } from \"./chart-legend-hover\";\nimport { transitionWithDelay } from \"./motion-utils\";\n\ntype ScaleBand<Domain extends { toString(): string }> = ReturnType<\n  typeof scaleBand<Domain>\n>;\n\nexport type BarLineCap = \"round\" | \"butt\" | number;\nexport type BarAnimationType = \"grow\" | \"fade\";\n\n// ── Bar-depth perspective trim ───────────────────────────────────────────\n// Uses the SHARED geometry (`bar-depth-geometry.ts`) so a\n// `<Bar perspective>` front face lines up exactly with\n// `<BarDepthBack>`'s lid — the formula lives in one place for both.\n\n/** perspectiveRise for a positive bar whose visual top sits at `topY`.\n * Returns 0 for a dead-center bar or a dense chart (degenerate depth). */\nfunction barDepthPerspectiveRise(\n  barScale: ScaleBand<string>,\n  bandWidth: number,\n  barXAccessor: (d: Record<string, unknown>) => string,\n  innerWidth: number,\n  datum: Record<string, unknown>,\n  topY: number,\n  baselineY: number\n): number {\n  const centerX = innerWidth / 2;\n  if (centerX <= 0) {\n    return 0;\n  }\n  const step =\n    (barScale as unknown as { step?: () => number }).step?.() ?? bandWidth;\n  const maxDepth = barDepthMaxDepth(step, bandWidth);\n  const bandX = barScale(barXAccessor(datum)) ?? 0;\n  const cx = bandX + bandWidth / 2;\n  const absOffset = Math.min(1, Math.abs((cx - centerX) / centerX));\n  const naturalHeight = Math.abs(baselineY - topY);\n  return barDepthAndRise(absOffset, naturalHeight, maxDepth).perspectiveRise;\n}\n\nexport interface BarProps {\n  /** Key in data to use for y values */\n  dataKey: string;\n  /** Y-scale group id for vertical bars (Recharts `yAxisId`). Default: `\"left\"`. */\n  yAxisId?: string | number;\n  /** Fill color for the bar. Can be a color, gradient url, or pattern url. Default: var(--chart-line-primary) */\n  fill?: string;\n  /** Color for tooltip dot. Use when fill is a gradient/pattern. Default: uses fill value */\n  stroke?: string;\n  /** Line cap style for bar ends: \"round\", \"butt\", or a number for custom radius. Default: \"round\" */\n  lineCap?: BarLineCap;\n  /** Whether to animate the bars. Default: true */\n  animate?: boolean;\n  /** Animation type: \"grow\" (height) or \"fade\" (opacity + blur). Default: \"grow\" */\n  animationType?: BarAnimationType;\n  /** Opacity when not hovered (when another bar is hovered). Default: 0.3 */\n  fadedOpacity?: number;\n  /** Stagger delay between bars in seconds. Auto-calculated if not provided. */\n  staggerDelay?: number;\n  /** Gap between stacked bars in pixels. Default: 0 */\n  stackGap?: number;\n  /** Gap between grouped bars in pixels. Default: 4 */\n  groupGap?: number;\n  /** Shrink each positive bar's top by its perspective rise so the front face\n   * lines up with `<BarDepthBack>`'s lid (instead of the lid sitting above the\n   * front face). Pass `true` whenever the chart also renders the bar-depth 3D\n   * surfaces. Default: false */\n  perspective?: boolean;\n  /** Minimum rendered bar height in px (non-stacked, vertical). Floors short or\n   * zero-value bars so they stay visible. Pair with the same value on\n   * `<BarDepthProvider minBarHeight>` when using the 3D surfaces. Default: 0 */\n  minBarHeight?: number;\n}\n\ninterface BarInnerProps extends BarProps {\n  barScale: ScaleBand<string>;\n  bandWidth: number;\n  barXAccessor: (d: Record<string, unknown>) => string;\n}\n\ninterface AnimatedBarProps {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n  fill: string;\n  rx: number;\n  ry: number;\n  index: number;\n  isFaded: boolean;\n  animationType: BarAnimationType;\n  innerHeight: number;\n  fadedOpacity: number;\n  staggerDelay: number;\n  enterTransition?: Transition;\n  revealEpoch: number;\n  isHorizontal: boolean;\n}\n\nfunction AnimatedBar({\n  x,\n  y,\n  width,\n  height,\n  fill,\n  rx,\n  ry,\n  index,\n  isFaded,\n  animationType,\n  innerHeight,\n  fadedOpacity,\n  staggerDelay,\n  enterTransition,\n  revealEpoch,\n  isHorizontal,\n}: AnimatedBarProps) {\n  const enterAnim = transitionWithDelay(enterTransition, index * staggerDelay);\n\n  if (animationType === \"fade\") {\n    return (\n      <motion.rect\n        animate={{\n          opacity: isFaded ? fadedOpacity : 1,\n          filter: \"blur(0px)\",\n        }}\n        fill={fill}\n        height={height}\n        initial={{ opacity: 0, filter: \"blur(2px)\" }}\n        key={`fade-${index}-${revealEpoch}`}\n        rx={rx}\n        ry={ry}\n        transition={enterAnim}\n        width={width}\n        x={x}\n        y={y}\n      />\n    );\n  }\n\n  const initial = isHorizontal\n    ? { width: 0, height, x: 0, y }\n    : { width, height: 0, x, y: innerHeight };\n  const target = isHorizontal\n    ? { width, height, x: 0, y }\n    : { width, height, x, y };\n\n  return (\n    <g\n      opacity={isFaded ? fadedOpacity : 1}\n      style={{ transition: \"opacity 0.15s ease-in-out\" }}\n    >\n      <motion.rect\n        animate={target}\n        fill={fill}\n        initial={initial}\n        key={`grow-${index}-${revealEpoch}`}\n        rx={rx}\n        ry={ry}\n        transition={enterAnim}\n      />\n    </g>\n  );\n}\n\nconst BarInner = memo(function BarInner({\n  dataKey,\n  yAxisId,\n  fill = chartCssVars.linePrimary,\n  lineCap = \"round\",\n  animate = true,\n  animationType = \"grow\",\n  fadedOpacity = 0.3,\n  staggerDelay,\n  stackGap = 0,\n  groupGap = 4,\n  perspective = false,\n  minBarHeight = 0,\n  barScale,\n  bandWidth,\n  barXAccessor,\n}: BarInnerProps) {\n  const {\n    data,\n    yScale: chartYScale,\n    innerHeight,\n    innerWidth,\n    isLoaded,\n    hoveredBarIndex,\n    lines,\n    orientation,\n    stacked,\n    stackOffsets,\n    animationDuration,\n    enterTransition,\n    revealEpoch = 0,\n  } = useChart();\n\n  // Calculate stagger delay automatically if not provided\n  // Total animation duration is ~1200ms, with 40% for stagger spread and 60% for bar animation\n  const totalAnimDuration = animationDuration || 1100;\n  const staggerSpread = totalAnimDuration * 0.4; // 40% of time for stagger spread\n  const calculatedStaggerDelay =\n    staggerDelay ?? (data.length > 1 ? staggerSpread / 1000 / data.length : 0);\n  const uniqueId = useId();\n\n  const isHorizontal = orientation === \"horizontal\";\n\n  // Find the index of this bar series among all bar series\n  const { hoveredIndex: legendHoveredIndex } = useChartLegendHover();\n\n  const seriesIndex = useMemo(() => {\n    const idx = lines.findIndex((l) => l.dataKey === dataKey);\n    return idx >= 0 ? idx : 0;\n  }, [lines, dataKey]);\n\n  const seriesConfig = lines[seriesIndex];\n  const valueScale = useYScale(yAxisId ?? seriesConfig?.yAxisId);\n\n  const isLegendDimmed =\n    legendHoveredIndex !== null && legendHoveredIndex !== seriesIndex;\n\n  const seriesCount = lines.length;\n  const isLastSeries = seriesIndex === seriesCount - 1;\n\n  // Calculate the width for each bar within a group (for non-stacked)\n  const barWidth = useMemo(() => {\n    if (!bandWidth || seriesCount === 0) {\n      return 0;\n    }\n    if (stacked) {\n      // Stacked bars use full band width\n      return bandWidth;\n    }\n    // Leave a gap between grouped bars (controlled by groupGap prop)\n    const effectiveGroupGap = seriesCount > 1 ? groupGap : 0;\n    return (bandWidth - effectiveGroupGap * (seriesCount - 1)) / seriesCount;\n  }, [bandWidth, seriesCount, stacked, groupGap]);\n\n  // Calculate corner radius based on lineCap. Perspective bars force a flat\n  // top (radius 0) so the 3D lid from `<BarDepthBack>` meets the bar with no\n  // gap — rounded corners would leave a wedge, so `perspective` overrides it.\n  const cornerRadius = useMemo(() => {\n    if (perspective) {\n      return 0;\n    }\n    if (typeof lineCap === \"number\") {\n      return lineCap;\n    }\n    if (lineCap === \"round\" && barWidth) {\n      return Math.min(barWidth / 2, 8);\n    }\n    return 0;\n  }, [lineCap, barWidth, perspective]);\n\n  return (\n    <g className={`bar-series-${uniqueId}`}>\n      {data.map((d, i) => {\n        const value = d[dataKey];\n        if (typeof value !== \"number\") {\n          return null;\n        }\n\n        const categoryValue = barXAccessor(d);\n        const bandPos = barScale(categoryValue) ?? 0;\n\n        let x: number;\n        let y: number;\n        let barHeight: number;\n        let barW: number;\n\n        const scale = isHorizontal ? chartYScale : valueScale;\n\n        if (isHorizontal) {\n          // Horizontal bars: category on y-axis, value on x-axis\n          const valuePos = scale(value) ?? 0;\n          barW = valuePos; // Width is the value position (grows from left)\n          barHeight = barWidth;\n\n          if (stacked && stackOffsets) {\n            const offset = stackOffsets.get(i)?.get(dataKey) ?? 0;\n            x = scale(offset) ?? 0;\n            barW = valuePos - x;\n            // Apply stack gap for horizontal: shift right and reduce width\n            const gapOffset = seriesIndex * stackGap;\n            x += gapOffset;\n            if (!isLastSeries && stackGap > 0) {\n              barW = Math.max(0, barW - stackGap);\n            }\n          } else {\n            x = 0;\n            // For grouped bars, offset y position\n            const effectiveGroupGap = seriesCount > 1 ? groupGap : 0;\n            y = bandPos + seriesIndex * (barWidth + effectiveGroupGap);\n          }\n          y = stacked\n            ? bandPos\n            : bandPos +\n              seriesIndex * (barWidth + (seriesCount > 1 ? groupGap : 0));\n        } else {\n          // Vertical bars: category on x-axis, value on y-axis\n          const valuePos = scale(value) ?? 0;\n          barHeight = innerHeight - valuePos;\n          barW = barWidth;\n\n          if (stacked && stackOffsets) {\n            const offset = stackOffsets.get(i)?.get(dataKey) ?? 0;\n            const offsetY = scale(offset) ?? innerHeight;\n            // Apply stack gap: shift up and reduce height\n            const gapOffset = seriesIndex * stackGap;\n            y = offsetY - barHeight - gapOffset;\n            // Reduce height slightly for non-last bars to create visual gap\n            if (!isLastSeries && stackGap > 0) {\n              barHeight = Math.max(0, barHeight - stackGap);\n            }\n          } else {\n            y = valuePos;\n            // For grouped bars, offset x position\n            const effectiveGroupGap = seriesCount > 1 ? groupGap : 0;\n            x = bandPos + seriesIndex * (barWidth + effectiveGroupGap);\n          }\n          x = stacked\n            ? bandPos\n            : bandPos +\n              seriesIndex * (barWidth + (seriesCount > 1 ? groupGap : 0));\n\n          // Minimum visible height — floor short/zero non-stacked bars so a\n          // zero-value data point still reads as a tiny bar instead of\n          // vanishing. Grows up from the baseline. Floored bars skip the\n          // perspective trim (sub-pixel on a 3px bar; keeps the front aligned\n          // with bar-depth, which also skips trim for floored bars).\n          let isFloored = false;\n          if (\n            !stacked &&\n            minBarHeight > 0 &&\n            value >= 0 &&\n            barHeight < minBarHeight\n          ) {\n            const baselineY = scale(0) ?? innerHeight;\n            barHeight = minBarHeight;\n            y = baselineY - minBarHeight;\n            isFloored = true;\n          }\n\n          // Perspective trim — shrink the topmost positive bar's front-face\n          // top down by its perspective rise so it meets `<BarDepthBack>`'s\n          // lid back edge. Stacked: only the last (topmost) series; grouped or\n          // single: every positive bar. Clamped to `barHeight - 1` so very\n          // short bars keep a positive height (matches bar-depth's clamp).\n          if (\n            perspective &&\n            value > 0 &&\n            !isFloored &&\n            (!stacked || isLastSeries)\n          ) {\n            const baselineY = scale(0) ?? innerHeight;\n            const rise = barDepthPerspectiveRise(\n              barScale,\n              bandWidth,\n              barXAccessor,\n              innerWidth,\n              d,\n              y,\n              baselineY\n            );\n            const trim = Math.min(rise, Math.max(0, barHeight - 1));\n            y += trim;\n            barHeight -= trim;\n          }\n        }\n\n        const isFaded =\n          (hoveredBarIndex !== null && hoveredBarIndex !== i) || isLegendDimmed;\n\n        // Use categoryValue as key since it's the unique identifier from data\n        const barKey = `bar-${dataKey}-${categoryValue}`;\n\n        // Apply rounded corners:\n        // - For non-stacked: always apply\n        // - For stacked with gap: apply to all bars\n        // - For stacked without gap: only apply to the last series\n        const applyRounding = !stacked || stackGap > 0 || isLastSeries;\n        const effectiveRx = applyRounding ? cornerRadius : 0;\n        const effectiveRy = applyRounding ? cornerRadius : 0;\n\n        if (animate && !isLoaded) {\n          return (\n            <AnimatedBar\n              animationType={animationType}\n              enterTransition={enterTransition}\n              fadedOpacity={fadedOpacity}\n              fill={fill}\n              height={barHeight}\n              index={i}\n              innerHeight={innerHeight}\n              isFaded={isFaded}\n              isHorizontal={isHorizontal}\n              key={barKey}\n              revealEpoch={revealEpoch}\n              rx={effectiveRx}\n              ry={effectiveRy}\n              staggerDelay={calculatedStaggerDelay}\n              width={barW}\n              x={x}\n              y={y}\n            />\n          );\n        }\n\n        // Static bar after animation completes\n        return (\n          <rect\n            fill={fill}\n            height={barHeight}\n            key={barKey}\n            opacity={isFaded ? fadedOpacity : 1}\n            rx={effectiveRx}\n            ry={effectiveRy}\n            style={{\n              cursor: \"default\",\n              transition: \"opacity 0.15s ease-in-out\",\n            }}\n            width={barW}\n            x={x}\n            y={y}\n          />\n        );\n      })}\n    </g>\n  );\n});\n\nexport function Bar(props: BarProps) {\n  const { barScale, bandWidth, barXAccessor } = useChartStable();\n\n  if (!(barScale && bandWidth && barXAccessor)) {\n    console.warn(\"Bar component must be used within a BarChart\");\n    return null;\n  }\n\n  return (\n    <BarInner\n      {...props}\n      bandWidth={bandWidth}\n      barScale={barScale}\n      barXAccessor={barXAccessor}\n    />\n  );\n}\n\nBar.displayName = \"Bar\";\n\nexport default Bar;\n",
      "type": "registry:component",
      "target": "components/charts/bar.tsx"
    },
    {
      "path": "src/charts/bar-squares.tsx",
      "content": "\"use client\";\n\nimport type { scaleBand } from \"@visx/scale\";\nimport type { Transition } from \"motion/react\";\nimport { motion } from \"motion/react\";\nimport { memo, useId, useMemo } from \"react\";\nimport { computeSquareColumn } from \"./bar-squares-layout\";\nimport {\n  chartCssVars,\n  useChart,\n  useChartStable,\n  useYScale,\n} from \"./chart-context\";\nimport { useChartLegendHover } from \"./chart-legend-hover\";\nimport { transitionWithDelay } from \"./motion-utils\";\nimport { type PatternPresetId, renderPatternPreset } from \"./pattern-preset\";\n\ntype ScaleBand<Domain extends { toString(): string }> = ReturnType<\n  typeof scaleBand<Domain>\n>;\n\nexport interface GradientStop {\n  offset: number;\n  color: string;\n}\n\nexport interface BarSquaresProps {\n  dataKey: string;\n  yAxisId?: string | number;\n  /** Fill color, gradient url, or pattern url. Default: var(--chart-line-primary) */\n  fill?: string;\n  /** Tooltip dot / ring stroke color when fill is gradient/pattern */\n  stroke?: string;\n  /** Gap between stacked squares in pixels. Default: 3 */\n  squareGap?: number;\n  /** Corner radius as a fraction of square size (0 = flat, 0.5 = circle). Default: 0.25 */\n  squareRadius?: number;\n  /** Redistribute gap so columns fit bar height exactly */\n  squareFit?: boolean;\n  /** Apply bar-spanning gradient from gradientStops */\n  useGradient?: boolean;\n  gradientStops?: GradientStop[];\n  /** Pattern preset when fill is a pattern (for gradient tinting) */\n  patternPreset?: PatternPresetId;\n  animate?: boolean;\n  fadedOpacity?: number;\n  staggerDelay?: number;\n  groupGap?: number;\n}\n\nexport interface BarColumnTrackProps {\n  /** Fill color or pattern url. Default: var(--chart-grid) */\n  fill?: string;\n  opacity?: number;\n  squareGap?: number;\n  /** Corner radius fraction (matches squares). Default: 0.25 */\n  squareRadius?: number;\n  groupGap?: number;\n  squareFit?: boolean;\n  staggerDelay?: number;\n}\n\ninterface BarSquaresInnerProps extends BarSquaresProps {\n  barScale: ScaleBand<string>;\n  bandWidth: number;\n  barXAccessor: (d: Record<string, unknown>) => string;\n}\n\ninterface SquareColumnProps {\n  x: number;\n  baselineY: number;\n  barLengthPx: number;\n  squareSize: number;\n  squareGap: number;\n  squareRadius: number;\n  squareFit: boolean;\n  fill: string;\n  useGradient: boolean;\n  gradientStops: GradientStop[];\n  patternPreset?: PatternPresetId;\n  index: number;\n  isFaded: boolean;\n  fadedOpacity: number;\n  animate: boolean;\n  staggerDelay: number;\n  animationDuration: number;\n  enterTransition?: Transition;\n  revealEpoch: number;\n}\n\nfunction isPatternFill(fill: string): boolean {\n  return fill.startsWith(\"url(\");\n}\n\n/** Delay between stacked squares within one column (bottom → top). */\nfunction squareCascadeStepSeconds(\n  enterTransition: Transition | undefined,\n  animationDurationMs: number,\n  squareCount: number\n): number {\n  if (squareCount <= 1) {\n    return 0;\n  }\n  const durationMs =\n    enterTransition?.type === \"tween\" &&\n    typeof enterTransition.duration === \"number\"\n      ? enterTransition.duration * 1000\n      : animationDurationMs;\n  const cascadeSpreadMs = durationMs * 0.4;\n  return cascadeSpreadMs / 1000 / (squareCount - 1);\n}\n\nfunction cascadeColumnTransition(\n  enterTransition: Transition | undefined,\n  animationDurationMs: number,\n  columnIndex: number,\n  columnStaggerDelay: number,\n  squareCount: number\n): Transition {\n  const cascadeStep = squareCascadeStepSeconds(\n    enterTransition,\n    animationDurationMs,\n    squareCount\n  );\n  const base = transitionWithDelay(\n    enterTransition,\n    columnIndex * columnStaggerDelay\n  );\n  if (squareCount <= 1 || base.type !== \"tween\") {\n    return base;\n  }\n  const baseDuration =\n    typeof base.duration === \"number\"\n      ? base.duration\n      : animationDurationMs / 1000;\n  return {\n    ...base,\n    duration: baseDuration + cascadeStep * (squareCount - 1),\n  };\n}\n\nfunction SquareColumn({\n  x,\n  baselineY,\n  barLengthPx,\n  squareSize,\n  squareGap,\n  squareRadius,\n  squareFit,\n  fill,\n  useGradient,\n  gradientStops,\n  patternPreset,\n  index,\n  isFaded,\n  fadedOpacity,\n  animate,\n  staggerDelay,\n  animationDuration,\n  enterTransition,\n  revealEpoch,\n}: SquareColumnProps) {\n  const layout = useMemo(\n    () =>\n      computeSquareColumn({\n        barLengthPx,\n        squareSize,\n        gap: squareGap,\n        fit: squareFit,\n      }),\n    [barLengthPx, squareSize, squareGap, squareFit]\n  );\n\n  const rx = squareSize * squareRadius;\n  const columnTop = baselineY - layout.columnHeight;\n  const gradientId = `bar-squares-gradient-${index}-${revealEpoch}`;\n  const patternFill = isPatternFill(fill);\n  const patternId = `bar-squares-pattern-${index}-${revealEpoch}`;\n\n  const effectiveFill = useMemo(() => {\n    if (useGradient) {\n      if (patternFill && patternPreset && patternPreset !== \"none\") {\n        return `url(#${patternId})`;\n      }\n      return `url(#${gradientId})`;\n    }\n    return fill;\n  }, [useGradient, patternFill, patternPreset, fill, gradientId, patternId]);\n\n  const cascadeStep = squareCascadeStepSeconds(\n    enterTransition,\n    animationDuration,\n    layout.count\n  );\n  const squareOpacity = isFaded ? fadedOpacity : 1;\n\n  const gradientPatternNode =\n    useGradient && patternFill && patternPreset && patternPreset !== \"none\"\n      ? renderPatternPreset(patternPreset, patternId, {\n          color: `url(#${gradientId})`,\n        })\n      : null;\n\n  const gradientDefs = useGradient ? (\n    <defs>\n      <linearGradient\n        gradientUnits=\"userSpaceOnUse\"\n        id={gradientId}\n        x1={0}\n        x2={0}\n        y1={baselineY}\n        y2={columnTop}\n      >\n        {gradientStops.map((stop) => (\n          <stop\n            key={`${stop.offset}-${stop.color}`}\n            offset={`${stop.offset}%`}\n            stopColor={stop.color}\n          />\n        ))}\n      </linearGradient>\n      {gradientPatternNode}\n    </defs>\n  ) : null;\n\n  const squares = layout.positions.map((relY, squareIndex) => {\n    const y = columnTop + relY;\n    const bottomY = y + squareSize;\n    const key = `sq-${index}-${squareIndex}-${revealEpoch}`;\n\n    if (!animate) {\n      return (\n        <rect\n          fill={effectiveFill}\n          height={squareSize}\n          key={key}\n          opacity={squareOpacity}\n          rx={rx}\n          ry={rx}\n          width={squareSize}\n          x={x}\n          y={y}\n        />\n      );\n    }\n\n    return (\n      <motion.rect\n        animate={{ attrY: y, height: squareSize, opacity: squareOpacity }}\n        fill={effectiveFill}\n        height={squareSize}\n        initial={{ attrY: bottomY, height: 0, opacity: 1 }}\n        key={key}\n        rx={rx}\n        ry={rx}\n        transition={{\n          ...transitionWithDelay(\n            enterTransition,\n            index * staggerDelay + squareIndex * cascadeStep\n          ),\n          opacity: { duration: 0.15 },\n        }}\n        width={squareSize}\n        x={x}\n      />\n    );\n  });\n\n  return (\n    <>\n      {gradientDefs}\n      {squares}\n    </>\n  );\n}\n\nconst BarSquaresInner = memo(function BarSquaresInner({\n  dataKey,\n  yAxisId,\n  fill = chartCssVars.linePrimary,\n  squareGap = 3,\n  squareRadius = 0.25,\n  squareFit = false,\n  useGradient = false,\n  gradientStops = [],\n  patternPreset,\n  animate = true,\n  fadedOpacity = 0.3,\n  staggerDelay,\n  groupGap = 4,\n  barScale,\n  bandWidth,\n  barXAccessor,\n}: BarSquaresInnerProps) {\n  const {\n    data,\n    innerHeight,\n    hoveredBarIndex,\n    lines,\n    orientation,\n    stacked,\n    animationDuration,\n    enterTransition,\n    revealEpoch = 0,\n  } = useChart();\n\n  const { hoveredIndex: legendHoveredIndex } = useChartLegendHover();\n  const uniqueId = useId();\n\n  const isHorizontal = orientation === \"horizontal\";\n  const isUnsupported = isHorizontal || stacked;\n\n  const seriesIndex = useMemo(() => {\n    const idx = lines.findIndex((l) => l.dataKey === dataKey);\n    return idx >= 0 ? idx : 0;\n  }, [lines, dataKey]);\n\n  const seriesConfig = lines[seriesIndex];\n  const valueScale = useYScale(yAxisId ?? seriesConfig?.yAxisId);\n\n  const isLegendDimmed =\n    legendHoveredIndex !== null && legendHoveredIndex !== seriesIndex;\n\n  const seriesCount = lines.length;\n  const squareSize = useMemo(() => {\n    if (!bandWidth || seriesCount === 0) {\n      return 0;\n    }\n    const effectiveGroupGap = seriesCount > 1 ? groupGap : 0;\n    return (bandWidth - effectiveGroupGap * (seriesCount - 1)) / seriesCount;\n  }, [bandWidth, seriesCount, groupGap]);\n\n  const totalAnimDuration = animationDuration || 1100;\n  const staggerSpread = totalAnimDuration * 0.4;\n  const calculatedStaggerDelay =\n    staggerDelay ?? (data.length > 1 ? staggerSpread / 1000 / data.length : 0);\n\n  const baselineY = valueScale(0) ?? innerHeight;\n  const stops =\n    gradientStops.length >= 2\n      ? gradientStops\n      : [\n          { offset: 0, color: fill },\n          { offset: 100, color: fill },\n        ];\n\n  if (isUnsupported) {\n    return null;\n  }\n\n  return (\n    <g className={`bar-squares-${uniqueId}`}>\n      {data.map((d, i) => {\n        const value = d[dataKey];\n        if (typeof value !== \"number\" || value <= 0) {\n          return null;\n        }\n\n        const categoryValue = barXAccessor(d);\n        const bandPos = barScale(categoryValue) ?? 0;\n        const effectiveGroupGap = seriesCount > 1 ? groupGap : 0;\n        const x = bandPos + seriesIndex * (squareSize + effectiveGroupGap);\n\n        const valuePos = valueScale(value) ?? 0;\n        const barLengthPx = baselineY - valuePos;\n\n        const isFaded =\n          (hoveredBarIndex !== null && hoveredBarIndex !== i) || isLegendDimmed;\n\n        return (\n          <SquareColumn\n            animate={animate}\n            animationDuration={animationDuration || 1100}\n            barLengthPx={barLengthPx}\n            baselineY={baselineY}\n            enterTransition={enterTransition}\n            fadedOpacity={fadedOpacity}\n            fill={fill}\n            gradientStops={stops}\n            index={i}\n            isFaded={isFaded}\n            key={`bar-squares-${dataKey}-${categoryValue}`}\n            patternPreset={patternPreset}\n            revealEpoch={revealEpoch}\n            squareFit={squareFit}\n            squareGap={squareGap}\n            squareRadius={squareRadius}\n            squareSize={squareSize}\n            staggerDelay={calculatedStaggerDelay}\n            useGradient={useGradient}\n            x={x}\n          />\n        );\n      })}\n    </g>\n  );\n});\n\nexport function BarSquares(props: BarSquaresProps) {\n  const { barScale, bandWidth, barXAccessor } = useChartStable();\n\n  if (!(barScale && bandWidth && barXAccessor)) {\n    console.warn(\"BarSquares must be used within a BarChart\");\n    return null;\n  }\n\n  return (\n    <BarSquaresInner\n      {...props}\n      bandWidth={bandWidth}\n      barScale={barScale}\n      barXAccessor={barXAccessor}\n    />\n  );\n}\n\nBarSquares.displayName = \"BarSquares\";\n\nconst BarColumnTrackInner = memo(function BarColumnTrackInner({\n  fill = chartCssVars.grid,\n  opacity = 0.3,\n  squareGap = 3,\n  squareRadius = 0.25,\n  squareFit = false,\n  groupGap = 4,\n  staggerDelay,\n  barScale,\n  bandWidth,\n  barXAccessor,\n}: BarColumnTrackProps & {\n  barScale: ScaleBand<string>;\n  bandWidth: number;\n  barXAccessor: (d: Record<string, unknown>) => string;\n}) {\n  const {\n    data,\n    lines,\n    orientation,\n    stacked,\n    hoveredBarIndex,\n    animationDuration,\n    enterTransition,\n    revealEpoch = 0,\n  } = useChart();\n  const uniqueId = useId();\n\n  const isHorizontal = orientation === \"horizontal\";\n  const isUnsupported = isHorizontal || stacked;\n  const seriesCount = lines.length;\n\n  const squareSize = useMemo(() => {\n    if (!bandWidth || seriesCount === 0) {\n      return 0;\n    }\n    const effectiveGroupGap = seriesCount > 1 ? groupGap : 0;\n    return (bandWidth - effectiveGroupGap * (seriesCount - 1)) / seriesCount;\n  }, [bandWidth, seriesCount, groupGap]);\n\n  const totalAnimDuration = animationDuration || 1100;\n  const staggerSpread = totalAnimDuration * 0.4;\n  const calculatedStaggerDelay =\n    staggerDelay ?? (data.length > 1 ? staggerSpread / 1000 / data.length : 0);\n\n  if (isUnsupported) {\n    return null;\n  }\n\n  const rx = squareSize * squareRadius;\n  const effectiveOpacity = hoveredBarIndex === null ? opacity : 0;\n\n  return (\n    <g\n      className={`bar-column-track-${uniqueId}`}\n      style={{ transition: \"opacity 0.15s ease-in-out\" }}\n    >\n      {data.map((d, i) => {\n        const categoryValue = barXAccessor(d);\n        const bandPos = barScale(categoryValue) ?? 0;\n        const effectiveGroupGap = seriesCount > 1 ? groupGap : 0;\n\n        return lines.map((line, seriesIndex) => (\n          <TrackColumn\n            animate\n            bandPos={bandPos}\n            d={d}\n            dataKey={line.dataKey}\n            effectiveGroupGap={effectiveGroupGap}\n            effectiveOpacity={effectiveOpacity}\n            enterTransition={enterTransition}\n            fill={fill}\n            index={i}\n            key={`track-${i}-${line.dataKey}`}\n            revealEpoch={revealEpoch}\n            rx={rx}\n            seriesIndex={seriesIndex}\n            squareFit={squareFit}\n            squareGap={squareGap}\n            squareSize={squareSize}\n            staggerDelay={calculatedStaggerDelay}\n            yAxisId={line.yAxisId}\n          />\n        ));\n      })}\n    </g>\n  );\n});\n\nfunction TrackColumn({\n  d,\n  dataKey,\n  yAxisId,\n  bandPos,\n  seriesIndex,\n  effectiveGroupGap,\n  squareSize,\n  squareGap,\n  squareFit,\n  fill,\n  rx,\n  effectiveOpacity,\n  index,\n  staggerDelay,\n  animate,\n  enterTransition,\n  revealEpoch,\n}: {\n  d: Record<string, unknown>;\n  dataKey: string;\n  yAxisId?: string | number;\n  bandPos: number;\n  seriesIndex: number;\n  effectiveGroupGap: number;\n  squareSize: number;\n  squareGap: number;\n  squareFit: boolean;\n  fill: string;\n  rx: number;\n  effectiveOpacity: number;\n  index: number;\n  staggerDelay: number;\n  animate: boolean;\n  enterTransition?: Transition;\n  revealEpoch: number;\n}) {\n  const { innerHeight, animationDuration: chartAnimationDuration } = useChart();\n  const valueScale = useYScale(yAxisId);\n  const value = d[dataKey];\n\n  if (typeof value !== \"number\" || value <= 0) {\n    return null;\n  }\n\n  const baselineY = valueScale(0) ?? innerHeight;\n  const valuePos = valueScale(value) ?? 0;\n  const barLengthPx = baselineY - valuePos;\n  const layout = computeSquareColumn({\n    barLengthPx,\n    squareSize,\n    gap: squareGap,\n    fit: squareFit,\n  });\n  const columnTop = baselineY - layout.columnHeight;\n  const trackHeight = Math.max(0, columnTop);\n\n  if (trackHeight <= 0 && !animate) {\n    return null;\n  }\n\n  const x = bandPos + seriesIndex * (squareSize + effectiveGroupGap);\n  const enterAnim = cascadeColumnTransition(\n    enterTransition,\n    chartAnimationDuration || 1100,\n    index,\n    staggerDelay,\n    layout.count\n  );\n  const animatedHeight = trackHeight > 0 ? trackHeight : 0;\n\n  if (animate) {\n    return (\n      <motion.rect\n        animate={{ height: animatedHeight, y: 0 }}\n        fill={fill}\n        height={animatedHeight}\n        initial={{ height: baselineY, y: 0 }}\n        key={`track-${index}-${seriesIndex}-${revealEpoch}`}\n        opacity={effectiveOpacity}\n        rx={rx}\n        ry={rx}\n        transition={enterAnim}\n        width={squareSize}\n        x={x}\n      />\n    );\n  }\n\n  if (trackHeight <= 0) {\n    return null;\n  }\n\n  return (\n    <rect\n      fill={fill}\n      height={trackHeight}\n      opacity={effectiveOpacity}\n      rx={rx}\n      ry={rx}\n      width={squareSize}\n      x={x}\n      y={0}\n    />\n  );\n}\n\nexport function BarColumnTrack(props: BarColumnTrackProps) {\n  const { barScale, bandWidth, barXAccessor } = useChartStable();\n\n  if (!(barScale && bandWidth && barXAccessor)) {\n    console.warn(\"BarColumnTrack must be used within a BarChart\");\n    return null;\n  }\n\n  return (\n    <BarColumnTrackInner\n      {...props}\n      bandWidth={bandWidth}\n      barScale={barScale}\n      barXAccessor={barXAccessor}\n    />\n  );\n}\n\nBarColumnTrack.displayName = \"BarColumnTrack\";\n\nexport default BarSquares;\n",
      "type": "registry:component",
      "target": "components/charts/bar-squares.tsx"
    },
    {
      "path": "src/charts/bar-squares-layout.ts",
      "content": "export interface SquareColumnLayout {\n  /** Number of squares in the column */\n  count: number;\n  /** Top-left Y of each square, bottom square first (relative to bar top at 0) */\n  positions: number[];\n  /** Quantized column height in pixels */\n  columnHeight: number;\n  squareSize: number;\n  /** Effective gap between squares (may differ when fit mode redistributes) */\n  gap: number;\n}\n\nexport interface SquareColumnInput {\n  /** Raw bar length in pixels (baseline − value) */\n  barLengthPx: number;\n  /** Square width/height — typically equals bar width */\n  squareSize: number;\n  /** Gap between stacked squares in pixels */\n  gap: number;\n  /** When true, redistribute gap so column height matches barLengthPx exactly */\n  fit?: boolean;\n}\n\n/** Quantize bar length into a stack of square cells. */\nexport function computeSquareColumn({\n  barLengthPx,\n  squareSize,\n  gap,\n  fit = false,\n}: SquareColumnInput): SquareColumnLayout {\n  if (barLengthPx <= 0 || squareSize <= 0) {\n    return { count: 0, positions: [], columnHeight: 0, squareSize, gap };\n  }\n\n  if (fit) {\n    const count = Math.max(\n      1,\n      Math.floor((barLengthPx + gap) / (squareSize + gap))\n    );\n    const effectiveGap =\n      count > 1\n        ? Math.max(0, (barLengthPx - count * squareSize) / (count - 1))\n        : 0;\n    const step = squareSize + effectiveGap;\n    const columnHeight = barLengthPx;\n    const positions: number[] = [];\n\n    for (let i = 0; i < count; i++) {\n      positions.push(columnHeight - squareSize - i * step);\n    }\n\n    return {\n      count,\n      positions,\n      columnHeight,\n      squareSize,\n      gap: effectiveGap,\n    };\n  }\n\n  const step = squareSize + gap;\n  const count = Math.max(1, Math.round(barLengthPx / step));\n  const columnHeight = count * squareSize + Math.max(0, count - 1) * gap;\n\n  const positions: number[] = [];\n  for (let i = 0; i < count; i++) {\n    const offsetFromBottom = i * step;\n    positions.push(columnHeight - squareSize - offsetFromBottom);\n  }\n\n  return { count, positions, columnHeight, squareSize, gap };\n}\n\n/** Y center of the topmost square in a vertical column. */\nexport function topSquareCenterY({\n  baselineY,\n  barLengthPx,\n  squareSize,\n  gap,\n  fit = false,\n}: SquareColumnInput & { baselineY: number }): number {\n  const {\n    count,\n    squareSize: size,\n    columnHeight,\n  } = computeSquareColumn({\n    barLengthPx,\n    squareSize,\n    gap,\n    fit,\n  });\n\n  if (count === 0) {\n    return baselineY;\n  }\n\n  const topY = baselineY - columnHeight;\n  return topY + size / 2;\n}\n",
      "type": "registry:lib",
      "target": "components/charts/bar-squares-layout.ts"
    },
    {
      "path": "src/charts/pattern-preset.tsx",
      "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { PatternCircles, PatternLines } from \"./visx-pattern\";\n\nexport const PATTERN_PRESET_IDS = [\n  \"none\",\n  \"diagonal\",\n  \"horizontal\",\n  \"vertical\",\n  \"cross\",\n  \"dots\",\n  \"circles\",\n  \"accent\",\n] as const;\n\nexport type PatternPresetId = (typeof PATTERN_PRESET_IDS)[number];\n\nexport interface PatternPresetOptions {\n  color?: string;\n  scale?: number;\n  strokeWidth?: number;\n  radius?: number;\n  complement?: boolean;\n  fill?: string;\n  /** Dot grid only — when false, render hollow dots (stroke only). Default: true */\n  dotFill?: boolean;\n  tileBackground?: string;\n}\n\n/** Presets rendered with @visx/pattern `PatternCircles`. */\nexport function isCirclePattern(preset: PatternPresetId): boolean {\n  return preset === \"circles\" || preset === \"dots\";\n}\n\n/** @deprecated Use `isCirclePattern`. */\nexport function isCirclesPattern(preset: PatternPresetId): boolean {\n  return isCirclePattern(preset);\n}\n\nexport function patternPresetTileSize(\n  preset: PatternPresetId,\n  scale = 1\n): { width: number; height: number; strokeWidth: number } {\n  let base = { width: 6, height: 6, strokeWidth: 1 };\n  if (preset === \"dots\") {\n    base = { width: 10, height: 10, strokeWidth: 0 };\n  } else if (preset === \"cross\") {\n    base = { width: 8, height: 8, strokeWidth: 1 };\n  } else if (preset === \"circles\") {\n    base = { width: 6, height: 6, strokeWidth: 1 };\n  }\n\n  return {\n    width: base.width * scale,\n    height: base.height * scale,\n    strokeWidth: base.strokeWidth * scale,\n  };\n}\n\nfunction renderPatternCircles(\n  preset: \"dots\" | \"circles\",\n  _id: string,\n  color: string,\n  common: {\n    id: string;\n    height: number;\n    width: number;\n    strokeWidth: number;\n    background?: string;\n  },\n  options: PatternPresetOptions,\n  scale: number\n) {\n  const isDotGrid = preset === \"dots\";\n  const radius =\n    options.radius ?? (isDotGrid ? Math.max(0.5, 1.5 * scale) : 2 * scale);\n  const dotFillEnabled = options.dotFill !== false;\n\n  if (isDotGrid) {\n    const dotFill = dotFillEnabled ? options.fill || color : undefined;\n    return (\n      <PatternCircles\n        {...common}\n        complement={options.complement}\n        fill={dotFill}\n        radius={radius}\n        stroke={dotFillEnabled && options.fill ? undefined : color}\n        strokeWidth={\n          dotFillEnabled && !options.fill\n            ? (options.strokeWidth ?? 0)\n            : (options.strokeWidth ?? 1)\n        }\n      />\n    );\n  }\n\n  return (\n    <PatternCircles\n      {...common}\n      complement={options.complement}\n      fill={options.fill || undefined}\n      radius={radius}\n      stroke={color}\n      strokeWidth={options.strokeWidth ?? common.strokeWidth}\n    />\n  );\n}\n\n/** Renders a @visx/pattern definition node for the given preset. */\nexport function renderPatternPreset(\n  preset: PatternPresetId,\n  id: string,\n  options: PatternPresetOptions = {}\n): ReactNode {\n  if (preset === \"none\") {\n    return null;\n  }\n\n  const color = options.color ?? \"var(--chart-1)\";\n  const scale = options.scale ?? 1;\n  const tile = patternPresetTileSize(preset, scale);\n  const common = {\n    id,\n    height: tile.height,\n    width: tile.width,\n    strokeWidth: tile.strokeWidth,\n    ...(options.tileBackground ? { background: options.tileBackground } : {}),\n  };\n\n  if (preset === \"dots\" || preset === \"circles\") {\n    return renderPatternCircles(preset, id, color, common, options, scale);\n  }\n\n  const strokeWidth = options.strokeWidth ?? tile.strokeWidth;\n\n  switch (preset) {\n    case \"diagonal\":\n      return (\n        <PatternLines\n          {...common}\n          orientation={[\"diagonal\"]}\n          stroke={color}\n          strokeWidth={strokeWidth}\n        />\n      );\n    case \"horizontal\":\n      return (\n        <PatternLines\n          {...common}\n          orientation={[\"horizontal\"]}\n          stroke={color}\n          strokeWidth={strokeWidth}\n        />\n      );\n    case \"vertical\":\n      return (\n        <PatternLines\n          {...common}\n          orientation={[\"vertical\"]}\n          stroke={color}\n          strokeWidth={strokeWidth}\n        />\n      );\n    case \"cross\":\n      return (\n        <PatternLines\n          {...common}\n          orientation={[\"diagonal\", \"diagonalRightToLeft\"]}\n          stroke={color}\n          strokeWidth={strokeWidth}\n        />\n      );\n    case \"accent\":\n      return (\n        <PatternLines\n          {...common}\n          orientation={[\"diagonal\"]}\n          stroke=\"#e879f9\"\n          strokeWidth={strokeWidth}\n        />\n      );\n    default:\n      return null;\n  }\n}\n",
      "type": "registry:component",
      "target": "components/charts/pattern-preset.tsx"
    },
    {
      "path": "src/charts/visx-pattern.tsx",
      "content": "\"use client\";\n\nimport {\n  PatternCircles as VisxPatternCircles,\n  PatternHexagons as VisxPatternHexagons,\n  PatternLines as VisxPatternLines,\n  PatternWaves as VisxPatternWaves,\n} from \"@visx/pattern\";\nimport type { ComponentProps } from \"react\";\n\nexport function PatternLines(props: ComponentProps<typeof VisxPatternLines>) {\n  return <VisxPatternLines {...props} />;\n}\nPatternLines.displayName = \"PatternLines\";\n\nexport function PatternCircles(\n  props: ComponentProps<typeof VisxPatternCircles>\n) {\n  return <VisxPatternCircles {...props} />;\n}\nPatternCircles.displayName = \"PatternCircles\";\n\nexport function PatternWaves(props: ComponentProps<typeof VisxPatternWaves>) {\n  return <VisxPatternWaves {...props} />;\n}\nPatternWaves.displayName = \"PatternWaves\";\n\nexport function PatternHexagons(\n  props: ComponentProps<typeof VisxPatternHexagons>\n) {\n  return <VisxPatternHexagons {...props} />;\n}\nPatternHexagons.displayName = \"PatternHexagons\";\n",
      "type": "registry:component",
      "target": "components/charts/visx-pattern.tsx"
    },
    {
      "path": "src/charts/bar-depth-geometry.ts",
      "content": "/**\n * Shared 3D bar-depth geometry — the single source of truth for the\n * perspective math used by BOTH `<Bar perspective>` (which shrinks a\n * bar's front-face top) AND the bar-depth surfaces (`<BarDepthBack>`'s side +\n * lid). Keeping the formula here means the front face and the 3D lid can never\n * drift out of alignment.\n */\n\n/** Hard ceiling on side-face thickness in px. Capped further by the column gap\n * so depth never bleeds past the next bar's leading edge. */\nexport const BAR_DEPTH_MAX_PX = 7;\n/** The side parallelogram's back edge lifts by `depth * this ratio`, giving a\n * subtle head-on perspective slope. */\nexport const BAR_DEPTH_PERSPECTIVE_RATIO = 0.45;\n\n/**\n * Maximum side-face depth in px for a chart, clamped so depth never spills past\n * the gap between bars. `stepWidth` is d3-scaleBand's `step()` (bandwidth +\n * gap); `bandWidth` is a single bar's width. Returns 0 for gapless/dense charts.\n */\nexport function barDepthMaxDepth(stepWidth: number, bandWidth: number): number {\n  const gap = Math.max(0, stepWidth - bandWidth);\n  return Math.min(bandWidth * 0.22, Math.max(0, gap - 1), BAR_DEPTH_MAX_PX);\n}\n\n/**\n * Per-bar side-face depth + perspective rise.\n *\n * - `absOffset` ∈ [0, 1]: the bar's normalized distance from the chart's\n *   horizontal center. 0 = dead center (no depth); 1 = chart edge (full depth).\n * - `naturalHeight`: the bar's pixel height. Depth is capped by it so a short\n *   bar's side never reads wider than the bar is tall.\n * - `maxDepth`: from `barDepthMaxDepth`.\n */\nexport function barDepthAndRise(\n  absOffset: number,\n  naturalHeight: number,\n  maxDepth: number\n): { depth: number; perspectiveRise: number } {\n  const offset = Math.min(1, Math.max(0, absOffset));\n  const cappedMaxDepth = Math.min(maxDepth, Math.max(0, naturalHeight));\n  const depth = offset * cappedMaxDepth;\n  return { depth, perspectiveRise: depth * BAR_DEPTH_PERSPECTIVE_RATIO };\n}\n",
      "type": "registry:lib",
      "target": "components/charts/bar-depth-geometry.ts"
    },
    {
      "path": "src/charts/bar-x-axis.tsx",
      "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport { memo, useEffect, useMemo, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/lib/utils\";\nimport { useChart, useChartStable } from \"./chart-context\";\n\nexport interface BarXAxisProps {\n  /** Width of the date ticker box for fade calculation. Default: 50 */\n  tickerHalfWidth?: number;\n  /** Whether to show all labels or skip some for dense data. Default: false */\n  showAllLabels?: boolean;\n  /** Maximum number of labels to show. Default: 12 */\n  maxLabels?: number;\n}\n\ninterface BarXAxisLabelProps {\n  label: string;\n  x: number;\n  crosshairX: number | null;\n  isHovering: boolean;\n  tickerHalfWidth: number;\n}\n\nfunction BarXAxisLabel({\n  label,\n  x,\n  crosshairX,\n  isHovering,\n  tickerHalfWidth,\n}: BarXAxisLabelProps) {\n  const fadeBuffer = 20;\n  const fadeRadius = tickerHalfWidth + fadeBuffer;\n\n  let opacity = 1;\n  if (isHovering && crosshairX !== null) {\n    const distance = Math.abs(x - crosshairX);\n    if (distance < tickerHalfWidth) {\n      opacity = 0;\n    } else if (distance < fadeRadius) {\n      opacity = (distance - tickerHalfWidth) / fadeBuffer;\n    }\n  }\n\n  // Zero-width container approach for perfect centering\n  return (\n    <div\n      className=\"absolute\"\n      style={{\n        left: x,\n        bottom: 12,\n        width: 0,\n        display: \"flex\",\n        justifyContent: \"center\",\n      }}\n    >\n      <motion.span\n        animate={{ opacity }}\n        className={cn(\"whitespace-nowrap text-chart-label text-xs\")}\n        initial={{ opacity: 1 }}\n        transition={{ duration: 0.4, ease: \"easeInOut\" }}\n      >\n        {label}\n      </motion.span>\n    </div>\n  );\n}\n\nexport function BarXAxis(props: BarXAxisProps) {\n  const { containerRef, barScale } = useChartStable();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  const container = containerRef.current;\n  if (!(mounted && container)) {\n    return null;\n  }\n\n  if (!barScale) {\n    return null;\n  }\n\n  return <BarXAxisInner {...props} container={container} />;\n}\n\nconst BarXAxisInner = memo(function BarXAxisInner({\n  tickerHalfWidth = 50,\n  showAllLabels = false,\n  maxLabels = 12,\n  container,\n}: BarXAxisProps & { container: HTMLDivElement }) {\n  const { margin, tooltipData, barScale, bandWidth, barXAccessor, data } =\n    useChart();\n\n  // Generate labels for each bar\n  const labelsToShow = useMemo(() => {\n    if (!(barScale && bandWidth && barXAccessor)) {\n      return [];\n    }\n\n    const allLabels = data.map((d) => {\n      const label = barXAccessor(d);\n      const bandX = barScale(label) ?? 0;\n      // Center the label under the bar group\n      const x = bandX + bandWidth / 2 + margin.left;\n      return { label, x };\n    });\n\n    // If showAllLabels is true or we have fewer than maxLabels, show all\n    if (showAllLabels || allLabels.length <= maxLabels) {\n      return allLabels;\n    }\n\n    // Otherwise, skip some labels to avoid crowding\n    const step = Math.ceil(allLabels.length / maxLabels);\n    return allLabels.filter((_, i) => i % step === 0);\n  }, [\n    barScale,\n    bandWidth,\n    barXAccessor,\n    data,\n    margin.left,\n    showAllLabels,\n    maxLabels,\n  ]);\n\n  const isHovering = tooltipData !== null;\n  const crosshairX = tooltipData ? tooltipData.x + margin.left : null;\n\n  return createPortal(\n    <div className=\"pointer-events-none absolute inset-0\">\n      {labelsToShow.map((item) => (\n        <BarXAxisLabel\n          crosshairX={crosshairX}\n          isHovering={isHovering}\n          key={`${item.label}-${item.x}`}\n          label={item.label}\n          tickerHalfWidth={tickerHalfWidth}\n          x={item.x}\n        />\n      ))}\n    </div>,\n    container\n  );\n});\n\nBarXAxis.displayName = \"BarXAxis\";\n\nexport default BarXAxis;\n",
      "type": "registry:component",
      "target": "components/charts/bar-x-axis.tsx"
    },
    {
      "path": "src/charts/bar-y-axis.tsx",
      "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport { memo, useEffect, useMemo, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/lib/utils\";\nimport { useChart, useChartStable } from \"./chart-context\";\n\nexport interface BarYAxisProps {\n  /** Whether to show all labels or skip some for dense data. Default: true */\n  showAllLabels?: boolean;\n  /** Maximum number of labels to show. Default: 20 */\n  maxLabels?: number;\n}\n\ninterface BarYAxisLabelProps {\n  label: string;\n  y: number;\n  bandHeight: number;\n  isHovered: boolean;\n}\n\nfunction BarYAxisLabel({\n  label,\n  y,\n  bandHeight,\n  isHovered,\n}: BarYAxisLabelProps) {\n  return (\n    <div\n      className=\"absolute right-0 flex items-center justify-end pr-2\"\n      style={{\n        top: y,\n        height: bandHeight,\n      }}\n    >\n      <motion.span\n        animate={{\n          opacity: isHovered ? 1 : 0.7,\n          color: isHovered\n            ? \"var(--foreground)\"\n            : \"var(--chart-label, var(--color-zinc-500))\",\n        }}\n        className={cn(\"truncate whitespace-nowrap text-right text-xs\")}\n        initial={{\n          opacity: 0.7,\n          color: \"var(--chart-label, var(--color-zinc-500))\",\n        }}\n        style={{ maxWidth: 70 }}\n        transition={{ duration: 0.15 }}\n      >\n        {label}\n      </motion.span>\n    </div>\n  );\n}\n\nexport function BarYAxis(props: BarYAxisProps) {\n  const { containerRef, barScale } = useChartStable();\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n  }, []);\n\n  const container = containerRef.current;\n  if (!(mounted && container)) {\n    return null;\n  }\n\n  if (!barScale) {\n    return null;\n  }\n\n  return <BarYAxisInner {...props} container={container} />;\n}\n\nconst BarYAxisInner = memo(function BarYAxisInner({\n  showAllLabels = true,\n  maxLabels = 20,\n  container,\n}: BarYAxisProps & { container: HTMLDivElement }) {\n  const { margin, barScale, bandWidth, barXAccessor, data, hoveredBarIndex } =\n    useChart();\n\n  // Generate labels for each bar\n  const labelsToShow = useMemo(() => {\n    if (!(barScale && bandWidth && barXAccessor)) {\n      return [];\n    }\n\n    const allLabels = data.map((d, i) => {\n      const label = barXAccessor(d);\n      const bandY = barScale(label) ?? 0;\n      // Center the label vertically within the band\n      const y = bandY + margin.top;\n      return { label, y, bandHeight: bandWidth, index: i };\n    });\n\n    // If showAllLabels is true or we have fewer than maxLabels, show all\n    if (showAllLabels || allLabels.length <= maxLabels) {\n      return allLabels;\n    }\n\n    // Otherwise, skip some labels to avoid crowding\n    const step = Math.ceil(allLabels.length / maxLabels);\n    return allLabels.filter((_, i) => i % step === 0);\n  }, [\n    barScale,\n    bandWidth,\n    barXAccessor,\n    data,\n    margin.top,\n    showAllLabels,\n    maxLabels,\n  ]);\n\n  return createPortal(\n    <div\n      className=\"pointer-events-none absolute top-0 bottom-0\"\n      style={{\n        left: 0,\n        width: margin.left,\n      }}\n    >\n      {labelsToShow.map((item) => (\n        <BarYAxisLabel\n          bandHeight={item.bandHeight}\n          isHovered={hoveredBarIndex === item.index}\n          key={`${item.label}-${item.y}`}\n          label={item.label}\n          y={item.y}\n        />\n      ))}\n    </div>,\n    container\n  );\n});\n\nBarYAxis.displayName = \"BarYAxis\";\n\nexport default BarYAxis;\n",
      "type": "registry:component",
      "target": "components/charts/bar-y-axis.tsx"
    },
    {
      "path": "src/charts/loading-sweep.tsx",
      "content": "\"use client\";\n\nimport { scaleLinear } from \"@visx/scale\";\nimport { AreaClosed, LinePath } from \"@visx/shape\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { chartCssVars, useChartStable } from \"./chart-context\";\nimport {\n  LINE_LOADING_PULSE_EASE,\n  LOADING_LABEL_EXIT_S,\n} from \"./line-loading-timing\";\n\n/**\n * Shared \"sweep\" loading visuals. A soft diagonal shimmer band travels across a\n * self-contained skeleton silhouette on a loop, painted via an SVG mask. The\n * silhouette re-randomizes between passes (held steady during a pass, re-rolled\n * once the band clears the right edge) so it reads as live and still loading,\n * without warping mid-sweep. Used as the `loadingStyle=\"sweep\"` alternative to\n * the traveling pulse on `<Line>` and `<Area>`, and as the skeleton for\n * `<BarChart status=\"loading\">`.\n */\n\n// CurveFactory type - simplified version compatible with visx\n// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory type\ntype CurveFactory = any;\n\n/** One shimmer sweep, in seconds. */\nconst DEFAULT_SWEEP_DURATION_S = 2;\n/** Sweep travel in objectBoundingBox space: off the left edge to off the right. */\nconst SWEEP_START_X = -1;\nconst SWEEP_END_X = 2;\n/** Diagonal tilt of the shimmer band, in degrees. */\nconst SWEEP_ANGLE_DEG = 25;\nconst HEIGHT_MIN_PCT = 20;\nconst HEIGHT_MAX_PCT = 80;\nconst DEFAULT_POINT_COUNT = 14;\nconst BAR_CORNER_RADIUS = 2;\nconst DEFAULT_BAR_COUNT = 12;\nconst DEFAULT_FILL = \"var(--foreground)\";\nconst DEFAULT_BAR_FILL_OPACITY = 0.45;\nconst LINE_STROKE_OPACITY = 0.55;\nconst AREA_FILL_TOP_OPACITY = 0.18;\nconst AREA_FILL_BOTTOM_OPACITY = 0.02;\n/** Bar width as a fraction of its band (the rest is the inter-bar gap). */\nconst DEFAULT_BAR_FRACTION = 0.7;\n\n// ─── Pure, SSR-safe helpers ──────────────────────────────────────────────\n// Heights come from a deterministic hash of (index, seed), never\n// `Math.random()`, so the first server render and first client render agree\n// (no Next.js hydration mismatch). Re-randomizing only bumps the numeric seed\n// on the client, after a sweep completes.\n\n/** Cheap deterministic hash to a fractional part in [0, 1). */\nfunction hashFract(n: number): number {\n  const x = Math.sin(n) * 43_758.5453;\n  return x - Math.floor(x);\n}\n\n/** Deterministic heights (percentages of the available height) for a seed. */\nexport function getSkeletonHeights(\n  count: number,\n  seed = 0,\n  min = HEIGHT_MIN_PCT,\n  max = HEIGHT_MAX_PCT\n): number[] {\n  const range = max - min;\n  return Array.from(\n    { length: count },\n    (_, i) => min + Math.floor(hashFract((i + 1) * 12.9898 + seed) * range)\n  );\n}\n\n/** Deterministic up/down (±1) per bar for the \"center\" baseline. */\nfunction getSkeletonSigns(count: number, seed = 0): number[] {\n  return Array.from({ length: count }, (_, i) =>\n    hashFract((i + 1) * 78.233 + seed) < 0.5 ? -1 : 1\n  );\n}\n\n/** Bell-curve opacity stops (sin squared) for the shimmer band's soft edges. */\nfunction generateEasedGradientStops(\n  steps = 17,\n  minOpacity = 0.05,\n  maxOpacity = 0.9\n) {\n  return Array.from({ length: steps }, (_, i) => {\n    const t = i / (steps - 1);\n    const eased = Math.sin(t * Math.PI) ** 2;\n    const opacity = minOpacity + eased * (maxOpacity - minOpacity);\n    return {\n      offset: `${(t * 100).toFixed(0)}%`,\n      opacity: Number(opacity.toFixed(3)),\n    };\n  });\n}\n\n// ─── Shared mask defs (`${chartId}-mask` is the mask id) ─────────────────────\n\nfunction LoadingSweepMask({\n  chartId,\n  width,\n  height,\n  durationSeconds,\n  onSweepComplete,\n}: {\n  chartId: string;\n  width: number;\n  height: number;\n  durationSeconds: number;\n  onSweepComplete: () => void;\n}) {\n  const gradientStops = useMemo(() => generateEasedGradientStops(), []);\n  const lastXRef = useRef(SWEEP_START_X);\n\n  const handleUpdate = useCallback(\n    (latest: { x?: number }) => {\n      const xValue = typeof latest.x === \"number\" ? latest.x : SWEEP_START_X;\n      // Re-roll once the band has cleared the visible area (crossed past 1),\n      // so the silhouette never changes shape under the user's eye.\n      if (xValue >= 1 && lastXRef.current < 1) {\n        onSweepComplete();\n      }\n      lastXRef.current = xValue;\n    },\n    [onSweepComplete]\n  );\n\n  return (\n    <>\n      <linearGradient id={`${chartId}-grad`} x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\">\n        {gradientStops.map(({ offset, opacity }) => (\n          <stop\n            key={offset}\n            offset={offset}\n            stopColor=\"white\"\n            stopOpacity={opacity}\n          />\n        ))}\n      </linearGradient>\n      <pattern\n        height=\"1\"\n        id={`${chartId}-pattern`}\n        patternContentUnits=\"objectBoundingBox\"\n        patternTransform={`rotate(${SWEEP_ANGLE_DEG})`}\n        patternUnits=\"objectBoundingBox\"\n        width={3}\n        x=\"0\"\n        y=\"0\"\n      >\n        <motion.rect\n          animate={{ x: SWEEP_END_X }}\n          fill={`url(#${chartId}-grad)`}\n          height=\"1\"\n          initial={{ x: SWEEP_START_X }}\n          onUpdate={handleUpdate}\n          transition={{\n            duration: durationSeconds,\n            ease: \"linear\",\n            repeat: Number.POSITIVE_INFINITY,\n            repeatType: \"loop\",\n          }}\n          width=\"1\"\n          y=\"0\"\n        />\n      </pattern>\n      <mask id={`${chartId}-mask`} maskUnits=\"userSpaceOnUse\">\n        <rect fill={`url(#${chartId}-pattern)`} height={height} width={width} />\n      </mask>\n    </>\n  );\n}\n\n// ─── Line / Area loading sweep (its own re-randomizing silhouette) ───────────\n\nexport interface LineLoadingSweepProps {\n  /** Curve factory from the host `<Line>` / `<Area>`, so the silhouette matches\n   * the chart's interpolation (step, smooth, linear, …). */\n  curve: CurveFactory;\n  /** Fill the silhouette as an area (for `<Area>`); otherwise stroke only. */\n  withArea?: boolean;\n  /** Loading phase: `\"loop\"` (steady), `\"exit\"` (loading → ready), or `\"enter\"`\n   * (ready → loading). Exit/enter fade the silhouette and then signal the chart\n   * to continue its reveal. Default: `\"loop\"`. */\n  mode?: \"loop\" | \"exit\" | \"enter\";\n  /** Fired when an exit/enter transition finishes, to advance the chart phase. */\n  onTransitionComplete?: () => void;\n  stroke?: string;\n  strokeOpacity?: number;\n  strokeWidth?: number;\n  pointCount?: number;\n  durationSeconds?: number;\n}\n\n/**\n * Renders a placeholder line/area silhouette (its own, not the chart's skeleton)\n * with the shimmer sweeping across it. The silhouette re-randomizes between\n * passes. Reads inner dimensions from chart context.\n */\nexport function LineLoadingSweep({\n  curve,\n  withArea = false,\n  mode = \"loop\",\n  onTransitionComplete,\n  stroke = chartCssVars.foreground,\n  strokeOpacity = LINE_STROKE_OPACITY,\n  strokeWidth = 2,\n  pointCount = DEFAULT_POINT_COUNT,\n  durationSeconds = DEFAULT_SWEEP_DURATION_S,\n}: LineLoadingSweepProps) {\n  const { innerWidth, innerHeight } = useChartStable();\n  const reduceMotion = useReducedMotion();\n  const reactId = useId();\n  const chartId = `line-sweep-${reactId.replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const isLoop = mode === \"loop\";\n\n  const [tick, setTick] = useState(0);\n  // Re-randomize only while looping; hold the silhouette steady through a\n  // transition so it fades out (or in) as one piece.\n  const onSweepComplete = useCallback(() => {\n    if (isLoop) {\n      setTick((prev) => prev + 1);\n    }\n  }, [isLoop]);\n  const heights = useMemo(\n    () => getSkeletonHeights(pointCount, tick),\n    [pointCount, tick]\n  );\n\n  // With reduced motion there is no fade to await, so signal the handoff\n  // immediately or the phase machine would stall mid-transition.\n  useEffect(() => {\n    if (reduceMotion && !isLoop) {\n      onTransitionComplete?.();\n    }\n  }, [reduceMotion, isLoop, onTransitionComplete]);\n\n  if (innerWidth <= 0 || innerHeight <= 0 || heights.length < 2) {\n    return null;\n  }\n\n  const xScale = scaleLinear({\n    domain: [0, heights.length - 1],\n    range: [0, innerWidth],\n  });\n  const yScale = scaleLinear({ domain: [0, 100], range: [innerHeight, 0] });\n  const points = heights.map((value, index) => ({ index, value }));\n  const getX = (d: { index: number }) => xScale(d.index);\n  const getY = (d: { value: number }) => yScale(d.value);\n\n  const silhouette = (\n    <>\n      {withArea ? (\n        <AreaClosed\n          curve={curve}\n          data={points}\n          fill={`url(#${chartId}-area)`}\n          x={getX}\n          y={getY}\n          yScale={yScale}\n        />\n      ) : null}\n      <LinePath\n        curve={curve}\n        data={points}\n        fill=\"none\"\n        stroke={stroke}\n        strokeLinecap=\"round\"\n        strokeOpacity={strokeOpacity}\n        strokeWidth={strokeWidth}\n        x={getX}\n        y={getY}\n      />\n    </>\n  );\n\n  const areaGradient = withArea ? (\n    <linearGradient id={`${chartId}-area`} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n      <stop\n        offset=\"0%\"\n        stopColor={stroke}\n        stopOpacity={AREA_FILL_TOP_OPACITY}\n      />\n      <stop\n        offset=\"100%\"\n        stopColor={stroke}\n        stopOpacity={AREA_FILL_BOTTOM_OPACITY}\n      />\n    </linearGradient>\n  ) : null;\n\n  if (reduceMotion) {\n    return (\n      <>\n        {areaGradient ? <defs>{areaGradient}</defs> : null}\n        {silhouette}\n      </>\n    );\n  }\n\n  const maskUrl = `url(#${chartId}-mask)`;\n  const defs = (\n    <defs>\n      {areaGradient}\n      <LoadingSweepMask\n        chartId={chartId}\n        durationSeconds={durationSeconds}\n        height={innerHeight}\n        onSweepComplete={onSweepComplete}\n        width={innerWidth}\n      />\n    </defs>\n  );\n\n  if (isLoop) {\n    return (\n      <>\n        {defs}\n        <g mask={maskUrl}>{silhouette}</g>\n      </>\n    );\n  }\n\n  // Transition: fade the swept silhouette out (exit) or in (enter), then hand\n  // off to the chart so it can reveal the real series.\n  return (\n    <>\n      {defs}\n      <motion.g\n        animate={{ opacity: mode === \"exit\" ? 0 : 1 }}\n        initial={{ opacity: mode === \"exit\" ? 1 : 0 }}\n        mask={maskUrl}\n        onAnimationComplete={onTransitionComplete}\n        transition={{\n          duration: LOADING_LABEL_EXIT_S,\n          ease: [...LINE_LOADING_PULSE_EASE],\n        }}\n      >\n        {silhouette}\n      </motion.g>\n    </>\n  );\n}\n\nLineLoadingSweep.displayName = \"LineLoadingSweep\";\n\n// ─── Bar loading skeleton (seeded bars under the sweep, inner coords) ─────────\n\nfunction SkeletonBars({\n  heights,\n  signs,\n  innerWidth,\n  innerHeight,\n  baseline,\n  barFraction,\n  fill,\n  fillOpacity,\n}: {\n  heights: number[];\n  signs: number[];\n  innerWidth: number;\n  innerHeight: number;\n  baseline: \"bottom\" | \"center\";\n  barFraction: number;\n  fill: string;\n  fillOpacity: number;\n}) {\n  const bandWidth = innerWidth / heights.length;\n  const barW = bandWidth * barFraction;\n  const xOffset = (bandWidth * (1 - barFraction)) / 2;\n  const isCenter = baseline === \"center\";\n  const baselineY = isCenter ? innerHeight / 2 : innerHeight;\n  const halfBarH = isCenter ? innerHeight / 2 : innerHeight;\n\n  return (\n    <>\n      {heights.map((value, i) => {\n        const sign = isCenter ? (signs[i] ?? 1) : 1;\n        const barH = Math.max(1, halfBarH * (value / 100));\n        const x = i * bandWidth + xOffset;\n        const y = sign === 1 ? baselineY - barH : baselineY;\n        return (\n          <rect\n            fill={fill}\n            fillOpacity={fillOpacity}\n            height={barH}\n            key={`${x.toFixed(2)}-${value}`}\n            rx={BAR_CORNER_RADIUS}\n            width={barW}\n            x={x}\n            y={y}\n          />\n        );\n      })}\n    </>\n  );\n}\n\nexport interface BarLoadingSkeletonProps {\n  innerWidth: number;\n  innerHeight: number;\n  /** Number of skeleton bars. Default: 12 */\n  barCount?: number;\n  /** Bar fill color. Default: `var(--foreground)` */\n  fill?: string;\n  /** Bar fill opacity. Default: 0.45 */\n  fillOpacity?: number;\n  /** Bars rise from the bottom or diverge from the vertical center. Default: `\"bottom\"` */\n  baseline?: \"bottom\" | \"center\";\n  /** Bar width as a fraction of its band (0–1). Default: 0.7 */\n  barFraction?: number;\n  /** One shimmer sweep, in seconds. Default: 2 */\n  durationSeconds?: number;\n}\n\n/**\n * Skeleton bars masked by the shimmer sweep, re-randomizing between passes.\n * Rendered in the chart's inner coordinate space (origin at the inner top-left),\n * so a `BarChart` drops it inside its margin-translated group.\n */\nexport function BarLoadingSkeleton({\n  innerWidth,\n  innerHeight,\n  barCount = DEFAULT_BAR_COUNT,\n  fill = DEFAULT_FILL,\n  fillOpacity = DEFAULT_BAR_FILL_OPACITY,\n  baseline = \"bottom\",\n  barFraction = DEFAULT_BAR_FRACTION,\n  durationSeconds = DEFAULT_SWEEP_DURATION_S,\n}: BarLoadingSkeletonProps) {\n  const reduceMotion = useReducedMotion();\n  const reactId = useId();\n  const chartId = `bar-sweep-${reactId.replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const [tick, setTick] = useState(0);\n  const onSweepComplete = useCallback(() => setTick((prev) => prev + 1), []);\n  const heights = useMemo(\n    () => getSkeletonHeights(barCount, tick),\n    [barCount, tick]\n  );\n  const signs = useMemo(\n    () => getSkeletonSigns(barCount, tick),\n    [barCount, tick]\n  );\n\n  if (innerWidth <= 0 || innerHeight <= 0) {\n    return null;\n  }\n\n  const bars = (\n    <SkeletonBars\n      barFraction={barFraction}\n      baseline={baseline}\n      fill={fill}\n      fillOpacity={fillOpacity}\n      heights={heights}\n      innerHeight={innerHeight}\n      innerWidth={innerWidth}\n      signs={signs}\n    />\n  );\n\n  if (reduceMotion) {\n    return bars;\n  }\n\n  return (\n    <>\n      <defs>\n        <LoadingSweepMask\n          chartId={chartId}\n          durationSeconds={durationSeconds}\n          height={innerHeight}\n          onSweepComplete={onSweepComplete}\n          width={innerWidth}\n        />\n      </defs>\n      <g mask={`url(#${chartId}-mask)`}>{bars}</g>\n    </>\n  );\n}\n\nBarLoadingSkeleton.displayName = \"BarLoadingSkeleton\";\n",
      "type": "registry:component",
      "target": "components/charts/loading-sweep.tsx"
    },
    {
      "path": "src/charts/bar-chart-loading.tsx",
      "content": "\"use client\";\n\nimport { BarChart } from \"./bar-chart\";\nimport type { Margin } from \"./chart-context\";\n\nconst EMPTY_DATA: Record<string, unknown>[] = [];\n\nexport interface BarChartLoadingProps {\n  /** Chart margins. */\n  margin?: Partial<Margin>;\n  /** Aspect ratio as \"width / height\". Default: \"2 / 1\" */\n  aspectRatio?: string;\n  /** Additional class name for the container. */\n  className?: string;\n}\n\n/**\n * Turnkey loading skeleton for bar charts, a thin shortcut for\n * `<BarChart status=\"loading\" />`. Renders shimmer-swept placeholder bars while\n * data is fetching; swap in a real `<BarChart>` once it resolves.\n */\nexport function BarChartLoading({\n  margin,\n  aspectRatio = \"2 / 1\",\n  className = \"\",\n}: BarChartLoadingProps) {\n  return (\n    <BarChart\n      aspectRatio={aspectRatio}\n      className={className}\n      data={EMPTY_DATA}\n      margin={margin}\n      status=\"loading\"\n    />\n  );\n}\n\nexport default BarChartLoading;\n",
      "type": "registry:component",
      "target": "components/charts/bar-chart-loading.tsx"
    }
  ]
}