{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-animation",
  "type": "registry:component",
  "title": "Chart Animation",
  "description": "Shared motion helpers for chart enter transitions, clip reveals, and staggered animations",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "src/charts/animation.ts",
      "content": "import type { Transition } from \"motion/react\";\n\n/** Default clip-reveal easing for cartesian charts. */\nexport const DEFAULT_ANIMATION_EASING = \"cubic-bezier(0.85, 0, 0.15, 1)\";\n\nexport const DEFAULT_ANIMATION_DURATION_MS = 1100;\n\n/** Default enter transition — matches the original line chart reveal. */\nexport const DEFAULT_CHART_ENTER_TRANSITION: Transition = {\n  type: \"tween\",\n  duration: DEFAULT_ANIMATION_DURATION_MS / 1000,\n  ease: [0.85, 0, 0.15, 1],\n};\n\n/**\n * Clip-path width reveal must use tween — spring does not reliably animate SVG width.\n */\nexport function clipRevealTransition(enterTransition?: Transition): Transition {\n  if (enterTransition?.type === \"tween\") {\n    return {\n      ...enterTransition,\n      ease: enterTransition.ease ?? DEFAULT_CHART_ENTER_TRANSITION.ease,\n    };\n  }\n\n  const duration =\n    typeof enterTransition?.duration === \"number\"\n      ? enterTransition.duration\n      : DEFAULT_ANIMATION_DURATION_MS / 1000;\n\n  return {\n    type: \"tween\",\n    duration,\n    ease: DEFAULT_CHART_ENTER_TRANSITION.ease,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/charts/animation.ts"
    },
    {
      "path": "src/charts/motion-utils.ts",
      "content": "import type { Transition } from \"motion/react\";\nimport { DEFAULT_CHART_ENTER_TRANSITION } from \"./animation\";\n\nexport function transitionWithDelay(\n  transition: Transition | undefined,\n  delaySeconds: number,\n  fallback: Transition = DEFAULT_CHART_ENTER_TRANSITION\n): Transition {\n  const base = transition ?? fallback;\n  return { ...base, delay: delaySeconds };\n}\n\nexport interface SpringOptions {\n  stiffness: number;\n  damping: number;\n  mass?: number;\n}\n\nexport function springOptionsFromTransition(\n  transition?: Transition,\n  fallback: SpringOptions = { stiffness: 60, damping: 20 }\n): SpringOptions {\n  if (!transition) {\n    return fallback;\n  }\n  if (transition.type === \"spring\") {\n    const bounce =\n      typeof transition.bounce === \"number\" ? transition.bounce : undefined;\n    const baseStiffness =\n      typeof transition.stiffness === \"number\"\n        ? transition.stiffness\n        : fallback.stiffness;\n    const baseDamping =\n      typeof transition.damping === \"number\"\n        ? transition.damping\n        : fallback.damping;\n    return {\n      stiffness:\n        bounce == null\n          ? baseStiffness\n          : Math.min(400, Math.max(80, baseStiffness * (1 + bounce * 0.35))),\n      damping:\n        bounce == null\n          ? baseDamping\n          : Math.max(8, baseDamping * (1 - bounce * 0.25)),\n      mass:\n        typeof transition.mass === \"number\" ? transition.mass : fallback.mass,\n    };\n  }\n  const duration =\n    \"duration\" in transition && typeof transition.duration === \"number\"\n      ? transition.duration\n      : 0.8;\n  return {\n    stiffness: Math.min(500, Math.max(40, 280 / duration)),\n    damping: Math.min(40, Math.max(12, 18 + duration * 4)),\n  };\n}\n",
      "type": "registry:component",
      "target": "components/charts/motion-utils.ts"
    },
    {
      "path": "src/charts/use-mount-progress.ts",
      "content": "\"use client\";\n\nimport { animate, type Transition, useMotionValue } from \"motion/react\";\nimport { useEffect, useRef } from \"react\";\nimport { DEFAULT_CHART_ENTER_TRANSITION } from \"./animation\";\n\n/** Drives 0→1 enter progress using the studio motion transition (spring or tween). */\nexport function useMountProgress(\n  enterTransition: Transition | undefined,\n  delaySeconds: number,\n  replayKey: number | string\n) {\n  const progress = useMotionValue(0);\n  const transitionRef = useRef(enterTransition);\n  transitionRef.current = enterTransition;\n\n  // replayKey intentionally retriggers enter when motion settings change\n  // biome-ignore lint/correctness/useExhaustiveDependencies: replayKey\n  useEffect(() => {\n    progress.set(0);\n    const controls = animate(progress, 1, {\n      ...(transitionRef.current ?? DEFAULT_CHART_ENTER_TRANSITION),\n      delay: delaySeconds,\n    });\n    return () => controls.stop();\n  }, [delaySeconds, replayKey, progress]);\n\n  return progress;\n}\n",
      "type": "registry:component",
      "target": "components/charts/use-mount-progress.ts"
    },
    {
      "path": "src/charts/use-enter-complete.ts",
      "content": "\"use client\";\n\nimport type { MotionValue } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true once a mount-progress MotionValue reaches 1.\n * Use to swap animated MotionValue-driven props for static values after\n * enter completes — drops per-frame subscriptions during pan/hover.\n */\nexport function useEnterComplete(mountProgress: MotionValue<number>): boolean {\n  const [complete, setComplete] = useState(() => mountProgress.get() >= 1);\n\n  useEffect(() => {\n    if (mountProgress.get() >= 1) {\n      setComplete(true);\n      return;\n    }\n\n    return mountProgress.on(\"change\", (value) => {\n      if (value >= 1) {\n        setComplete(true);\n      }\n    });\n  }, [mountProgress]);\n\n  return complete;\n}\n",
      "type": "registry:component",
      "target": "components/charts/use-enter-complete.ts"
    },
    {
      "path": "src/charts/chart-reveal-clip.tsx",
      "content": "\"use client\";\n\nimport type { Transition } from \"motion/react\";\nimport { motion } from \"motion/react\";\nimport { clipRevealTransition } from \"./animation\";\n\nexport type ChartRevealClipMode = \"reveal\" | \"conceal\";\n\nexport interface ChartRevealClipProps {\n  clipPathId: string;\n  height: number;\n  targetWidth: number;\n  enterTransition?: Transition;\n  /** Bumps when motion settings change to replay the reveal. */\n  revealEpoch: number;\n  /** Extra inset around the clip rect so edge glyphs are not cut off. */\n  padding?: number;\n  /** When false, clip stays at full width (no grow animation). */\n  animating?: boolean;\n  /** Reveal grows 0 → full; conceal shrinks full → 0 (ready → loading). */\n  mode?: ChartRevealClipMode;\n  /** Called when a conceal animation finishes. */\n  onComplete?: () => void;\n}\n\n/**\n * Left-to-right clip reveal for cartesian series.\n * Grows clip rect width from 0 → full (true LTR; scaleX is avoided — it reveals from center).\n */\nexport function ChartRevealClip({\n  clipPathId,\n  height,\n  targetWidth,\n  enterTransition,\n  revealEpoch,\n  padding = 0,\n  animating = true,\n  mode = \"reveal\",\n  onComplete,\n}: ChartRevealClipProps) {\n  const transition = clipRevealTransition(enterTransition);\n  const paddedWidth = Math.max(0, targetWidth + padding * 2);\n  const paddedHeight = height + padding * 2;\n\n  if (!animating) {\n    return (\n      <clipPath id={clipPathId}>\n        <rect\n          height={paddedHeight}\n          width={paddedWidth}\n          x={-padding}\n          y={-padding}\n        />\n      </clipPath>\n    );\n  }\n\n  if (mode === \"conceal\") {\n    // Mirror the LTR reveal: advance the clip's left edge rightward while width\n    // shrinks (same geometry as `LineLoadingPulseStroke` exit half-cycle).\n    const rightEdge = -padding + paddedWidth;\n\n    return (\n      <clipPath id={clipPathId}>\n        <motion.rect\n          animate={{ width: 0, x: rightEdge }}\n          height={paddedHeight}\n          initial={{ width: paddedWidth, x: -padding }}\n          key={`conceal-${revealEpoch}`}\n          onAnimationComplete={() => onComplete?.()}\n          transition={transition}\n          y={-padding}\n        />\n      </clipPath>\n    );\n  }\n\n  return (\n    <clipPath id={clipPathId}>\n      <motion.rect\n        animate={{ width: paddedWidth }}\n        height={paddedHeight}\n        initial={{ width: 0 }}\n        key={`reveal-${revealEpoch}`}\n        transition={transition}\n        width={paddedWidth}\n        x={-padding}\n        y={-padding}\n      />\n    </clipPath>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/chart-reveal-clip.tsx"
    },
    {
      "path": "src/charts/static-chart-preview-context.tsx",
      "content": "\"use client\";\n\nimport { createContext, type ReactNode, useContext } from \"react\";\n\nconst StaticChartPreviewContext = createContext(false);\n\n/** Disables cartesian reveal clip-path for static docs previews. */\nexport function StaticChartPreviewProvider({\n  children,\n}: {\n  children: ReactNode;\n}) {\n  return (\n    <StaticChartPreviewContext.Provider value={true}>\n      {children}\n    </StaticChartPreviewContext.Provider>\n  );\n}\n\nexport function useStaticChartPreview() {\n  return useContext(StaticChartPreviewContext);\n}\n",
      "type": "registry:component",
      "target": "components/charts/static-chart-preview-context.tsx"
    },
    {
      "path": "src/charts/chart-defs.ts",
      "content": "import {\n  Children,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\n\nexport function getChartChildComponentName(child: ReactElement): string {\n  const childType = child.type as { displayName?: string; name?: string };\n  return typeof child.type === \"function\"\n    ? childType.displayName || childType.name || \"\"\n    : \"\";\n}\n\nconst VISX_PATTERN_COMPONENT_NAMES = new Set([\n  \"Lines\",\n  \"Circles\",\n  \"Waves\",\n  \"Hexagons\",\n  \"Path\",\n  \"Pattern\",\n]);\n\n/** @visx/pattern default exports use short names (e.g. `Lines`); also match *Pattern* displayNames. */\nexport function isPatternDefComponent(child: ReactElement): boolean {\n  const name = getChartChildComponentName(child);\n  return name.includes(\"Pattern\") || VISX_PATTERN_COMPONENT_NAMES.has(name);\n}\n\nexport function isGradientDefComponent(child: ReactElement): boolean {\n  const name = getChartChildComponentName(child);\n  return (\n    name.includes(\"Gradient\") ||\n    name === \"LinearGradient\" ||\n    name === \"RadialGradient\"\n  );\n}\n\nexport function isChartDefsComponent(child: ReactElement): boolean {\n  return isPatternDefComponent(child) || isGradientDefComponent(child);\n}\n\n/** Split hoisted defs: @visx/pattern nodes already wrap `<defs>` and render at the svg root. */\nexport function partitionChartDefNodes(defNodes: ReactElement[]): {\n  patternDefNodes: ReactElement[];\n  gradientDefNodes: ReactElement[];\n} {\n  const patternDefNodes: ReactElement[] = [];\n  const gradientDefNodes: ReactElement[] = [];\n\n  for (const node of defNodes) {\n    if (isPatternDefComponent(node)) {\n      patternDefNodes.push(node);\n    } else {\n      gradientDefNodes.push(node);\n    }\n  }\n\n  return { patternDefNodes, gradientDefNodes };\n}\n\nexport function collectChartDefsChildren(children: ReactNode): ReactElement[] {\n  const defNodes: ReactElement[] = [];\n\n  Children.forEach(children, (child) => {\n    if (isValidElement(child) && isChartDefsComponent(child)) {\n      defNodes.push(child);\n    }\n  });\n\n  return defNodes;\n}\n",
      "type": "registry:component",
      "target": "components/charts/chart-defs.ts"
    }
  ]
}