{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-chart",
  "type": "registry:block",
  "title": "Animated Chart",
  "description": "An animated column chart with spring physics, viewport-triggered animations, and support for dynamic data transitions.",
  "dependencies": ["motion@12", "motion"],
  "files": [
    {
      "path": "registry/abui/marketing/animated-chart.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useRef, useEffect, useMemo } from \"react\"\nimport { motion, useInView, useAnimationControls } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface ColumnData {\n  title: string\n  value: number\n  /** String to prepend before the value (e.g., \"$\") */\n  prependString?: string\n  /** String to append after the value (e.g., \"%\") */\n  appendString?: string\n  /** Animation duration in seconds */\n  animationDuration?: number\n  /** Animation delay in seconds */\n  animationDelay?: number\n  /** ClassName applied to the animated column bar element */\n  className?: string\n  /** ClassName applied to the column's top border */\n  topBorderClassName?: string\n  /** ClassName applied to this column's title (overrides global titleClassName) */\n  titleClassName?: string\n  /** ClassName applied to this column's value (overrides global valueClassName) */\n  valueClassName?: string\n}\n\ninterface AnimatedChartProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  columns: ColumnData[]\n  maxValue: number\n  /** ClassName applied to all column titles */\n  titleClassName?: string\n  /** ClassName applied to all column values */\n  valueClassName?: string\n  /** When true, columns animate from zero whenever data changes. Default: false */\n  restartOnDataChange?: boolean\n}\n\ninterface AnimatedChartColumnProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  title: string\n  value: number\n  maxValue: number\n  prependString?: string\n  appendString?: string\n  animationDuration: number\n  animationDelay: number\n  columnClassName?: string\n  topBorderClassName?: string\n  /** Global titleClassName from parent */\n  globalTitleClassName?: string\n  /** Global valueClassName from parent */\n  globalValueClassName?: string\n  /** Column-specific titleClassName (overrides global) */\n  columnTitleClassName?: string\n  /** Column-specific valueClassName (overrides global) */\n  columnValueClassName?: string\n  isInView: boolean\n  isLast: boolean\n  restartTrigger: number\n}\n\nfunction AnimatedChartColumn({\n  title,\n  value,\n  maxValue,\n  prependString,\n  appendString,\n  animationDuration,\n  animationDelay,\n  columnClassName,\n  topBorderClassName,\n  globalTitleClassName,\n  globalValueClassName,\n  columnTitleClassName,\n  columnValueClassName,\n  isInView,\n  isLast,\n  restartTrigger,\n  className,\n  ...props\n}: AnimatedChartColumnProps) {\n  const heightPercentage = (value / maxValue) * 100\n  const heightPercentageRef = useRef(heightPercentage)\n  heightPercentageRef.current = heightPercentage\n\n  const barControls = useAnimationControls()\n  const valueControls = useAnimationControls()\n\n  // Handle animation when in view or when restart is triggered\n  useEffect(() => {\n    const animateFromZero = async () => {\n      // Stop any running animations first\n      barControls.stop()\n      valueControls.stop()\n\n      // Reset to zero instantly\n      barControls.set({ height: 0 })\n      valueControls.set({ opacity: 0 })\n\n      // Small delay to ensure the reset is rendered before animating\n      await new Promise(resolve => requestAnimationFrame(resolve))\n\n      // Animate to target values\n      if (isInView) {\n        barControls.start({\n          height: `${heightPercentageRef.current}%`,\n          transition: {\n            type: \"spring\",\n            damping: 25,\n            stiffness: 50,\n            delay: animationDelay,\n          },\n        })\n\n        valueControls.start({\n          opacity: 1,\n          transition: {\n            delay: animationDelay + animationDuration * 0.5,\n            duration: 0.3,\n          },\n        })\n      }\n    }\n\n    animateFromZero()\n  }, [restartTrigger, isInView, animationDelay, animationDuration, barControls, valueControls])\n\n  return (\n    <div\n      data-slot=\"animated-charts-column\"\n      className={cn(\"relative flex-1 flex flex-col\", !isLast && \"border-r\", className)}\n      {...props}\n    >\n      {/* Title wrapper - fixed position at top of column */}\n      <div data-slot=\"animated-charts-column-title-wrapper\" className=\"absolute top-0 left-0 right-0 p-2 px-3\">\n        <span\n          data-slot=\"animated-charts-column-title\"\n          className={cn(\"text-base font-normal text-foreground/50\", globalTitleClassName, columnTitleClassName)}\n        >\n          {title}\n        </span>\n      </div>\n\n      {/* Bar container - takes remaining space and aligns bar to bottom */}\n      <div className=\"relative flex-1 flex flex-col justify-end border-t border-border/10\">\n        {/* Column bar */}\n        <motion.div\n          data-slot=\"animated-charts-column-bar\"\n          className={cn(\"relative w-full border-t-2 border-border/30 bg-muted/20\", columnClassName, topBorderClassName)}\n          initial={{ height: 0 }}\n          animate={barControls}\n        >\n          {/* Value positioned at top-left inside the bar */}\n          <motion.span\n            data-slot=\"animated-charts-column-value\"\n            className={cn(\n              \"absolute top-2 left-3 text-base font-normal text-foreground\",\n              globalValueClassName,\n              columnValueClassName,\n            )}\n            initial={{ opacity: 0 }}\n            animate={valueControls}\n          >\n            {prependString && `${prependString} `}\n            {value}\n            {appendString && ` ${appendString}`}\n          </motion.span>\n        </motion.div>\n      </div>\n    </div>\n  )\n}\n\nfunction AnimatedChart({\n  columns,\n  maxValue,\n  titleClassName,\n  valueClassName,\n  restartOnDataChange = false,\n  className,\n  ...props\n}: AnimatedChartProps) {\n  const ref = useRef<HTMLDivElement>(null)\n  const isInView = useInView(ref, { once: true, amount: 0.5 })\n\n  // Generate a data signature to detect changes\n  const dataSignature = useMemo(() => {\n    return JSON.stringify(columns.map(c => ({ title: c.title, value: c.value })))\n  }, [columns])\n\n  // Track restart trigger - increments when data changes (if restartOnDataChange is true)\n  const restartTriggerRef = useRef(0)\n  const prevDataSignatureRef = useRef(dataSignature)\n\n  if (restartOnDataChange && prevDataSignatureRef.current !== dataSignature) {\n    restartTriggerRef.current += 1\n    prevDataSignatureRef.current = dataSignature\n  }\n\n  return (\n    <div ref={ref} data-slot=\"animated-charts\" className={cn(\"flex w-full gap-0 border\", className)} {...props}>\n      {columns.map((column, index) => (\n        <AnimatedChartColumn\n          key={index}\n          title={column.title}\n          value={column.value}\n          maxValue={maxValue}\n          prependString={column.prependString}\n          appendString={column.appendString}\n          animationDuration={column.animationDuration ?? 1}\n          animationDelay={column.animationDelay ?? 0}\n          columnClassName={column.className}\n          topBorderClassName={column.topBorderClassName}\n          globalTitleClassName={titleClassName}\n          globalValueClassName={valueClassName}\n          columnTitleClassName={column.titleClassName}\n          columnValueClassName={column.valueClassName}\n          isInView={isInView}\n          isLast={index === columns.length - 1}\n          restartTrigger={restartTriggerRef.current}\n        />\n      ))}\n    </div>\n  )\n}\n\nexport { AnimatedChart, AnimatedChartColumn }\nexport type { AnimatedChartProps, AnimatedChartColumnProps, ColumnData }\n",
      "type": "registry:component"
    }
  ]
}
