diff --git a/app/sites/[id]/funnels/[funnelId]/page.tsx b/app/sites/[id]/funnels/[funnelId]/page.tsx
index 43ebae6..c4171f3 100644
--- a/app/sites/[id]/funnels/[funnelId]/page.tsx
+++ b/app/sites/[id]/funnels/[funnelId]/page.tsx
@@ -7,7 +7,7 @@ import { getFunnel, getFunnelStats, deleteFunnel, type Funnel, type FunnelStats
import { toast, Select, DatePicker, ChevronLeftIcon, ArrowRightIcon, TrashIcon, Button } from '@ciphera-net/ui'
import { FunnelDetailSkeleton, useMinimumLoading } from '@/components/skeletons'
import Link from 'next/link'
-import FunnelChart from '@/components/dashboard/FunnelChart'
+import { FunnelChart } from '@/components/ui/funnel-chart'
import { getDateRange } from '@ciphera-net/ui'
export default function FunnelReportPage() {
@@ -108,10 +108,8 @@ export default function FunnelReportPage() {
}
const chartData = stats.steps.map(s => ({
- name: s.step.name,
- visitors: s.visitors,
- dropoff: s.dropoff,
- conversion: s.conversion
+ label: s.step.name,
+ value: s.visitors,
}))
return (
@@ -173,7 +171,12 @@ export default function FunnelReportPage() {
Funnel Visualization
-
+
{/* Detailed Stats Table */}
diff --git a/components/dashboard/FunnelChart.tsx b/components/dashboard/FunnelChart.tsx
deleted file mode 100644
index 3894361..0000000
--- a/components/dashboard/FunnelChart.tsx
+++ /dev/null
@@ -1,176 +0,0 @@
-'use client'
-
-import { useState, useMemo } from 'react'
-import { motion } from 'framer-motion'
-import { cn, formatNumber } from '@ciphera-net/ui'
-
-interface FunnelChartProps {
- steps: Array<{
- name: string
- visitors: number
- dropoff: number
- conversion: number
- }>
- className?: string
-}
-
-export default function FunnelChart({ steps, className }: FunnelChartProps) {
- const [hoveredIndex, setHoveredIndex] = useState(null)
-
- const maxVisitors = steps[0]?.visitors ?? 0
- const n = steps.length
-
- if (!n || maxVisitors === 0) return null
-
- // SVG layout
- const W = 800
- const H = 400
- const cx = W / 2
- const maxHW = W * 0.28
- const minHW = 28
- const segH = H / n
- const k = 0.5 // bezier tension
-
- // n+1 boundary points: top of each step + bottom of last
- const bounds = useMemo(() => {
- return Array.from({ length: n + 1 }, (_, i) => {
- const y = i * segH
- const visitors = i < n ? steps[i].visitors : steps[n - 1].visitors * 0.5
- const hw = Math.max(minHW, (visitors / maxVisitors) * maxHW)
- return { y, hw }
- })
- }, [steps, n, maxVisitors, segH])
-
- // Curved path for one segment
- const segPath = (i: number) => {
- const t = bounds[i], b = bounds[i + 1]
- const dy = b.y - t.y
- return [
- `M${cx - t.hw},${t.y}`,
- `L${cx + t.hw},${t.y}`,
- `C${cx + t.hw},${t.y + dy * k} ${cx + b.hw},${b.y - dy * k} ${cx + b.hw},${b.y}`,
- `L${cx - b.hw},${b.y}`,
- `C${cx - b.hw},${b.y - dy * k} ${cx - t.hw},${t.y + dy * k} ${cx - t.hw},${t.y}`,
- 'Z',
- ].join(' ')
- }
-
- // Full outline for background glow
- const glowPath = useMemo(() => {
- let d = `M${cx - bounds[0].hw},${bounds[0].y} L${cx + bounds[0].hw},${bounds[0].y}`
- for (let i = 0; i < n; i++) {
- const t = bounds[i], b = bounds[i + 1], dy = b.y - t.y
- d += ` C${cx + t.hw},${t.y + dy * k} ${cx + b.hw},${b.y - dy * k} ${cx + b.hw},${b.y}`
- }
- d += ` L${cx - bounds[n].hw},${bounds[n].y}`
- for (let i = n - 1; i >= 0; i--) {
- const t = bounds[i], b = bounds[i + 1], dy = b.y - t.y
- d += ` C${cx - b.hw},${b.y - dy * k} ${cx - t.hw},${t.y + dy * k} ${cx - t.hw},${t.y}`
- }
- return d + ' Z'
- }, [bounds, n])
-
- return (
-
-
-
- )
-}
diff --git a/components/ui/funnel-chart.tsx b/components/ui/funnel-chart.tsx
new file mode 100644
index 0000000..78359da
--- /dev/null
+++ b/components/ui/funnel-chart.tsx
@@ -0,0 +1,935 @@
+"use client";
+
+import { motion, useSpring, useTransform } from "motion/react";
+import {
+ type CSSProperties,
+ type ReactNode,
+ useCallback,
+ useEffect,
+ useRef,
+ useState,
+} from "react";
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+// ─── Utils ───────────────────────────────────────────────────────────────────
+
+function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
+
+// ─── PatternLines ────────────────────────────────────────────────────────────
+
+export interface PatternLinesProps {
+ id: string;
+ width?: number;
+ height?: number;
+ stroke?: string;
+ strokeWidth?: number;
+ orientation?: ("diagonal" | "horizontal" | "vertical")[];
+ background?: string;
+}
+
+export function PatternLines({
+ id,
+ width = 6,
+ height = 6,
+ stroke = "var(--chart-line-primary)",
+ strokeWidth = 1,
+ orientation = ["diagonal"],
+ background,
+}: PatternLinesProps) {
+ const paths: string[] = [];
+
+ for (const o of orientation) {
+ if (o === "diagonal") {
+ paths.push(`M0,${height}l${width},${-height}`);
+ paths.push(`M${-width / 4},${height / 4}l${width / 2},${-height / 2}`);
+ paths.push(
+ `M${(3 * width) / 4},${height + height / 4}l${width / 2},${-height / 2}`
+ );
+ } else if (o === "horizontal") {
+ paths.push(`M0,${height / 2}l${width},0`);
+ } else if (o === "vertical") {
+ paths.push(`M${width / 2},0l0,${height}`);
+ }
+ }
+
+ return (
+
+ {background && (
+
+ )}
+
+
+ );
+}
+
+PatternLines.displayName = "PatternLines";
+
+// ─── Types ───────────────────────────────────────────────────────────────────
+
+export interface FunnelGradientStop {
+ offset: string | number;
+ color: string;
+}
+
+export interface FunnelStage {
+ label: string;
+ value: number;
+ displayValue?: string;
+ color?: string;
+ gradient?: FunnelGradientStop[];
+}
+
+export interface FunnelChartProps {
+ data: FunnelStage[];
+ orientation?: "horizontal" | "vertical";
+ color?: string;
+ layers?: number;
+ className?: string;
+ style?: CSSProperties;
+ showPercentage?: boolean;
+ showValues?: boolean;
+ showLabels?: boolean;
+ hoveredIndex?: number | null;
+ onHoverChange?: (index: number | null) => void;
+ formatPercentage?: (pct: number) => string;
+ formatValue?: (value: number) => string;
+ staggerDelay?: number;
+ gap?: number;
+ renderPattern?: (id: string, color: string) => ReactNode;
+ edges?: "curved" | "straight";
+ labelLayout?: "spread" | "grouped";
+ labelOrientation?: "vertical" | "horizontal";
+ labelAlign?: "center" | "start" | "end";
+ grid?:
+ | boolean
+ | {
+ bands?: boolean;
+ bandColor?: string;
+ lines?: boolean;
+ lineColor?: string;
+ lineOpacity?: number;
+ lineWidth?: number;
+ };
+}
+
+// ─── Defaults ────────────────────────────────────────────────────────────────
+
+const fmtPct = (p: number) => `${Math.round(p)}%`;
+const fmtVal = (v: number) => v.toLocaleString("en-US");
+
+const springConfig = { stiffness: 120, damping: 20, mass: 1 };
+const hoverSpring = { stiffness: 300, damping: 24 };
+
+// ─── SVG Helpers ─────────────────────────────────────────────────────────────
+
+function hSegmentPath(
+ normStart: number,
+ normEnd: number,
+ segW: number,
+ H: number,
+ layerScale: number,
+ straight = false
+) {
+ const my = H / 2;
+ const h0 = normStart * H * 0.44 * layerScale;
+ const h1 = normEnd * H * 0.44 * layerScale;
+
+ if (straight) {
+ return `M 0 ${my - h0} L ${segW} ${my - h1} L ${segW} ${my + h1} L 0 ${my + h0} Z`;
+ }
+
+ const cx = segW * 0.55;
+ const top = `M 0 ${my - h0} C ${cx} ${my - h0}, ${segW - cx} ${my - h1}, ${segW} ${my - h1}`;
+ const bot = `L ${segW} ${my + h1} C ${segW - cx} ${my + h1}, ${cx} ${my + h0}, 0 ${my + h0}`;
+ return `${top} ${bot} Z`;
+}
+
+function vSegmentPath(
+ normStart: number,
+ normEnd: number,
+ segH: number,
+ W: number,
+ layerScale: number,
+ straight = false
+) {
+ const mx = W / 2;
+ const w0 = normStart * W * 0.44 * layerScale;
+ const w1 = normEnd * W * 0.44 * layerScale;
+
+ if (straight) {
+ return `M ${mx - w0} 0 L ${mx - w1} ${segH} L ${mx + w1} ${segH} L ${mx + w0} 0 Z`;
+ }
+
+ const cy = segH * 0.55;
+ const left = `M ${mx - w0} 0 C ${mx - w0} ${cy}, ${mx - w1} ${segH - cy}, ${mx - w1} ${segH}`;
+ const right = `L ${mx + w1} ${segH} C ${mx + w1} ${segH - cy}, ${mx + w0} ${cy}, ${mx + w0} 0`;
+ return `${left} ${right} Z`;
+}
+
+// ─── Animated Ring ───────────────────────────────────────────────────────────
+
+function HRing({
+ d,
+ color,
+ fill,
+ opacity,
+ hovered,
+ ringIndex,
+ totalRings,
+}: {
+ d: string;
+ color: string;
+ fill?: string;
+ opacity: number;
+ hovered: boolean;
+ ringIndex: number;
+ totalRings: number;
+}) {
+ const extraScale = 1 + (ringIndex / Math.max(totalRings - 1, 1)) * 0.12;
+ const ringSpring = {
+ stiffness: 300 - ringIndex * 60,
+ damping: 24 - ringIndex * 3,
+ };
+ const scaleY = useSpring(1, ringSpring);
+
+ useEffect(() => {
+ scaleY.set(hovered ? extraScale : 1);
+ }, [hovered, scaleY, extraScale]);
+
+ return (
+
+ );
+}
+
+function VRing({
+ d,
+ color,
+ fill,
+ opacity,
+ hovered,
+ ringIndex,
+ totalRings,
+}: {
+ d: string;
+ color: string;
+ fill?: string;
+ opacity: number;
+ hovered: boolean;
+ ringIndex: number;
+ totalRings: number;
+}) {
+ const extraScale = 1 + (ringIndex / Math.max(totalRings - 1, 1)) * 0.12;
+ const ringSpring = {
+ stiffness: 300 - ringIndex * 60,
+ damping: 24 - ringIndex * 3,
+ };
+ const scaleX = useSpring(1, ringSpring);
+
+ useEffect(() => {
+ scaleX.set(hovered ? extraScale : 1);
+ }, [hovered, scaleX, extraScale]);
+
+ return (
+
+ );
+}
+
+// ─── Animated Segments ───────────────────────────────────────────────────────
+
+function HSegment({
+ index,
+ normStart,
+ normEnd,
+ segW,
+ fullH,
+ color,
+ layers,
+ staggerDelay,
+ hovered,
+ dimmed,
+ renderPattern,
+ straight,
+ gradientStops,
+}: {
+ index: number;
+ normStart: number;
+ normEnd: number;
+ segW: number;
+ fullH: number;
+ color: string;
+ layers: number;
+ staggerDelay: number;
+ hovered: boolean;
+ dimmed: boolean;
+ renderPattern?: (id: string, color: string) => ReactNode;
+ straight: boolean;
+ gradientStops?: FunnelGradientStop[];
+}) {
+ const patternId = `funnel-h-pattern-${index}`;
+ const gradientId = `funnel-h-grad-${index}`;
+ const growProgress = useSpring(0, springConfig);
+ const entranceScaleX = useTransform(growProgress, [0, 1], [0, 1]);
+ const entranceScaleY = useTransform(growProgress, [0, 1], [0, 1]);
+ const dimOpacity = useSpring(1, hoverSpring);
+
+ useEffect(() => {
+ dimOpacity.set(dimmed ? 0.4 : 1);
+ }, [dimmed, dimOpacity]);
+
+ useEffect(() => {
+ const timeout = setTimeout(
+ () => growProgress.set(1),
+ index * staggerDelay * 1000
+ );
+ return () => clearTimeout(timeout);
+ }, [growProgress, index, staggerDelay]);
+
+ const rings = Array.from({ length: layers }, (_, l) => {
+ const scale = 1 - (l / layers) * 0.35;
+ const opacity = 0.18 + (l / (layers - 1 || 1)) * 0.65;
+ return {
+ d: hSegmentPath(normStart, normEnd, segW, fullH, scale, straight),
+ opacity,
+ };
+ });
+
+ return (
+
+
+
+
+
+ );
+}
+
+function VSegment({
+ index,
+ normStart,
+ normEnd,
+ segH,
+ fullW,
+ color,
+ layers,
+ staggerDelay,
+ hovered,
+ dimmed,
+ renderPattern,
+ straight,
+ gradientStops,
+}: {
+ index: number;
+ normStart: number;
+ normEnd: number;
+ segH: number;
+ fullW: number;
+ color: string;
+ layers: number;
+ staggerDelay: number;
+ hovered: boolean;
+ dimmed: boolean;
+ renderPattern?: (id: string, color: string) => ReactNode;
+ straight: boolean;
+ gradientStops?: FunnelGradientStop[];
+}) {
+ const patternId = `funnel-v-pattern-${index}`;
+ const gradientId = `funnel-v-grad-${index}`;
+ const growProgress = useSpring(0, springConfig);
+ const entranceScaleY = useTransform(growProgress, [0, 1], [0, 1]);
+ const entranceScaleX = useTransform(growProgress, [0, 1], [0, 1]);
+ const dimOpacity = useSpring(1, hoverSpring);
+
+ useEffect(() => {
+ dimOpacity.set(dimmed ? 0.4 : 1);
+ }, [dimmed, dimOpacity]);
+
+ useEffect(() => {
+ const timeout = setTimeout(
+ () => growProgress.set(1),
+ index * staggerDelay * 1000
+ );
+ return () => clearTimeout(timeout);
+ }, [growProgress, index, staggerDelay]);
+
+ const rings = Array.from({ length: layers }, (_, l) => {
+ const scale = 1 - (l / layers) * 0.35;
+ const opacity = 0.18 + (l / (layers - 1 || 1)) * 0.65;
+ return {
+ d: vSegmentPath(normStart, normEnd, segH, fullW, scale, straight),
+ opacity,
+ };
+ });
+
+ return (
+
+
+
+
+
+ );
+}
+
+// ─── Label Overlay ───────────────────────────────────────────────────────────
+
+function SegmentLabel({
+ stage,
+ pct,
+ isHorizontal,
+ showValues,
+ showPercentage,
+ showLabels,
+ formatPercentage,
+ formatValue,
+ index,
+ staggerDelay,
+ layout = "spread",
+ orientation,
+ align = "center",
+}: {
+ stage: FunnelStage;
+ pct: number;
+ isHorizontal: boolean;
+ showValues: boolean;
+ showPercentage: boolean;
+ showLabels: boolean;
+ formatPercentage: (p: number) => string;
+ formatValue: (v: number) => string;
+ index: number;
+ staggerDelay: number;
+ layout?: "spread" | "grouped";
+ orientation?: "vertical" | "horizontal";
+ align?: "center" | "start" | "end";
+}) {
+ const display = stage.displayValue ?? formatValue(stage.value);
+
+ const valueEl = showValues && (
+
+ {display}
+
+ );
+ const pctEl = showPercentage && (
+
+ {formatPercentage(pct)}
+
+ );
+ const labelEl = showLabels && (
+
+ {stage.label}
+
+ );
+
+ if (layout === "spread") {
+ return (
+
+ {isHorizontal ? (
+ <>
+
+ {valueEl}
+
+
+ {pctEl}
+
+
+ {labelEl}
+
+ >
+ ) : (
+ <>
+
+ {valueEl}
+
+
+ {pctEl}
+
+
+ {labelEl}
+
+ >
+ )}
+
+ );
+ }
+
+ // Grouped layout
+ const resolvedOrientation =
+ orientation ?? (isHorizontal ? "vertical" : "horizontal");
+ const isVerticalStack = resolvedOrientation === "vertical";
+
+ const justifyMap = {
+ start: "justify-start",
+ center: "justify-center",
+ end: "justify-end",
+ } as const;
+ const itemsMap = {
+ start: "items-start",
+ center: "items-center",
+ end: "items-end",
+ } as const;
+
+ return (
+
+
+ {valueEl}
+ {pctEl}
+ {labelEl}
+
+
+ );
+}
+
+// ─── FunnelChart ─────────────────────────────────────────────────────────────
+
+export function FunnelChart({
+ data,
+ orientation = "horizontal",
+ color = "var(--chart-1)",
+ layers = 3,
+ className,
+ style,
+ showPercentage = true,
+ showValues = true,
+ showLabels = true,
+ hoveredIndex: hoveredIndexProp,
+ onHoverChange,
+ formatPercentage = fmtPct,
+ formatValue = fmtVal,
+ staggerDelay = 0.12,
+ gap = 4,
+ renderPattern,
+ edges = "curved",
+ labelLayout = "spread",
+ labelOrientation,
+ labelAlign = "center",
+ grid: gridProp = false,
+}: FunnelChartProps) {
+ const ref = useRef(null);
+ const [sz, setSz] = useState({ w: 0, h: 0 });
+ const [internalHoveredIndex, setInternalHoveredIndex] = useState<
+ number | null
+ >(null);
+
+ const isControlled = hoveredIndexProp !== undefined;
+ const hoveredIndex = isControlled ? hoveredIndexProp : internalHoveredIndex;
+ const setHoveredIndex = useCallback(
+ (index: number | null) => {
+ if (isControlled) {
+ onHoverChange?.(index);
+ } else {
+ setInternalHoveredIndex(index);
+ }
+ },
+ [isControlled, onHoverChange]
+ );
+
+ const measure = useCallback(() => {
+ if (!ref.current) return;
+ const { width: w, height: h } = ref.current.getBoundingClientRect();
+ if (w > 0 && h > 0) setSz({ w, h });
+ }, []);
+
+ useEffect(() => {
+ measure();
+ const ro = new ResizeObserver(measure);
+ if (ref.current) ro.observe(ref.current);
+ return () => ro.disconnect();
+ }, [measure]);
+
+ if (!data.length) return null;
+
+ const first = data[0];
+ if (!first) return null;
+
+ const max = first.value;
+ const n = data.length;
+ const norms = data.map((d) => d.value / max);
+ const horiz = orientation === "horizontal";
+ const { w: W, h: H } = sz;
+
+ const totalGap = gap * (n - 1);
+ const segW = (W - (horiz ? totalGap : 0)) / n;
+ const segH = (H - (horiz ? 0 : totalGap)) / n;
+
+ // Grid config
+ const gridEnabled = gridProp !== false;
+ const gridCfg = typeof gridProp === "object" ? gridProp : {};
+ const showBands = gridEnabled && (gridCfg.bands ?? true);
+ const bandColor = gridCfg.bandColor ?? "var(--color-muted)";
+ const showGridLines = gridEnabled && (gridCfg.lines ?? true);
+ const gridLineColor = gridCfg.lineColor ?? "var(--chart-grid)";
+ const gridLineOpacity = gridCfg.lineOpacity ?? 1;
+ const gridLineWidth = gridCfg.lineWidth ?? 1;
+
+ return (
+
+ {W > 0 && H > 0 && (
+ <>
+ {/* Grid background bands */}
+ {gridEnabled && (
+
+ )}
+
+ {/* Segments */}
+
+ {data.map((stage, i) => {
+ const normStart = norms[i] ?? 0;
+ const normEnd = norms[Math.min(i + 1, n - 1)] ?? 0;
+ const firstStop = stage.gradient?.[0];
+ const segColor = firstStop
+ ? firstStop.color
+ : (stage.color ?? color);
+
+ return horiz ? (
+
+ ) : (
+
+ );
+ })}
+
+
+ {/* Grid lines */}
+ {gridEnabled && showGridLines && (
+
+ )}
+
+ {/* Label overlays — hover triggers */}
+ {data.map((stage, i) => {
+ const pct = (stage.value / max) * 100;
+ const posStyle: CSSProperties = horiz
+ ? { left: (segW + gap) * i, width: segW, top: 0, height: H }
+ : { top: (segH + gap) * i, height: segH, left: 0, width: W };
+ const isDimmed = hoveredIndex !== null && hoveredIndex !== i;
+
+ return (
+
setHoveredIndex(i)}
+ onMouseLeave={() => setHoveredIndex(null)}
+ style={{ ...posStyle, zIndex: 20 }}
+ transition={{ type: "spring", stiffness: 300, damping: 24 }}
+ >
+
+
+ );
+ })}
+ >
+ )}
+
+ );
+}
+
+FunnelChart.displayName = "FunnelChart";
+
+export default FunnelChart;
diff --git a/package-lock.json b/package-lock.json
index 8600b6b..6d307ce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -24,6 +24,7 @@
"iso-3166-2": "^1.0.0",
"jspdf": "^4.0.0",
"jspdf-autotable": "^5.0.7",
+ "motion": "^12.35.2",
"next": "^16.1.1",
"radix-ui": "^1.4.3",
"react": "^19.2.3",
@@ -9277,12 +9278,12 @@
}
},
"node_modules/framer-motion": {
- "version": "12.34.1",
- "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.1.tgz",
- "integrity": "sha512-kcZyNaYQfvE2LlH6+AyOaJAQV4rGp5XbzfhsZpiSZcwDMfZUHhuxLWeyRzf5I7jip3qKRpuimPA9pXXfr111kQ==",
+ "version": "12.35.2",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.35.2.tgz",
+ "integrity": "sha512-dhfuEMaNo0hc+AEqyHiIfiJRNb9U9UQutE9FoKm5pjf7CMitp9xPEF1iWZihR1q86LBmo6EJ7S8cN8QXEy49AA==",
"license": "MIT",
"dependencies": {
- "motion-dom": "^12.34.1",
+ "motion-dom": "^12.35.2",
"motion-utils": "^12.29.2",
"tslib": "^2.4.0"
},
@@ -11566,10 +11567,36 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/motion": {
+ "version": "12.35.2",
+ "resolved": "https://registry.npmjs.org/motion/-/motion-12.35.2.tgz",
+ "integrity": "sha512-8zCi1DkNyU6a/tgEHn/GnnXZDcaMpDHbDOGORY1Rg/6lcNMSOuvwDB3i4hMSOvxqMWArc/vrGaw/Xek1OP69/A==",
+ "license": "MIT",
+ "dependencies": {
+ "framer-motion": "^12.35.2",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/motion-dom": {
- "version": "12.34.1",
- "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.1.tgz",
- "integrity": "sha512-SC7ZC5dRcGwku2g7EsPvI4q/EzHumUbqsDNumBmZTLFg+goBO5LTJvDu9MAxx+0mtX4IA78B2be/A3aRjY0jnw==",
+ "version": "12.35.2",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.35.2.tgz",
+ "integrity": "sha512-pWXFMTwvGDbx1Fe9YL5HZebv2NhvGBzRtiNUv58aoK7+XrsuaydQ0JGRKK2r+bTKlwgSWwWxHbP5249Qr/BNpg==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.29.2"
diff --git a/package.json b/package.json
index 7017e87..917e3f4 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"iso-3166-2": "^1.0.0",
"jspdf": "^4.0.0",
"jspdf-autotable": "^5.0.7",
+ "motion": "^12.35.2",
"next": "^16.1.1",
"radix-ui": "^1.4.3",
"react": "^19.2.3",