{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"timeline","type":"registry:component","title":"Timeline","description":"Vertical changelog timeline whose connector line draws with scroll progress, with dots that pop in, content that slides into view, done/current/upcoming states, a pulsing ring on the current step, and a static line under reduced motion.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/timeline.tsx","type":"registry:component","target":"@components/motion/timeline.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/motion/timeline\n\nimport {\n  motion,\n  type Transition,\n  useInView,\n  useReducedMotion,\n  useScroll,\n  useSpring,\n} from \"motion/react\";\nimport {\n  createContext,\n  type ReactNode,\n  type RefObject,\n  useContext,\n  useRef,\n} from \"react\";\n\nimport { EASE_OUT, SPRING_GLIDE, SPRING_SWAP } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type TimelineStatus = \"done\" | \"current\" | \"upcoming\";\n\ntype TimelineContextValue = {\n  root?: RefObject<HTMLElement | null>;\n  reduce: boolean;\n};\n\nconst TimelineContext = createContext<TimelineContextValue>({ reduce: false });\n\nexport interface TimelineProps {\n  children: ReactNode;\n  /**\n   * Scroll container for contained scroll areas. Defaults to the viewport.\n   * Drives both the line progress and each item's reveal.\n   */\n  scrollContainer?: RefObject<HTMLElement | null>;\n  className?: string;\n}\n\n// Dot pop: a touch of overshoot so the marker lands with weight.\nconst DOT_POP: Transition = { type: \"spring\", stiffness: 520, damping: 22, mass: 0.6 };\nconst CONTENT_IN: Transition = { duration: 0.45, ease: EASE_OUT };\nconst PULSE: Transition = { duration: 1.6, ease: EASE_OUT, repeat: Number.POSITIVE_INFINITY };\n\nexport function Timeline({ children, scrollContainer, className }: TimelineProps) {\n  const reduce = Boolean(useReducedMotion());\n  const listRef = useRef<HTMLOListElement>(null);\n  const { scrollYProgress } = useScroll({\n    target: listRef,\n    container: scrollContainer,\n    // Line starts drawing as the list's top crosses 80% of the viewport and\n    // completes when its end reaches the bottom edge.\n    offset: [\"start 0.8\", \"end end\"],\n  });\n  const progress = useSpring(scrollYProgress, SPRING_GLIDE);\n\n  return (\n    <TimelineContext.Provider value={{ root: scrollContainer, reduce }}>\n      <div className={cn(\"relative\", className)}>\n        <span\n          aria-hidden\n          className=\"pointer-events-none absolute top-2 bottom-2 left-[15.5px] w-px bg-border\"\n        />\n        <motion.span\n          aria-hidden\n          className=\"pointer-events-none absolute top-2 bottom-2 left-[15.5px] w-px origin-top bg-foreground\"\n          style={{ scaleY: reduce ? 1 : progress }}\n        />\n        <ol ref={listRef} className=\"relative flex flex-col gap-8\">\n          {children}\n        </ol>\n      </div>\n    </TimelineContext.Provider>\n  );\n}\n\nexport interface TimelineItemProps {\n  title: ReactNode;\n  date?: ReactNode;\n  /** ISO date for the `<time>` element when `date` is a display string. */\n  dateTime?: string;\n  description?: ReactNode;\n  icon?: ReactNode;\n  status?: TimelineStatus;\n  /** Extra content under the description (tags, links). */\n  children?: ReactNode;\n  className?: string;\n}\n\nconst STATUS_LABEL: Record<TimelineStatus, string> = {\n  done: \"Completed\",\n  current: \"In progress\",\n  upcoming: \"Upcoming\",\n};\n\nexport function TimelineItem({\n  title,\n  date,\n  dateTime,\n  description,\n  icon,\n  status,\n  children,\n  className,\n}: TimelineItemProps) {\n  const { root, reduce } = useContext(TimelineContext);\n  const ref = useRef<HTMLLIElement>(null);\n  const inView = useInView(ref, { root, once: true, amount: 0.4 });\n  const shown = inView || reduce;\n\n  return (\n    <li\n      ref={ref}\n      aria-current={status === \"current\" ? \"step\" : undefined}\n      className={cn(\"relative flex gap-4\", className)}\n    >\n      <div className=\"relative z-10 grid size-8 shrink-0 place-items-center\">\n        {status === \"current\" && !reduce ? (\n          <motion.span\n            aria-hidden\n            className=\"absolute inset-0 rounded-full bg-foreground/25\"\n            initial={{ scale: 1, opacity: 0 }}\n            animate={shown ? { scale: [1, 1.7], opacity: [0.6, 0] } : undefined}\n            transition={PULSE}\n          />\n        ) : null}\n        {status === \"current\" && reduce ? (\n          <span aria-hidden className=\"absolute -inset-1 rounded-full border border-foreground/30\" />\n        ) : null}\n        <motion.span\n          aria-hidden\n          initial={reduce ? false : { scale: 0 }}\n          animate={{ scale: shown ? 1 : 0 }}\n          transition={DOT_POP}\n          className={cn(\n            \"relative grid size-8 place-items-center rounded-full border text-[13px] [&>svg]:size-3.5\",\n            status === \"done\" && \"border-foreground bg-foreground text-background\",\n            status === \"current\" && \"border-foreground bg-background text-foreground\",\n            (status === \"upcoming\" || !status) && \"border-border bg-background text-muted-foreground\",\n          )}\n        >\n          {icon ?? (\n            <span\n              className={cn(\n                \"size-2 rounded-full\",\n                status === \"done\" ? \"bg-background\" : status === \"current\" ? \"bg-foreground\" : \"bg-muted-foreground/50\",\n              )}\n            />\n          )}\n        </motion.span>\n      </div>\n\n      <motion.div\n        initial={reduce ? { opacity: 0 } : { opacity: 0, x: -8 }}\n        animate={shown ? { opacity: 1, x: 0 } : undefined}\n        transition={reduce ? { duration: 0.2 } : { ...CONTENT_IN, x: SPRING_SWAP }}\n        className=\"min-w-0 flex-1 pt-1\"\n      >\n        <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n          {date ? (\n            <time dateTime={dateTime} className=\"text-xs tabular-nums text-muted-foreground\">\n              {date}\n            </time>\n          ) : null}\n          {status ? (\n            <span\n              className={cn(\n                \"rounded-full px-1.5 py-px text-[10px] font-medium\",\n                status === \"current\" ? \"bg-foreground text-background\" : \"bg-muted text-muted-foreground\",\n              )}\n            >\n              {STATUS_LABEL[status]}\n            </span>\n          ) : null}\n        </div>\n        <h3 className=\"mt-1 text-sm font-medium text-foreground\">{title}</h3>\n        {description ? (\n          <p className=\"mt-1 text-sm leading-6 text-muted-foreground\">{description}</p>\n        ) : null}\n        {children}\n      </motion.div>\n    </li>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}