{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"timeline","type":"registry:ui","title":"Timeline","description":"Timeline from react-fancy","package":"react-fancy","dependencies":[],"registryDependencies":[],"files":[{"path":"components\/fancy\/timeline\/Timeline.context.ts","content":"import { createContext, useContext } from \"react\";\r\nimport type { TimelineVariant } from \".\/Timeline.types\";\r\n\r\ninterface TimelineContextValue {\r\n  variant: TimelineVariant;\r\n  index: number;\r\n  total: number;\r\n  animated: boolean;\r\n}\r\n\r\nexport const TimelineContext = createContext<TimelineContextValue>({\r\n  variant: \"stacked\",\r\n  index: 0,\r\n  total: 0,\r\n  animated: true,\r\n});\r\n\r\nexport function useTimeline() {\r\n  return useContext(TimelineContext);\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/timeline\/Timeline.context.ts"},{"path":"components\/fancy\/timeline\/Timeline.tsx","content":"import { Children, forwardRef, useCallback, useRef, type WheelEvent } from \"react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { TimelineContext } from \".\/Timeline.context\";\r\nimport { TimelineItem } from \".\/TimelineItem\";\r\nimport { TimelineBlock } from \".\/TimelineBlock\";\r\nimport type { TimelineProps, TimelineVariant } from \".\/Timeline.types\";\r\n\r\nconst TimelineRoot = forwardRef<HTMLDivElement, TimelineProps>(\r\n  ({\r\n    children,\r\n    variant: variantProp,\r\n    orientation,\r\n    events,\r\n    heading,\r\n    description,\r\n    animated = true,\r\n    className,\r\n  }, ref) => {\r\n    const scrollRef = useRef<HTMLDivElement>(null);\r\n\r\n    \/\/ Backward compat: map deprecated orientation to variant\r\n    let variant: TimelineVariant = variantProp ?? \"stacked\";\r\n    if (!variantProp && orientation) {\r\n      variant = orientation === \"horizontal\" ? \"horizontal\" : \"stacked\";\r\n    }\r\n\r\n    const isHorizontal = variant === \"horizontal\";\r\n    const isAlternating = variant === \"alternating\";\r\n\r\n    \/\/ Build items from events prop (data-driven) or children (compound)\r\n    const items = events\r\n      ? events.map((e, i) => (\r\n          <TimelineItem key={i} date={e.date} emoji={e.emoji} icon={e.icon} color={e.color}>\r\n            {e.title && <h3 className=\"font-semibold text-zinc-900 dark:text-white\">{e.title}<\/h3>}\r\n            {e.description && <div className=\"mt-1 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400\">{e.description}<\/div>}\r\n          <\/TimelineItem>\r\n        ))\r\n      : Children.toArray(children);\r\n\r\n    const handleWheel = useCallback((e: WheelEvent) => {\r\n      if (!scrollRef.current) return;\r\n      e.preventDefault();\r\n      scrollRef.current.scrollLeft += e.deltaY;\r\n    }, []);\r\n\r\n    const content = items.map((child, i) => (\r\n      <TimelineContext.Provider key={i} value={{ variant, index: i, total: items.length, animated }}>\r\n        {child}\r\n      <\/TimelineContext.Provider>\r\n    ));\r\n\r\n    return (\r\n      <div ref={ref} data-react-fancy-timeline=\"\" data-variant={variant} className={className}>\r\n        {(heading || description) && (\r\n          <div className=\"mb-8\">\r\n            {heading && <h2 className=\"text-xl font-semibold text-zinc-900 dark:text-white\">{heading}<\/h2>}\r\n            {description && <p className=\"mt-1 text-sm text-zinc-500 dark:text-zinc-400\">{description}<\/p>}\r\n          <\/div>\r\n        )}\r\n\r\n        {isHorizontal ? (\r\n          <div\r\n            ref={scrollRef}\r\n            className=\"overflow-x-auto pb-4 -mb-4\"\r\n            style={{ scrollbarWidth: \"thin\", scrollbarColor: \"rgb(161 161 170) transparent\" }}\r\n            onWheel={handleWheel}\r\n          >\r\n            <div className=\"flex min-w-max items-start\">\r\n              {content}\r\n            <\/div>\r\n          <\/div>\r\n        ) : (\r\n          \/* Vertical variants: continuous background line behind all events *\/\r\n          <div className=\"relative\">\r\n            {items.length > 1 && (\r\n              <div className={cn(\r\n                \"absolute top-0 bottom-0 w-px bg-zinc-200 dark:bg-zinc-700\",\r\n                isAlternating\r\n                  ? \"left-4 md:left-1\/2 md:-translate-x-px\"\r\n                  : \"left-4\",\r\n              )} \/>\r\n            )}\r\n            {content}\r\n          <\/div>\r\n        )}\r\n      <\/div>\r\n    );\r\n  },\r\n);\r\n\r\nTimelineRoot.displayName = \"Timeline\";\r\n\r\nexport const Timeline = Object.assign(TimelineRoot, {\r\n  Item: TimelineItem,\r\n  Block: TimelineBlock,\r\n});\r\n","type":"registry:ui","target":"components\/fancy\/timeline\/Timeline.tsx"},{"path":"components\/fancy\/timeline\/Timeline.types.ts","content":"import type { ReactNode } from \"react\";\r\nimport type { Color } from \"..\/..\/utils\/types\";\r\n\r\nexport type TimelineVariant = \"stacked\" | \"alternating\" | \"horizontal\";\r\n\r\n\/** @deprecated Use TimelineVariant instead *\/\r\nexport type TimelineOrientation = \"vertical\" | \"horizontal\";\r\n\r\nexport interface TimelineEvent {\r\n  \/** Free-form date string *\/\r\n  date?: string;\r\n  \/** Event heading *\/\r\n  title: string;\r\n  \/** Body text (rendered as children) *\/\r\n  description?: string;\r\n  \/** Emoji character for the dot *\/\r\n  emoji?: string;\r\n  \/** Custom icon ReactNode for the dot *\/\r\n  icon?: ReactNode;\r\n  \/** Accent color for the dot *\/\r\n  color?: Color;\r\n}\r\n\r\nexport interface TimelineProps {\r\n  children?: ReactNode;\r\n  \/** Layout variant. Default: \"stacked\" *\/\r\n  variant?: TimelineVariant;\r\n  \/** @deprecated Use variant instead. Maps \"vertical\" \u2192 \"stacked\", \"horizontal\" \u2192 \"horizontal\" *\/\r\n  orientation?: TimelineOrientation;\r\n  \/** Data-driven events (alternative to children) *\/\r\n  events?: TimelineEvent[];\r\n  \/** Optional heading above the timeline *\/\r\n  heading?: ReactNode;\r\n  \/** Optional description below the heading *\/\r\n  description?: ReactNode;\r\n  \/** Enable scroll-reveal animation. Default: true *\/\r\n  animated?: boolean;\r\n  className?: string;\r\n}\r\n\r\nexport interface TimelineItemProps {\r\n  children?: ReactNode;\r\n  \/** Custom icon ReactNode for the dot *\/\r\n  icon?: ReactNode;\r\n  \/** Emoji character for the dot *\/\r\n  emoji?: string;\r\n  \/** Date label displayed above the title *\/\r\n  date?: string;\r\n  \/** Dot accent color *\/\r\n  color?: Color;\r\n  \/** Whether this item is the active\/current step *\/\r\n  active?: boolean;\r\n  className?: string;\r\n}\r\n\r\nexport interface TimelineBlockProps {\r\n  \/** Block heading *\/\r\n  heading?: ReactNode;\r\n  \/** Block description \/ body content *\/\r\n  children: ReactNode;\r\n  \/** Optional icon displayed in the timeline dot *\/\r\n  icon?: ReactNode;\r\n  \/** Emoji character for the dot *\/\r\n  emoji?: string;\r\n  \/** Dot\/icon color *\/\r\n  color?: Color;\r\n  \/** Whether this block is the active\/current step *\/\r\n  active?: boolean;\r\n  className?: string;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/timeline\/Timeline.types.ts"},{"path":"components\/fancy\/timeline\/TimelineBlock.tsx","content":"import { forwardRef } from \"react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { TimelineItem } from \".\/TimelineItem\";\r\nimport type { TimelineBlockProps } from \".\/Timeline.types\";\r\n\r\nexport const TimelineBlock = forwardRef<HTMLDivElement, TimelineBlockProps>(\r\n  ({ heading, children, icon, emoji, color = \"zinc\", active = false, className }, ref) => {\r\n    return (\r\n      <TimelineItem icon={icon} emoji={emoji} color={color} active={active}>\r\n        <div\r\n          ref={ref}\r\n          data-react-fancy-timeline-block=\"\"\r\n          className={cn(\r\n            \"rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-700 dark:bg-zinc-900\",\r\n            active && \"ring-2 ring-blue-500\/20 dark:ring-blue-400\/20\",\r\n            className,\r\n          )}\r\n        >\r\n          {heading && (\r\n            <div className=\"mb-2 text-sm font-semibold text-zinc-900 dark:text-zinc-100\">\r\n              {heading}\r\n            <\/div>\r\n          )}\r\n          <div className=\"text-sm text-zinc-600 dark:text-zinc-400\">{children}<\/div>\r\n        <\/div>\r\n      <\/TimelineItem>\r\n    );\r\n  },\r\n);\r\n\r\nTimelineBlock.displayName = \"TimelineBlock\";\r\n","type":"registry:ui","target":"components\/fancy\/timeline\/TimelineBlock.tsx"},{"path":"components\/fancy\/timeline\/TimelineItem.tsx","content":"import { forwardRef, useEffect, useRef, useState } from \"react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { useTimeline } from \".\/Timeline.context\";\r\nimport type { TimelineItemProps } from \".\/Timeline.types\";\r\nimport type { Color } from \"..\/..\/utils\/types\";\r\n\r\nconst dotColorClasses: Record<Color, string> = {\r\n  slate: \"bg-slate-400 dark:bg-slate-600\", gray: \"bg-gray-400 dark:bg-gray-600\",\r\n  zinc: \"bg-zinc-300 dark:bg-zinc-600\", neutral: \"bg-neutral-400 dark:bg-neutral-600\",\r\n  stone: \"bg-stone-400 dark:bg-stone-600\",\r\n  red: \"bg-red-500\", orange: \"bg-orange-500\", amber: \"bg-amber-500\",\r\n  yellow: \"bg-yellow-500\", lime: \"bg-lime-500\", green: \"bg-green-500\",\r\n  emerald: \"bg-emerald-500\", teal: \"bg-teal-500\", cyan: \"bg-cyan-500\",\r\n  sky: \"bg-sky-500\", blue: \"bg-blue-500\", indigo: \"bg-indigo-500\",\r\n  violet: \"bg-violet-500\", purple: \"bg-purple-500\", fuchsia: \"bg-fuchsia-500\",\r\n  pink: \"bg-pink-500\", rose: \"bg-rose-500\",\r\n};\r\n\r\nconst ringColorClasses: Record<Color, string> = {\r\n  slate: \"ring-slate-400\/30\", gray: \"ring-gray-400\/30\",\r\n  zinc: \"ring-zinc-400\/30 dark:ring-zinc-500\/30\", neutral: \"ring-neutral-400\/30\",\r\n  stone: \"ring-stone-400\/30\",\r\n  red: \"ring-red-500\/30\", orange: \"ring-orange-500\/30\", amber: \"ring-amber-500\/30\",\r\n  yellow: \"ring-yellow-500\/30\", lime: \"ring-lime-500\/30\", green: \"ring-green-500\/30\",\r\n  emerald: \"ring-emerald-500\/30\", teal: \"ring-teal-500\/30\", cyan: \"ring-cyan-500\/30\",\r\n  sky: \"ring-sky-500\/30\", blue: \"ring-blue-500\/30\", indigo: \"ring-indigo-500\/30\",\r\n  violet: \"ring-violet-500\/30\", purple: \"ring-purple-500\/30\", fuchsia: \"ring-fuchsia-500\/30\",\r\n  pink: \"ring-pink-500\/30\", rose: \"ring-rose-500\/30\",\r\n};\r\n\r\nfunction Dot({ icon, emoji, color, active }: Pick<TimelineItemProps, \"icon\" | \"emoji\" | \"color\" | \"active\">) {\r\n  const c = color ?? \"zinc\";\r\n\r\n  if (emoji) {\r\n    return (\r\n      <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-zinc-200 bg-zinc-100 dark:border-zinc-700 dark:bg-zinc-800\">\r\n        <span className=\"text-sm\">{emoji}<\/span>\r\n      <\/span>\r\n    );\r\n  }\r\n\r\n  if (icon) {\r\n    return (\r\n      <span\r\n        className={cn(\r\n          \"flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-white\",\r\n          dotColorClasses[c],\r\n          active && \"ring-4\",\r\n          active && ringColorClasses[c],\r\n        )}\r\n      >\r\n        {icon}\r\n      <\/span>\r\n    );\r\n  }\r\n\r\n  return (\r\n    <span\r\n      className={cn(\r\n        \"h-3 w-3 shrink-0 rounded-full\",\r\n        dotColorClasses[c],\r\n        active && \"ring-4\",\r\n        active && ringColorClasses[c],\r\n      )}\r\n    \/>\r\n  );\r\n}\r\n\r\nfunction useIntersectionReveal(animated: boolean) {\r\n  const ref = useRef<HTMLDivElement>(null);\r\n  const [visible, setVisible] = useState(!animated);\r\n\r\n  useEffect(() => {\r\n    if (!animated || !ref.current) return;\r\n    const el = ref.current;\r\n    const observer = new IntersectionObserver(\r\n      ([entry]) => { if (entry.isIntersecting) { setVisible(true); observer.disconnect(); } },\r\n      { threshold: 0.2 },\r\n    );\r\n    observer.observe(el);\r\n    return () => observer.disconnect();\r\n  }, [animated]);\r\n\r\n  return { ref, visible };\r\n}\r\n\r\nexport const TimelineItem = forwardRef<HTMLDivElement, TimelineItemProps>(\r\n  ({ children, icon, emoji, date, color = \"zinc\", active = false, className }, _ref) => {\r\n    const { variant, index, total, animated } = useTimeline();\r\n    const { ref, visible } = useIntersectionReveal(animated);\r\n    const isLast = index === total - 1;\r\n    const isLargeDot = !!icon || !!emoji;\r\n    const isEven = index % 2 === 0;\r\n\r\n    \/\/ \u2500\u2500 Horizontal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n    if (variant === \"horizontal\") {\r\n      return (\r\n        <div\r\n          ref={ref}\r\n          data-react-fancy-timeline-item=\"\"\r\n          className={cn(\r\n            \"flex flex-col items-center\",\r\n            !isLast && \"min-w-40\",\r\n            animated && \"transition duration-500 ease-out\",\r\n            animated && (visible ? \"translate-y-0 opacity-100\" : \"translate-y-4 opacity-0\"),\r\n            className,\r\n          )}\r\n        >\r\n          {\/* Dot + horizontal line (fixed height for alignment) *\/}\r\n          <div className=\"flex h-8 w-full items-center\">\r\n            {index > 0\r\n              ? <div className=\"h-px flex-1 bg-zinc-200 dark:bg-zinc-700\" \/>\r\n              : <div className=\"flex-1\" \/>}\r\n            <Dot icon={icon} emoji={emoji} color={color} active={active} \/>\r\n            {!isLast\r\n              ? <div className=\"h-px flex-1 bg-zinc-200 dark:bg-zinc-700\" \/>\r\n              : <div className=\"flex-1\" \/>}\r\n          <\/div>\r\n\r\n          {\/* Content below *\/}\r\n          <div className=\"mt-3 max-w-40 px-2 text-center\">\r\n            {date && <time className=\"text-xs font-medium uppercase tracking-wide text-zinc-500 dark:text-zinc-400\">{date}<\/time>}\r\n            {children}\r\n          <\/div>\r\n        <\/div>\r\n      );\r\n    }\r\n\r\n    \/\/ \u2500\u2500 Alternating \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n    if (variant === \"alternating\") {\r\n      return (\r\n        <div\r\n          ref={ref}\r\n          data-react-fancy-timeline-item=\"\"\r\n          className={cn(\r\n            \"relative flex gap-x-4 md:grid md:grid-cols-[1fr_1.5rem_1fr] md:gap-x-6\",\r\n            animated && \"transition duration-500 ease-out\",\r\n            animated && (visible ? \"translate-y-0 opacity-100\" : \"translate-y-4 opacity-0\"),\r\n            className,\r\n          )}\r\n        >\r\n          {\/* Dot (sits on top of the background line) *\/}\r\n          <div className=\"relative z-10 flex w-8 shrink-0 justify-center md:col-start-2 md:row-start-1 md:w-auto md:justify-center\">\r\n            {!isLargeDot ? <div className=\"mt-1.5\"><Dot icon={icon} emoji={emoji} color={color} active={active} \/><\/div> : <Dot icon={icon} emoji={emoji} color={color} active={active} \/>}\r\n          <\/div>\r\n\r\n          {\/* Content *\/}\r\n          <div className={cn(\r\n            \"min-w-0 flex-1\",\r\n            !isLast && \"pb-8\",\r\n            isLargeDot && \"pt-1\",\r\n            isEven ? \"md:col-start-1 md:row-start-1 md:text-right\" : \"md:col-start-3\",\r\n          )}>\r\n            {date && <time className=\"text-xs font-medium uppercase tracking-wide text-zinc-500 dark:text-zinc-400\">{date}<\/time>}\r\n            {children}\r\n          <\/div>\r\n        <\/div>\r\n      );\r\n    }\r\n\r\n    \/\/ \u2500\u2500 Stacked (default) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n    return (\r\n      <div\r\n        ref={ref}\r\n        data-react-fancy-timeline-item=\"\"\r\n        className={cn(\r\n          \"relative flex gap-x-4\",\r\n          animated && \"transition duration-500 ease-out\",\r\n          animated && (visible ? \"translate-y-0 opacity-100\" : \"translate-y-4 opacity-0\"),\r\n          className,\r\n        )}\r\n      >\r\n        {\/* Dot (sits on top of the background line) *\/}\r\n        <div className=\"relative z-10 flex w-8 shrink-0 justify-center\">\r\n          {!isLargeDot\r\n            ? <div className=\"mt-1.5\"><Dot icon={icon} emoji={emoji} color={color} active={active} \/><\/div>\r\n            : <Dot icon={icon} emoji={emoji} color={color} active={active} \/>}\r\n        <\/div>\r\n\r\n        {\/* Content *\/}\r\n        <div className={cn(\"min-w-0 flex-1\", !isLast && \"pb-8\", isLargeDot && \"pt-1\")}>\r\n          {date && <time className=\"text-xs font-medium uppercase tracking-wide text-zinc-500 dark:text-zinc-400\">{date}<\/time>}\r\n          {children}\r\n        <\/div>\r\n      <\/div>\r\n    );\r\n  },\r\n);\r\n\r\nTimelineItem.displayName = \"TimelineItem\";\r\n","type":"registry:ui","target":"components\/fancy\/timeline\/TimelineItem.tsx"},{"path":"components\/fancy\/timeline\/index.ts","content":"export { Timeline } from \".\/Timeline\";\r\nexport { TimelineItem } from \".\/TimelineItem\";\r\nexport { TimelineBlock } from \".\/TimelineBlock\";\r\nexport type {\r\n  TimelineProps,\r\n  TimelineItemProps,\r\n  TimelineBlockProps,\r\n  TimelineEvent,\r\n  TimelineVariant,\r\n  TimelineOrientation,\r\n} from \".\/Timeline.types\";\r\n","type":"registry:ui","target":"components\/fancy\/timeline\/index.ts"}]}