{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "timeline",
  "title": "Timeline",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/timeline/Timeline.vue",
      "content": "<script setup lang=\"ts\">\nimport { provide, ref, toRef } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport {\n  TIMELINE_CONTEXT,\n  type TimelineAlign,\n  type TimelineDensity,\n  type TimelineDirection,\n  type TimelineSide,\n} from './context'\n\nconst props = withDefaults(\n  defineProps<{\n    class?: HTMLAttributes['class']\n    direction?: TimelineDirection\n    align?: TimelineAlign\n    side?: TimelineSide\n    density?: TimelineDensity\n  }>(),\n  {\n    direction: 'vertical',\n    align: 'start',\n    density: 'default',\n  },\n)\n\nconst itemIds = ref<symbol[]>([])\n\nprovide(TIMELINE_CONTEXT, {\n  direction: toRef(props, 'direction'),\n  align: toRef(props, 'align'),\n  side: toRef(() => props.side ?? (props.direction === 'horizontal' ? 'top' : 'left')),\n  density: toRef(props, 'density'),\n  itemIds,\n  register: (id) => {\n    if (!itemIds.value.includes(id)) itemIds.value.push(id)\n  },\n  unregister: (id) => {\n    itemIds.value = itemIds.value.filter((i) => i !== id)\n  },\n})\n</script>\n\n<template>\n  <div\n    data-uipkge\n    data-slot=\"timeline\"\n    :data-direction=\"direction\"\n    :data-align=\"align\"\n    :class=\"cn('relative', direction === 'vertical' ? 'flex flex-col' : 'flex flex-row', props.class)\"\n    v-bind=\"$attrs\"\n  >\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/timeline/Timeline.vue"
    },
    {
      "path": "packages/registry-vue/components/timeline/TimelineItem.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, inject, onBeforeUnmount, provide, toRef } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { TIMELINE_CONTEXT, TIMELINE_ITEM_CONTEXT, type TimelineSide, type TimelineStatus } from './context'\n\nconst props = withDefaults(\n  defineProps<{\n    class?: HTMLAttributes['class']\n    side?: TimelineSide\n    status?: TimelineStatus\n  }>(),\n  {\n    status: 'default',\n  },\n)\n\nconst ctx = inject(TIMELINE_CONTEXT, null)\nconst id = Symbol('TimelineItem')\n\nif (ctx) {\n  ctx.register(id)\n  onBeforeUnmount(() => ctx.unregister(id))\n}\n\nconst index = computed(() => (ctx ? ctx.itemIds.value.indexOf(id) : 0))\nconst isFirst = computed(() => index.value === 0)\nconst isLast = computed(() => (ctx ? index.value === ctx.itemIds.value.length - 1 : false))\n\nconst effectiveSide = computed<TimelineSide>(() => {\n  if (props.side) return props.side\n  if (!ctx) return 'left'\n  if (ctx.align.value === 'center') {\n    if (ctx.direction.value === 'vertical') {\n      return index.value % 2 === 0 ? 'left' : 'right'\n    }\n    return index.value % 2 === 0 ? 'top' : 'bottom'\n  }\n  return ctx.side.value\n})\n\nconst direction = computed(() => ctx?.direction.value ?? 'vertical')\nconst density = computed(() => ctx?.density.value ?? 'default')\nconst isCenter = computed(() => ctx?.align.value === 'center')\n\nconst verticalSpacing = computed(() => {\n  if (isLast.value) return ''\n  return {\n    compact: '[&>[data-slot=timeline-content]]:pb-2',\n    default: '[&>[data-slot=timeline-content]]:pb-6',\n    comfortable: '[&>[data-slot=timeline-content]]:pb-10',\n  }[density.value]\n})\n\nconst horizontalSpacing = computed(() => {\n  if (isLast.value) return ''\n  return {\n    compact: '[&>[data-slot=timeline-content]]:pr-3',\n    default: '[&>[data-slot=timeline-content]]:pr-6',\n    comfortable: '[&>[data-slot=timeline-content]]:pr-10',\n  }[density.value]\n})\n\nprovide(TIMELINE_ITEM_CONTEXT, {\n  index,\n  isFirst,\n  isLast,\n  side: effectiveSide,\n  status: toRef(props, 'status'),\n  direction,\n  density,\n})\n</script>\n\n<template>\n  <div\n    data-uipkge\n    data-slot=\"timeline-item\"\n    :data-side=\"effectiveSide\"\n    :data-last=\"isLast || undefined\"\n    :class=\"\n      cn(\n        'relative',\n        // start mode (default): simple flex\n        // No item padding — TimelineMedia's continuous line relies on items\n        // butting up edge-to-edge. Use TimelineContent's own padding for\n        // breathing room between rows/cards.\n        !isCenter &&\n          direction === 'vertical' && [\n            'flex gap-4',\n            effectiveSide === 'right' && 'flex-row-reverse text-right',\n            verticalSpacing,\n          ],\n        !isCenter &&\n          direction === 'horizontal' && [\n            'flex flex-col gap-2',\n            effectiveSide === 'bottom' && 'flex-col-reverse',\n            horizontalSpacing,\n          ],\n        // center alternating: 3-col / 3-row grid\n        isCenter &&\n          direction === 'vertical' && [\n            'grid grid-cols-[1fr_auto_1fr] items-start gap-x-4',\n            '[&>[data-slot=timeline-media]]:col-start-2',\n            '[&>[data-slot=timeline-separator]]:col-start-2',\n            effectiveSide === 'left' &&\n              '[&>[data-slot=timeline-content]]:col-start-1 [&>[data-slot=timeline-content]]:text-right',\n            effectiveSide === 'right' && '[&>[data-slot=timeline-content]]:col-start-3',\n            verticalSpacing,\n          ],\n        isCenter &&\n          direction === 'horizontal' && [\n            'grid grid-rows-[1fr_auto_1fr] items-start gap-y-2',\n            '[&>[data-slot=timeline-media]]:row-start-2',\n            '[&>[data-slot=timeline-separator]]:row-start-2',\n            effectiveSide === 'top' &&\n              '[&>[data-slot=timeline-content]]:row-start-1 [&>[data-slot=timeline-content]]:self-end',\n            effectiveSide === 'bottom' && '[&>[data-slot=timeline-content]]:row-start-3',\n            horizontalSpacing,\n          ],\n        props.class,\n      )\n    \"\n    v-bind=\"$attrs\"\n  >\n    <slot :index=\"index\" :is-last=\"isLast\" :side=\"effectiveSide\" :status=\"status\" />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/timeline/TimelineItem.vue"
    },
    {
      "path": "packages/registry-vue/components/timeline/TimelineMedia.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, inject } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { TIMELINE_ITEM_CONTEXT, type TimelineStatus } from './context'\nimport { timelineMediaVariants, type TimelineMediaVariant } from './timeline.variants'\n\nconst props = withDefaults(\n  defineProps<{\n    class?: HTMLAttributes['class']\n    variant?: TimelineMediaVariant\n    status?: TimelineStatus\n    /** Manually hide the auto-generated connector line. */\n    hideConnector?: boolean\n    /**\n     * Color the connector line below the marker using the item's status\n     * (success → green, muted → gray, etc) instead of the neutral border.\n     * Opt-in so existing timelines stay visually unchanged.\n     */\n    coloredConnector?: boolean\n  }>(),\n  {\n    variant: 'dot',\n  },\n)\n\nconst item = inject(TIMELINE_ITEM_CONTEXT, null)\n\nconst direction = computed(() => item?.direction.value ?? 'vertical')\nconst isFirst = computed(() => item?.isFirst.value ?? true)\nconst isLast = computed(() => item?.isLast.value ?? true)\nconst effectiveStatus = computed<TimelineStatus>(() => props.status ?? item?.status.value ?? 'default')\nconst showConnector = computed(() => !props.hideConnector && !(isFirst.value && isLast.value))\n\n// Marker half-size in rem, used to crop the line so it visually emerges\n// from the marker center on the first item.\nconst markerHalfRem = computed(\n  () =>\n    ({\n      dot: '0.375rem', // size-3 = 12px / 2\n      icon: '1rem', // size-8 = 32px / 2\n      avatar: '1.125rem', // size-9 = 36px / 2\n    })[(props.variant ?? 'dot') as 'dot' | 'icon' | 'avatar'],\n)\n\nconst connectorBgClass = computed(() => {\n  if (!props.coloredConnector) return 'bg-border'\n  return {\n    default: 'bg-primary',\n    success: 'bg-success',\n    warning: 'bg-warning',\n    error: 'bg-destructive',\n    info: 'bg-info',\n    muted: 'bg-muted-foreground/40',\n  }[effectiveStatus.value]\n})\n</script>\n\n<template>\n  <div\n    data-uipkge\n    data-slot=\"timeline-media\"\n    :class=\"\n      cn(\n        'relative shrink-0 self-stretch',\n        direction === 'vertical' ? 'flex w-9 flex-col items-center' : 'flex h-9 flex-row items-center',\n        props.class,\n      )\n    \"\n    :style=\"{ '--timeline-marker-half': markerHalfRem }\"\n  >\n    <!-- Single continuous connector line, positioned through the marker.\n         The marker's bg + ring-background acts as a \"punch-through\" so the\n         line appears to break at each marker without any per-item math. -->\n    <div\n      v-if=\"showConnector\"\n      data-uipkge\n      data-slot=\"timeline-media-connector\"\n      aria-hidden=\"true\"\n      :class=\"\n        direction === 'vertical'\n          ? cn('absolute left-1/2 w-px -translate-x-1/2', connectorBgClass)\n          : cn('absolute top-1/2 h-px -translate-y-1/2', connectorBgClass)\n      \"\n      :style=\"\n        direction === 'vertical'\n          ? {\n              top: isFirst ? 'var(--timeline-marker-half)' : '0',\n              bottom: isLast ? 'calc(100% - var(--timeline-marker-half))' : '0',\n            }\n          : {\n              left: isFirst ? 'var(--timeline-marker-half)' : '0',\n              right: isLast ? 'calc(100% - var(--timeline-marker-half))' : '0',\n            }\n      \"\n    />\n\n    <!-- Marker — its bg + ring-background hides the line behind it. -->\n    <div\n      data-uipkge\n      data-slot=\"timeline-media-marker\"\n      :class=\"cn(timelineMediaVariants({ variant, status: effectiveStatus }), 'relative z-10')\"\n    >\n      <slot />\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/timeline/TimelineMedia.vue"
    },
    {
      "path": "packages/registry-vue/components/timeline/TimelineSeparator.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, inject } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { TIMELINE_ITEM_CONTEXT } from './context'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n  /** Manually hide the auto-generated connector line. */\n  hideConnector?: boolean\n}>()\n\nconst item = inject(TIMELINE_ITEM_CONTEXT, null)\nconst direction = computed(() => item?.direction.value ?? 'vertical')\nconst isFirst = computed(() => item?.isFirst.value ?? true)\nconst isLast = computed(() => item?.isLast.value ?? true)\nconst showConnector = computed(() => !props.hideConnector && !(isFirst.value && isLast.value))\n</script>\n\n<template>\n  <!-- Legacy compact separator: 16px marker, customizable inner dot via #dot\n       slot. Uses the same single-absolute-line strategy as TimelineMedia so\n       the connector is pixel-aligned across items. --marker-half = 0.5rem\n       (= size-4 / 2). -->\n  <div\n    data-uipkge\n    data-slot=\"timeline-separator\"\n    :class=\"\n      cn(\n        'relative shrink-0 self-stretch',\n        direction === 'vertical' ? 'flex w-4 flex-col items-center' : 'flex h-4 flex-row items-center',\n        props.class,\n      )\n    \"\n    style=\"--timeline-marker-half: 0.5rem\"\n    v-bind=\"$attrs\"\n  >\n    <div\n      v-if=\"showConnector\"\n      aria-hidden=\"true\"\n      :class=\"\n        direction === 'vertical'\n          ? 'bg-border absolute left-1/2 w-px -translate-x-1/2'\n          : 'bg-border absolute top-1/2 h-px -translate-y-1/2'\n      \"\n      :style=\"\n        direction === 'vertical'\n          ? {\n              top: isFirst ? 'var(--timeline-marker-half)' : '0',\n              bottom: isLast ? 'calc(100% - var(--timeline-marker-half))' : '0',\n            }\n          : {\n              left: isFirst ? 'var(--timeline-marker-half)' : '0',\n              right: isLast ? 'calc(100% - var(--timeline-marker-half))' : '0',\n            }\n      \"\n    />\n    <div class=\"bg-primary ring-background relative z-10 flex size-4 items-center justify-center rounded-full ring-4\">\n      <slot name=\"dot\">\n        <div class=\"bg-primary-foreground size-2 rounded-full\" />\n      </slot>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/timeline/TimelineSeparator.vue"
    },
    {
      "path": "packages/registry-vue/components/timeline/TimelineContent.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n}>()\n</script>\n\n<template>\n  <div data-uipkge data-slot=\"timeline-content\" :class=\"cn('flex-1 space-y-1', props.class)\" v-bind=\"$attrs\">\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/timeline/TimelineContent.vue"
    },
    {
      "path": "packages/registry-vue/components/timeline/TimelineTitle.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n  as?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div'\n  class?: HTMLAttributes['class']\n}>()\n</script>\n\n<template>\n  <component\n    :is=\"props.as ?? 'h3'\"\n    data-uipkge\n    data-slot=\"timeline-title\"\n    :class=\"cn('text-sm leading-none font-semibold tracking-tight', props.class)\"\n    v-bind=\"$attrs\"\n  >\n    <slot />\n  </component>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/timeline/TimelineTitle.vue"
    },
    {
      "path": "packages/registry-vue/components/timeline/TimelineDescription.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n}>()\n</script>\n\n<template>\n  <p\n    data-uipkge\n    data-slot=\"timeline-description\"\n    :class=\"cn('text-muted-foreground text-sm', props.class)\"\n    v-bind=\"$attrs\"\n  >\n    <slot />\n  </p>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/timeline/TimelineDescription.vue"
    },
    {
      "path": "packages/registry-vue/components/timeline/TimelineDate.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { cn } from '@/lib/utils'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n}>()\n</script>\n\n<template>\n  <time data-uipkge data-slot=\"timeline-date\" :class=\"cn('text-muted-foreground text-xs', props.class)\" v-bind=\"$attrs\">\n    <slot />\n  </time>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/timeline/TimelineDate.vue"
    },
    {
      "path": "packages/registry-vue/components/timeline/context.ts",
      "content": "import type { InjectionKey, Ref } from 'vue'\n\nexport type TimelineDirection = 'vertical' | 'horizontal'\nexport type TimelineAlign = 'start' | 'center'\nexport type TimelineSide = 'left' | 'right' | 'top' | 'bottom'\nexport type TimelineStatus = 'default' | 'success' | 'warning' | 'error' | 'info' | 'muted'\nexport type TimelineDensity = 'compact' | 'default' | 'comfortable'\n\nexport interface TimelineContext {\n  direction: Ref<TimelineDirection>\n  align: Ref<TimelineAlign>\n  side: Ref<TimelineSide>\n  density: Ref<TimelineDensity>\n  itemIds: Ref<symbol[]>\n  register: (id: symbol) => void\n  unregister: (id: symbol) => void\n}\n\nexport const TIMELINE_CONTEXT: InjectionKey<TimelineContext> = Symbol('TimelineContext')\n\nexport interface TimelineItemContext {\n  index: Ref<number>\n  isFirst: Ref<boolean>\n  isLast: Ref<boolean>\n  side: Ref<TimelineSide>\n  status: Ref<TimelineStatus>\n  direction: Ref<TimelineDirection>\n  density: Ref<TimelineDensity>\n}\n\nexport const TIMELINE_ITEM_CONTEXT: InjectionKey<TimelineItemContext> = Symbol('TimelineItemContext')\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/timeline/context.ts"
    },
    {
      "path": "packages/registry-vue/components/timeline/timeline.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file (rather than the package\n * `index.ts`) so consuming Vue SFCs can import without creating a circular\n * dependency through the index. See card.variants.ts for the canonical\n * example + the SSR symptom that motivated the split.\n */\n\nexport const timelineMediaVariants = cva(\n  'relative z-10 flex shrink-0 items-center justify-center rounded-full ring-4 ring-background [&>svg]:shrink-0',\n  {\n    variants: {\n      variant: {\n        dot: 'size-3',\n        icon: 'size-8 [&>svg]:size-4',\n        avatar: 'size-9 ring-2 [&>img]:size-full [&>img]:rounded-full [&>img]:object-cover',\n      },\n      status: {\n        default: 'bg-primary text-primary-foreground',\n        success: 'bg-success text-success-foreground',\n        warning: 'bg-warning text-warning-foreground',\n        error: 'bg-destructive text-destructive-foreground',\n        info: 'bg-info text-info-foreground',\n        muted: 'bg-muted text-muted-foreground',\n      },\n    },\n    defaultVariants: {\n      variant: 'dot',\n      status: 'default',\n    },\n  },\n)\n\nexport type TimelineMediaVariantsProps = VariantProps<typeof timelineMediaVariants>\nexport type TimelineMediaVariant = 'dot' | 'icon' | 'avatar'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/timeline/timeline.variants.ts"
    },
    {
      "path": "packages/registry-vue/components/timeline/index.ts",
      "content": "export { default as Timeline } from './Timeline.vue'\nexport { default as TimelineItem } from './TimelineItem.vue'\nexport { default as TimelineMedia } from './TimelineMedia.vue'\nexport { default as TimelineSeparator } from './TimelineSeparator.vue'\nexport { default as TimelineContent } from './TimelineContent.vue'\nexport { default as TimelineTitle } from './TimelineTitle.vue'\nexport { default as TimelineDescription } from './TimelineDescription.vue'\nexport { default as TimelineDate } from './TimelineDate.vue'\n\nexport type { TimelineDirection, TimelineAlign, TimelineSide, TimelineStatus, TimelineDensity } from './context'\n\n// Re-export variant API from the sibling file (kept separate to avoid the\n// Component.vue <-> index.ts circular import that broke dev SSR for Card).\nexport { timelineMediaVariants, type TimelineMediaVariantsProps, type TimelineMediaVariant } from './timeline.variants'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/timeline/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Vertical or horizontal sequence of events with connectors and node markers. Statuses (pending, current, completed, failed) tint the connector. Use for activity feeds, audit logs, and progress tracking.",
  "categories": [
    "data-display"
  ]
}