{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "timeline", "type": "registry:component", "title": "Vertical timeline", "description": "Three-axis state story: solid past, dotted future, donut current; terminal failure is never a green check.", "registryDependencies": [ "@venlyfinance/venly-tokens" ], "files": [ { "path": "registry/components/timeline.tsx", "type": "registry:component", "target": "~/components/venly/components/timeline.tsx", "content": "import type { CSSProperties, ReactElement, ReactNode } from \"react\";\n\n/**\n * Vertical timeline – status story for one money movement.\n *\n * Design contract encoded by this component:\n * - State is distinguished on three axes at once: node, rail below, label.\n * - Solid rail = past, dotted rail = future; the switch happens at the\n * current node. Never inverted.\n * - Current node = accent donut + bold label. Future = hollow node, grey\n * regular label.\n * - Terminal failure/cancellation: the node goes neutral grey or danger,\n * NEVER a success-green check – a green check on a cancelled step makes\n * the timeline read \"all good\" at a glance (the most dangerous timeline\n * error observed in the reference corpus).\n */\nexport type TimelineStepState = \"completed\" | \"current\" | \"future\" | \"failed\" | \"cancelled\";\n\nexport interface TimelineStep {\n key: string;\n label: ReactNode;\n /** Completion word, timestamp, or estimate. */\n meta?: ReactNode;\n state: TimelineStepState;\n}\n\nexport interface TimelineProps {\n steps: TimelineStep[];\n style?: CSSProperties;\n className?: string;\n}\n\nconst NODE_SIZE = \"var(--timeline-node)\";\n\nfunction Node({ state }: { state: TimelineStepState }): ReactElement {\n const base: CSSProperties = {\n width: NODE_SIZE,\n height: NODE_SIZE,\n borderRadius: \"50%\",\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n fontSize: \"calc(var(--timeline-node) * 0.625)\",\n lineHeight: 1,\n flex: \"none\",\n };\n switch (state) {\n case \"completed\":\n return (\n \n ✓\n \n );\n case \"current\":\n return (\n \n \n \n );\n case \"failed\":\n return (\n \n ✕\n \n );\n case \"cancelled\":\n return (\n \n ↺\n \n );\n case \"future\":\n return (\n \n );\n }\n}\n\nexport function Timeline({ steps, style, className }: TimelineProps): ReactElement {\n return (\n \n {steps.map((step, i) => {\n const isLast = i === steps.length - 1;\n // The rail below a node is solid while we are in the past, dotted\n // once the story crosses the current node into the future.\n const railFuture =\n step.state === \"future\" ||\n step.state === \"current\" ||\n step.state === \"failed\" ||\n step.state === \"cancelled\";\n const labelColor =\n step.state === \"future\"\n ? \"var(--text-tertiary)\"\n : step.state === \"cancelled\"\n ? \"var(--text-secondary)\"\n : \"var(--text-primary)\";\n return (\n
  • \n \n \n {!isLast ? (\n \n ) : null}\n \n \n \n {step.label}\n \n {step.meta !== undefined ? (\n \n {step.meta}\n \n ) : null}\n \n
  • \n );\n })}\n \n );\n}\n" } ] }