{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "pipeline-journey", "title": "Pipeline Journey", "description": "A Kanban ticket arcs across columns triggering landings and a final confetti burst.", "dependencies": [ "remotion" ], "files": [ { "path": "registry/remocn/pipeline-journey/index.tsx", "content": "\"use client\";\n\nimport { interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\n\nexport interface PipelineJourneyProps {\n cardLabel?: string;\n accentColor?: string;\n speed?: number;\n className?: string;\n}\n\nconst FONT_FAMILY =\n \"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif\";\nconst MONO_FAMILY =\n \"var(--font-geist-mono), ui-monospace, SFMono-Regular, Menlo, monospace\";\n\nconst COLUMNS = [\n { id: \"todo\", title: \"Todo\", count: 3 },\n { id: \"in_progress\", title: \"In Progress\", count: 2 },\n { id: \"done\", title: \"Done\", count: 4 },\n];\n\nconst COL_WIDTH = 320;\nconst COL_GAP = 40;\nconst BOARD_TOP = 120;\nconst BOARD_LEFT = 120;\nconst CARD_W = 280;\nconst CARD_H = 88;\n\nfunction colCenter(index: number) {\n const x = BOARD_LEFT + index * (COL_WIDTH + COL_GAP) + COL_WIDTH / 2;\n const y = BOARD_TOP + 90;\n return { x, y };\n}\n\nfunction pseudoRandom(i: number) {\n const v = Math.sin(i * 12.9898) * 43758.5453;\n return v - Math.floor(v);\n}\n\nfunction PlaceholderCard({ opacity = 1 }: { opacity?: number }) {\n return (\n \n \n \n
\n \n
\n );\n}\n\nexport function PipelineJourney({\n cardLabel = \"Build pipeline\",\n accentColor = \"#22c55e\",\n speed = 1,\n className,\n}: PipelineJourneyProps) {\n const frame = useCurrentFrame() * speed;\n const { fps } = useVideoConfig();\n\n // Phases\n const flight1Start = 30;\n const flight1End = 70;\n const waitEnd = 110;\n const flight2Start = 110;\n const flight2End = 150;\n const confettiStart = 150;\n\n const todoCenter = colCenter(0);\n const progCenter = colCenter(1);\n const doneCenter = colCenter(2);\n\n // Card position interpolation\n let cx: number;\n let cy: number;\n let rotZ = 0;\n let scaleBoost = 1;\n let shadowAlpha = 0.25;\n let shadowBlur = 20;\n\n if (frame < flight1Start) {\n cx = todoCenter.x;\n cy = todoCenter.y;\n } else if (frame < flight1End) {\n const p = (frame - flight1Start) / (flight1End - flight1Start);\n cx = interpolate(p, [0, 1], [todoCenter.x, progCenter.x]);\n // arc\n const arc = Math.sin(p * Math.PI) * 80;\n cy = interpolate(p, [0, 1], [todoCenter.y, progCenter.y]) - arc;\n rotZ = Math.sin(p * Math.PI) * 3;\n scaleBoost = 1 + Math.sin(p * Math.PI) * 0.15;\n shadowAlpha = 0.25 + Math.sin(p * Math.PI) * 0.4;\n shadowBlur = 20 + Math.sin(p * Math.PI) * 60;\n } else if (frame < flight2Start) {\n cx = progCenter.x;\n cy = progCenter.y;\n } else if (frame < flight2End) {\n const p = (frame - flight2Start) / (flight2End - flight2Start);\n cx = interpolate(p, [0, 1], [progCenter.x, doneCenter.x]);\n const arc = Math.sin(p * Math.PI) * 80;\n cy = interpolate(p, [0, 1], [progCenter.y, doneCenter.y]) - arc;\n rotZ = Math.sin(p * Math.PI) * 3;\n scaleBoost = 1 + Math.sin(p * Math.PI) * 0.15;\n shadowAlpha = 0.25 + Math.sin(p * Math.PI) * 0.4;\n shadowBlur = 20 + Math.sin(p * Math.PI) * 60;\n } else {\n cx = doneCenter.x;\n cy = doneCenter.y;\n }\n\n // Landing micro-spring on each arrival\n const land1 = spring({\n frame: frame - flight1End,\n fps,\n config: { damping: 10, stiffness: 180, mass: 0.7 },\n durationInFrames: 18,\n });\n const land2 = spring({\n frame: frame - flight2End,\n fps,\n config: { damping: 10, stiffness: 180, mass: 0.7 },\n durationInFrames: 18,\n });\n\n let landingScale = 1;\n if (frame >= flight1End && frame < flight2Start) {\n landingScale = interpolate(land1, [0, 1], [0.92, 1]);\n } else if (frame >= flight2End) {\n landingScale = interpolate(land2, [0, 1], [0.92, 1]);\n }\n\n // Column flash on landing\n const progFlash = interpolate(\n frame,\n [flight1End, flight1End + 4, flight1End + 18],\n [0, 1, 0],\n { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" },\n );\n const doneFlash = interpolate(\n frame,\n [flight2End, flight2End + 4, flight2End + 18],\n [0, 1, 0],\n { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" },\n );\n\n // Timer visibility (during in_progress wait)\n const timerOpacity = interpolate(\n frame,\n [flight1End + 4, flight1End + 12, waitEnd - 8, waitEnd],\n [0, 1, 1, 0],\n { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" },\n );\n const timerSecs = Math.floor(\n interpolate(frame, [flight1End, waitEnd], [0, 42], {\n extrapolateLeft: \"clamp\",\n extrapolateRight: \"clamp\",\n }),\n );\n\n // Confetti particles\n const PARTICLE_COUNT = 36;\n const confettiFrame = frame - confettiStart;\n\n return (\n \n {/* Subtle grid bg */}\n \n\n {/* Title */}\n \n Sprint board\n \n\n {/* Columns */}\n {COLUMNS.map((col, i) => {\n const flash =\n col.id === \"in_progress\"\n ? progFlash\n : col.id === \"done\"\n ? doneFlash\n : 0;\n return (\n \n \n
\n {col.title}\n
\n \n {col.count}\n \n \n {Array.from({ length: col.count }).map((_, k) => (\n \n ))}\n \n );\n })}\n\n {/* Flying card */}\n \n \n \n REMO-128\n \n \n \n \n {cardLabel}\n \n \n \n 00:{String(timerSecs).padStart(2, \"0\")}\n \n \n assignee\n \n \n \n\n {/* Confetti */}\n {confettiFrame > 0 &&\n Array.from({ length: PARTICLE_COUNT }).map((_, i) => {\n const angle = pseudoRandom(i + 1) * Math.PI * 2;\n const dist = 60 + pseudoRandom(i + 99) * 220;\n const drift = pseudoRandom(i + 7) * 40 - 20;\n const sizeR = pseudoRandom(i + 3);\n const size = 6 + sizeR * 8;\n const colorPick = pseudoRandom(i + 5);\n const color =\n colorPick < 0.33\n ? accentColor\n : colorPick < 0.66\n ? \"#fbbf24\"\n : \"#60a5fa\";\n const lifespan = 50;\n const t = Math.min(confettiFrame / lifespan, 1);\n const eased = 1 - (1 - t) * (1 - t);\n const px = doneCenter.x + Math.cos(angle) * dist * eased + drift * t;\n const py =\n doneCenter.y +\n Math.sin(angle) * dist * eased +\n // gravity\n t * t * 220;\n const opacity = interpolate(t, [0, 0.7, 1], [1, 1, 0]);\n const rot = pseudoRandom(i + 11) * 360 + confettiFrame * 12;\n return (\n \n );\n })}\n \n );\n}\n", "type": "registry:component", "target": "components/remocn/pipeline-journey.tsx" } ], "type": "registry:component" }