{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-timeline",
  "title": "Animated Timeline",
  "description": "Modular timeline layout with animated progress and custom thumbnails.",
  "dependencies": [
    "framer-motion"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "src/components/library/animated-timeline.tsx",
      "content": "\"use client\"\n\nimport Image from \"next/image\"\nimport * as React from \"react\"\nimport { motion, useReducedMotion, useScroll, useSpring } from \"framer-motion\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type TimelineItemBase = {\n  id: string\n  title: string\n  period?: string\n  description?: string\n  tags?: string[]\n  thumbnailSrc?: string\n  thumbnailAlt?: string\n}\n\nexport type AnimatedTimelineProps<T extends TimelineItemBase> = {\n  items: T[]\n  orientation?: \"vertical\" | \"horizontal\"\n  layout?: \"stack\" | \"alternating\"\n  responsive?: boolean\n  renderThumbnail?: (item: T) => React.ReactNode\n  renderContent?: (item: T) => React.ReactNode\n  className?: string\n  railClassName?: string\n  itemClassName?: string\n}\n\nfunction DefaultThumbnail({ src, alt }: { src?: string; alt?: string }) {\n  if (!src) {\n    return (\n      <div className=\"flex h-full w-full items-center justify-center rounded-2xl border border-foreground/15 bg-foreground/5 text-[10px] font-semibold uppercase tracking-[0.2em] text-foreground/50\">\n        No image\n      </div>\n    )\n  }\n\n  return (\n    <div className=\"relative h-full w-full\">\n      <Image\n        src={src}\n        alt={alt ?? \"Timeline thumbnail\"}\n        fill\n        sizes=\"80px\"\n        className=\"rounded-2xl object-cover\"\n      />\n    </div>\n  )\n}\n\nexport function AnimatedTimeline<T extends TimelineItemBase>({\n  items,\n  orientation = \"vertical\",\n  layout = \"stack\",\n  responsive = true,\n  renderThumbnail,\n  renderContent,\n  className,\n  railClassName,\n  itemClassName,\n}: AnimatedTimelineProps<T>) {\n  const containerRef = React.useRef<HTMLDivElement | null>(null)\n  const contentRef = React.useRef<HTMLDivElement | null>(null)\n  const itemRefs = React.useRef<Array<HTMLDivElement | null>>([])\n  const prefersReducedMotion = useReducedMotion()\n  const isVertical = orientation === \"vertical\"\n  const isAlternating = isVertical && layout === \"alternating\"\n  const [isCompact, setIsCompact] = React.useState(false)\n  const [activeProgress, setActiveProgress] = React.useState(0)\n\n  React.useEffect(() => {\n    const container = containerRef.current\n    if (!container) return\n    const update = () => {\n      const width = container.getBoundingClientRect().width\n      setIsCompact(width < 720)\n    }\n    update()\n    const observer = new ResizeObserver(update)\n    observer.observe(container)\n    return () => observer.disconnect()\n  }, [])\n\n  const useAlternatingLayout = isAlternating && (!responsive || !isCompact)\n\n  const scrollConfig = isVertical\n    ? { target: contentRef, offset: [\"start end\", \"end start\"] as (\"start end\" | \"end start\")[] }\n    : { container: containerRef }\n\n  const { scrollYProgress, scrollXProgress } = useScroll(scrollConfig)\n  const progress = isVertical ? scrollYProgress : scrollXProgress ?? scrollYProgress\n  const smoothProgress = useSpring(progress, { stiffness: 140, damping: 30, mass: 0.2 })\n  const smoothActiveProgress = useSpring(activeProgress, { stiffness: 180, damping: 28, mass: 0.2 })\n  const railProgress = isVertical\n    ? prefersReducedMotion\n      ? activeProgress\n      : smoothActiveProgress\n    : prefersReducedMotion\n    ? progress\n    : smoothProgress\n\n  React.useEffect(() => {\n    if (!isVertical) return\n    const container = containerRef.current\n    const content = contentRef.current\n    if (!container || !content) return\n\n    const update = () => {\n      const contentRect = content.getBoundingClientRect()\n      if (!contentRect.height) return\n\n      const viewAnchor = contentRect.top + contentRect.height * 0.25\n      let closestIndex = 0\n      let closestDistance = Number.POSITIVE_INFINITY\n\n      itemRefs.current.forEach((node, index) => {\n        if (!node) return\n        const rect = node.getBoundingClientRect()\n        const center = rect.top + rect.height / 2\n        const distance = Math.abs(center - viewAnchor)\n        if (distance < closestDistance) {\n          closestDistance = distance\n          closestIndex = index\n        }\n      })\n\n      const activeNode = itemRefs.current[closestIndex]\n      if (!activeNode) return\n      const activeRect = activeNode.getBoundingClientRect()\n      const activeCenter = activeRect.top + activeRect.height / 2\n      const offset = activeCenter - contentRect.top\n      const nextProgress = Math.min(Math.max(offset / contentRect.height, 0), 1)\n      setActiveProgress(nextProgress)\n    }\n\n    update()\n    const onScroll = () => requestAnimationFrame(update)\n    const observer = new ResizeObserver(onScroll)\n    observer.observe(content)\n    window.addEventListener(\"scroll\", onScroll, { passive: true })\n    container.addEventListener(\"scroll\", onScroll, { passive: true })\n\n    return () => {\n      observer.disconnect()\n      window.removeEventListener(\"scroll\", onScroll)\n      container.removeEventListener(\"scroll\", onScroll)\n    }\n  }, [isVertical])\n\n  return (\n    <div\n      ref={containerRef}\n      className={cn(\"relative\", isVertical ? \"\" : \"overflow-x-auto pb-6\", className)}\n    >\n      <div\n        ref={contentRef}\n        className={cn(\n          \"relative\",\n          isVertical\n            ? isAlternating\n              ? \"space-y-12\"\n              : \"space-y-10\"\n            : \"flex w-max gap-10 pr-10\"\n        )}\n      >\n        <div\n          className={cn(\n            \"pointer-events-none absolute z-0 bg-foreground/10\",\n            isVertical\n              ? useAlternatingLayout\n                ? \"left-1/2 top-0 h-full w-px -translate-x-1/2\"\n                : \"left-5 top-0 h-full w-px\"\n              : \"left-0 top-5 h-px w-full\"\n          )}\n        />\n        <motion.div\n          className={cn(\n            \"pointer-events-none absolute z-0 origin-top bg-foreground/40\",\n            isVertical\n              ? useAlternatingLayout\n                ? \"left-1/2 top-0 h-full w-px -translate-x-1/2\"\n                : \"left-5 top-0 h-full w-px\"\n              : \"left-0 top-5 h-px w-full\",\n            railClassName\n          )}\n          style={isVertical ? { scaleY: railProgress } : { scaleX: railProgress, transformOrigin: \"0% 50%\" }}\n        />\n\n        {items.map((item, index) => {\n          const thumb = renderThumbnail ? renderThumbnail(item) : <DefaultThumbnail src={item.thumbnailSrc} alt={item.thumbnailAlt} />\n          const content = renderContent ? (\n            renderContent(item)\n          ) : (\n            <div className=\"space-y-2\">\n              <div className=\"space-y-1\">\n                <p className=\"text-[11px] font-semibold uppercase tracking-[0.3em] text-foreground/50\">\n                  {item.period ?? `Phase ${index + 1}`}\n                </p>\n                <h3 className=\"text-lg font-semibold text-foreground\">{item.title}</h3>\n              </div>\n              {item.description ? <p className=\"text-sm text-foreground/70\">{item.description}</p> : null}\n              {item.tags && item.tags.length > 0 ? (\n                <div className=\"flex flex-wrap gap-2 pt-2\">\n                  {item.tags.map((tag) => (\n                    <span\n                      key={tag}\n                      className=\"rounded-full border border-foreground/15 bg-background/70 px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.2em] text-foreground/70 transition hover:border-foreground/30 hover:bg-foreground/10 hover:text-foreground\"\n                    >\n                      {tag}\n                    </span>\n                  ))}\n                </div>\n              ) : null}\n            </div>\n          )\n\n          const isLeft = index % 2 === 0\n          const rowDirection = useAlternatingLayout ? (isLeft ? \"flex-row\" : \"flex-row-reverse\") : \"flex-row\"\n          const cardAlign = useAlternatingLayout ? (isLeft ? \"justify-start\" : \"justify-end\") : \"justify-start\"\n          return (\n            <motion.div\n              key={item.id}\n              ref={(node) => {\n                itemRefs.current[index] = node\n              }}\n              initial={prefersReducedMotion ? false : { opacity: 0, y: isVertical ? 24 : 0, x: isVertical ? 0 : 24 }}\n              whileInView={prefersReducedMotion ? undefined : { opacity: 1, y: 0, x: 0 }}\n              viewport={{ once: true, margin: \"-15%\" }}\n              transition={{ duration: 0.45, ease: \"easeOut\", delay: prefersReducedMotion ? 0 : index * 0.12 }}\n              className={cn(\n                \"relative\",\n                useAlternatingLayout\n                  ? cn(\"flex items-stretch gap-8\", rowDirection)\n                  : isVertical\n                  ? \"flex items-stretch gap-6\"\n                  : \"flex min-w-[260px] flex-col gap-5 pt-12\",\n                itemClassName\n              )}\n            >\n              <div className=\"flex w-10 shrink-0 items-center justify-center self-stretch\">\n                <span className=\"flex h-3 w-3 items-center justify-center rounded-full border border-foreground/20 bg-background\">\n                  <span className=\"h-1.5 w-1.5 rounded-full bg-foreground/70\" />\n                </span>\n              </div>\n\n              <div className={cn(\"relative z-10 flex-1 self-stretch\", \"flex items-center\", cardAlign)}>\n                <div className=\"flex w-full max-w-[520px] items-start gap-6 rounded-3xl border border-foreground/10 bg-background p-6 shadow-[0_20px_40px_-32px_rgba(15,23,42,0.6)]\">\n                  <div className=\"h-20 w-20 shrink-0 self-center rounded-2xl border border-foreground/10 bg-background/70 shadow-sm\">\n                    {thumb}\n                  </div>\n                  <div className=\"flex-1\">{content}</div>\n                </div>\n              </div>\n            </motion.div>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/library/animated-timeline.tsx"
    }
  ],
  "type": "registry:ui"
}