{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "animated-line-chart", "title": "Animated Line Chart", "description": "Line chart whose SVG path draws on from left to right with a leading dot.", "dependencies": [ "remotion" ], "files": [ { "path": "registry/remocn/animated-line-chart/index.tsx", "content": "\"use client\";\n\nimport { spring, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport interface AnimatedLineChartProps {\n data?: number[];\n width?: number;\n height?: number;\n strokeColor?: string;\n strokeWidth?: number;\n background?: string;\n gridColor?: string;\n showDot?: boolean;\n speed?: number;\n className?: string;\n}\n\nexport function AnimatedLineChart({\n data = [12, 19, 8, 15, 22, 18, 28, 25, 32],\n width = 1000,\n height = 500,\n strokeColor = \"#22c55e\",\n strokeWidth = 4,\n background = \"#0a0a0a\",\n gridColor = \"#27272a\",\n showDot = true,\n speed = 1,\n className,\n}: AnimatedLineChartProps) {\n const frame = useCurrentFrame() * speed;\n const { fps, durationInFrames } = useVideoConfig();\n\n const padding = 60;\n const innerWidth = width - padding * 2;\n const innerHeight = height - padding * 2;\n\n const min = Math.min(...data);\n const max = Math.max(...data);\n const range = max - min || 1;\n\n const points = data.map((value, index) => {\n const x = padding + (index / (data.length - 1)) * innerWidth;\n const y = padding + innerHeight - ((value - min) / range) * innerHeight;\n return { x, y };\n });\n\n // Analytical path length: sum of segment distances\n let pathLength = 0;\n for (let i = 1; i < points.length; i++) {\n const dx = points[i].x - points[i - 1].x;\n const dy = points[i].y - points[i - 1].y;\n pathLength += Math.sqrt(dx * dx + dy * dy);\n }\n\n const d = points\n .map((p, i) => `${i === 0 ? \"M\" : \"L\"}${p.x.toFixed(2)},${p.y.toFixed(2)}`)\n .join(\" \");\n\n // Spring-driven progress: smooth, no bounce. Matches the \"smooth\" preset\n // from remotion-best-practices/timing.md.\n const progress = spring({\n frame,\n fps,\n durationInFrames: Math.round(durationInFrames * 0.85),\n config: { damping: 200 },\n });\n\n const dashOffset = pathLength * (1 - progress);\n\n // Find dot position along the path at current progress\n const targetLen = pathLength * progress;\n let traveled = 0;\n let dotX = points[0].x;\n let dotY = points[0].y;\n for (let i = 1; i < points.length; i++) {\n const dx = points[i].x - points[i - 1].x;\n const dy = points[i].y - points[i - 1].y;\n const segLen = Math.sqrt(dx * dx + dy * dy);\n if (traveled + segLen >= targetLen) {\n const t = (targetLen - traveled) / segLen;\n dotX = points[i - 1].x + dx * t;\n dotY = points[i - 1].y + dy * t;\n break;\n }\n traveled += segLen;\n dotX = points[i].x;\n dotY = points[i].y;\n }\n\n const gridRows = 4;\n const gridCols = data.length - 1;\n\n return (\n \n \n {/* Horizontal grid */}\n {Array.from({ length: gridRows + 1 }).map((_, i) => {\n const y = padding + (i / gridRows) * innerHeight;\n return (\n \n );\n })}\n {/* Vertical grid */}\n {Array.from({ length: gridCols + 1 }).map((_, i) => {\n const x = padding + (i / gridCols) * innerWidth;\n return (\n \n );\n })}\n {/* Axis */}\n \n \n\n {/* Animated line */}\n \n\n {showDot && progress > 0 && progress < 1 && (\n \n )}\n \n \n );\n}\n", "type": "registry:component", "target": "components/remocn/animated-line-chart.tsx" } ], "type": "registry:component" }