{ "name": "timeline-rail", "type": "registry:ui", "registryDependencies": [], "dependencies": [], "devDependencies": [], "tailwind": {}, "cssVars": { "light": {}, "dark": {} }, "files": [ { "path": "timeline-rail.tsx", "content": "'use client';\n\nimport * as React from 'react';\nimport { cn } from '@/lib/utils';\n\ntype AnchorOrButton =\n | ({\n href: string;\n onClick?: never;\n } & React.AnchorHTMLAttributes)\n | ({\n href?: undefined;\n onClick?: React.MouseEventHandler;\n } & React.ButtonHTMLAttributes);\n\nexport type TimelineItem = {\n /** Main small label shown near the dot (e.g., “headset”) */\n label?: string;\n /** Secondary label shown under the rail (e.g., “1910”) */\n caption?: string;\n /** Is this item emphasized (darker dot/rail before it) */\n active?: boolean;\n} & AnchorOrButton;\n\nexport type TimelineRailProps = {\n /** Items in left→right order */\n items: TimelineItem[];\n\n /** Size density (dot size + vertical offsets) */\n size?: 'sm' | 'md';\n\n /** Visually emphasize the rail up to the last `active` item */\n emphasizeActiveTrail?: boolean;\n\n /** Angle for the top labels (deg). use 0 for straight text. */\n labelAngle?: number;\n\n /** Layout spacing between dots (Tailwind gap classes allowed). */\n gapClassName?: string;\n\n /** Visual customization */\n lineColorClass?: string; // rail color\n lineThickness?: number; // pixels\n dotClass?: string; // dot color/class\n dotActiveClass?: string; // active dot color/class\n\n /** Slots for precise control */\n className?: string; // root\n railClassName?: string; // the line element\n itemClassName?: string; //
  • \n labelClassName?: string; // top tiny label\n captionClassName?: string; // bottom caption\n\n /** Optional renderers to fully control content */\n renderLabel?: (item: TimelineItem, index: number) => React.ReactNode;\n renderCaption?: (item: TimelineItem, index: number) => React.ReactNode;\n};\n\n/** SRP: purely visual timeline rail. No data fetching or state kept here. */\nexport default function TimelineRail({\n items,\n size = 'md',\n emphasizeActiveTrail = true,\n labelAngle = 45,\n gapClassName = 'gap-14',\n lineColorClass = 'bg-zinc-300 dark:bg-zinc-700',\n lineThickness = 6,\n dotClass = 'bg-zinc-400 dark:bg-zinc-600',\n dotActiveClass = 'bg-zinc-900 dark:bg-zinc-100',\n className,\n railClassName,\n itemClassName,\n labelClassName,\n captionClassName,\n renderLabel,\n renderCaption,\n}: TimelineRailProps) {\n // compute last active index (for emphasized rail)\n const lastActive = React.useMemo(() => {\n let idx = -1;\n items.forEach((it, i) => {\n if (it.active) idx = i;\n });\n return idx;\n }, [items]);\n\n const dotSize = size === 'sm' ? 14 : 18;\n const topOffset = size === 'sm' ? -22 : -26; // label y\n const captionOffset = size === 'sm' ? 18 : 22; // caption y\n\n return (\n
    \n {/* Rail */}\n \n
    \n\n {/* Emphasized segment up to last active */}\n {emphasizeActiveTrail && lastActive >= 0 && (\n 1 ? (lastActive / (items.length - 1)) * 100 : 0}%`,\n }}\n />\n )}\n
    \n\n {/* Dots row */}\n \n {items.map((item, i) => {\n const isActive = !!item.active;\n return (\n \n {/* diagonal label above */}\n {item.label && (\n \n {renderLabel ? renderLabel(item, i) : item.label}\n \n )}\n\n {/* dot */}\n {item.href ? (\n \n ) : (\n \n )}\n\n {/* caption below */}\n {item.caption && (\n \n {renderCaption ? renderCaption(item, i) : item.caption}\n \n )}\n
  • \n );\n })}\n \n \n );\n}\n", "type": "registry:ui" } ] }