{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "timeline",
  "title": "Timeline",
  "type": "registry:ui",
  "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"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/timeline/timeline.ts",
      "content": "import { LitElement, css, html, isServer, nothing } from 'lit'\nimport { styleMap } from 'lit/directives/style-map.js'\nimport { html as staticHtml, unsafeStatic } from 'lit/static-html.js'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { timelineMediaVariants, type TimelineMediaVariant } from './timeline.variants'\n\nexport type TimelineDirection = 'vertical' | 'horizontal'\nexport type TimelineAlign = 'start' | 'center'\nexport type TimelineSide = 'left' | 'right' | 'top' | 'bottom'\nexport type TimelineStatus = 'default' | 'current' | 'success' | 'warning' | 'error' | 'info' | 'muted'\nexport type TimelineDensity = 'compact' | 'default' | 'comfortable'\n\n/** What <uip-timeline> pushes down to each slotted <uip-timeline-item> (React's TimelineContext). */\nexport interface TimelineContextValue {\n  direction: TimelineDirection\n  align: TimelineAlign\n  side: TimelineSide\n  density: TimelineDensity\n  index: number\n  count: number\n}\n\n/** What <uip-timeline-item> pushes down to its media / separator (React's TimelineItemContext). */\nexport interface TimelineItemContextValue {\n  index: number\n  isFirst: boolean\n  isLast: boolean\n  side: TimelineSide\n  status: TimelineStatus\n  direction: TimelineDirection\n  density: TimelineDensity\n}\n\n// React's injected `timeline-current-pulse` keyframes, replayed with the Web\n// Animations API (the shadow sheet takes no custom CSS). Same timing/easing;\n// honours prefers-reduced-motion like React's @media rule.\nconst PULSE_ID = 'timeline-current-pulse'\nconst PULSE: Keyframe[] = [\n  { boxShadow: '0 0 0 0 color-mix(in oklab, var(--primary) 45%, transparent)', easing: 'ease-in-out' },\n  { boxShadow: '0 0 0 6px color-mix(in oklab, var(--primary) 0%, transparent)', offset: 0.5, easing: 'ease-in-out' },\n  { boxShadow: '0 0 0 0 color-mix(in oklab, var(--primary) 45%, transparent)' },\n]\n\nfunction syncPulse(el: Element | null | undefined, on: boolean) {\n  if (isServer || !el) return\n  const running = el.getAnimations().find((a) => a.id === PULSE_ID)\n  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches\n  if (on && !reduce) {\n    if (running) return\n    const anim = el.animate(PULSE, { duration: 1800, iterations: Infinity })\n    anim.id = PULSE_ID\n  } else running?.cancel()\n}\n\n/* ------------------------------------------------------------------ Timeline */\n\n/**\n * <uip-timeline> — the registry Timeline root, one of nine composable parts:\n * <uip-timeline>, <uip-timeline-item>, <uip-timeline-media>,\n * <uip-timeline-separator>, <uip-timeline-content>, <uip-timeline-header>,\n * <uip-timeline-title>, <uip-timeline-description>, <uip-timeline-date>.\n * The parts reference nothing by id, so they are separate elements (no\n * cross-shadow ARIA).\n *\n * React's context (register/indexOf) becomes a push-down: the root hands\n * each slotted <uip-timeline-item> its index, count, direction, align, side\n * and density; each item hands its media/separator the item state.\n *\n * Properties: `direction` ('vertical' | 'horizontal'), `align`\n * ('start' | 'center'), `side` ('left' | 'right' | 'top' | 'bottom'; default\n * 'left' vertical / 'top' horizontal), `density` ('compact' | 'default' |\n * 'comfortable'). No events (React has no callbacks).\n */\nexport class UipTimeline extends LitElement {\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    direction: { reflect: true },\n    // Not reflected: `align` is a legacy presentational attribute (browsers map\n    // align=\"center\" to text-align: center on any element).\n    align: {},\n    side: { reflect: true },\n    density: { reflect: true },\n  }\n\n  direction: TimelineDirection = 'vertical'\n  align: TimelineAlign = 'start'\n  side?: TimelineSide\n  density: TimelineDensity = 'default'\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'timeline')\n  }\n\n  willUpdate() {\n    this.setAttribute('data-direction', this.direction)\n    this.setAttribute('data-align', this.align)\n  }\n\n  private get items() {\n    const slot = this.renderRoot?.querySelector('slot')\n    return (slot?.assignedElements() ?? []).filter((el): el is UipTimelineItem => el instanceof UipTimelineItem)\n  }\n\n  private pushContext() {\n    const items = this.items\n    const side: TimelineSide = this.side ?? (this.direction === 'horizontal' ? 'top' : 'left')\n    items.forEach((item, index) => {\n      item.timelineContext = {\n        direction: this.direction,\n        align: this.align,\n        side,\n        density: this.density,\n        index,\n        count: items.length,\n      }\n    })\n  }\n\n  updated() {\n    this.pushContext()\n  }\n\n  render() {\n    return html`<div\n      part=\"base\"\n      data-direction=${this.direction}\n      data-align=${this.align}\n      class=${cn(\n        'relative',\n        this.direction === 'vertical' ? 'flex flex-col' : 'flex flex-row',\n        // An authored align=\"center\" attribute centres the host's text (see\n        // above); reset it so items align like React's.\n        this.align === 'center' && 'text-start',\n      )}\n    >\n      <slot @slotchange=${() => this.pushContext()}></slot>\n    </div>`\n  }\n}\n\n/* -------------------------------------------------------------- TimelineItem */\n\n// React's `[&>[data-slot=…]]:` child selectors can't reach slotted children\n// from the shadow tree; the same rules are written as `::slotted()` on the\n// <slot>. The media/separator/content hosts are the flex/grid items here, so\n// their flex-item utilities (shrink-0 self-stretch / flex-1) are applied to\n// the hosts the same way. Padding/margin rules carry `!`: the host page's\n// preflight (`* { padding: 0; margin: 0 }`) is in an outer tree context and\n// beats any non-important ::slotted() declaration.\nconst slottedBase =\n  '[&::slotted([data-slot=timeline-media])]:shrink-0 [&::slotted([data-slot=timeline-media])]:self-stretch [&::slotted([data-slot=timeline-separator])]:shrink-0 [&::slotted([data-slot=timeline-separator])]:self-stretch [&::slotted([data-slot=timeline-content])]:flex-1'\n\nconst verticalSpacingClasses = {\n  compact: '[&::slotted([data-slot=timeline-content])]:pb-2!',\n  default: '[&::slotted([data-slot=timeline-content])]:pb-6!',\n  comfortable: '[&::slotted([data-slot=timeline-content])]:pb-10!',\n}\n\nconst horizontalSpacingClasses = {\n  compact: '[&::slotted([data-slot=timeline-content])]:pr-3!',\n  default: '[&::slotted([data-slot=timeline-content])]:pr-6!',\n  comfortable: '[&::slotted([data-slot=timeline-content])]:pr-10!',\n}\n\n// React's `timeline-item-enter` keyframes as tw-animate utilities: fade from 0,\n// rise 6px, 320ms, cubic-bezier(0.22, 1, 0.36, 1), both, staggered delay\n// (via --tw-animation-delay: `delay-[…]` resolves to transition-delay);\n// motion-safe matches React's prefers-reduced-motion opt-out.\nconst enterClasses =\n  'motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-bottom-[6px] motion-safe:duration-320 motion-safe:ease-[cubic-bezier(0.22,1,0.36,1)] motion-safe:fill-mode-both motion-safe:[--tw-animation-delay:var(--timeline-stagger)]'\n\n/**\n * <uip-timeline-item> — one entry. Children: <uip-timeline-media> or\n * <uip-timeline-separator>, then <uip-timeline-content>.\n *\n * Properties: `side` (overrides the timeline's side / center alternation),\n * `status` ('default' | 'current' | 'success' | 'warning' | 'error' | 'info'\n * | 'muted', inherited by the media marker). Host reflects `data-side`,\n * `data-status`, `data-last` like React. React's render-prop children\n * ({ index, isLast, side, status }) have no web-component equivalent; read\n * the host's data attributes instead.\n */\nexport class UipTimelineItem extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    side: { reflect: true },\n    status: { reflect: true },\n    timelineContext: { attribute: false },\n  }\n\n  side?: TimelineSide\n  status: TimelineStatus = 'default'\n  /** Set by the parent <uip-timeline>; not part of the public API. */\n  timelineContext?: TimelineContextValue\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'timeline-item')\n  }\n\n  private get state(): TimelineItemContextValue {\n    const ctx = this.timelineContext\n    const index = ctx ? ctx.index : 0\n    const isLast = ctx ? index === ctx.count - 1 : false\n    let side: TimelineSide\n    if (this.side) side = this.side\n    else if (!ctx) side = 'left'\n    else if (ctx.align === 'center') {\n      if (ctx.direction === 'vertical') side = index % 2 === 0 ? 'left' : 'right'\n      else side = index % 2 === 0 ? 'top' : 'bottom'\n    } else side = ctx.side\n    return {\n      index,\n      isFirst: index === 0,\n      isLast,\n      side,\n      status: this.status,\n      direction: ctx?.direction ?? 'vertical',\n      density: ctx?.density ?? 'default',\n    }\n  }\n\n  willUpdate() {\n    const s = this.state\n    this.setAttribute('data-side', s.side)\n    this.setAttribute('data-status', s.status)\n    // React: data-last={isLast || undefined} → \"true\" or absent.\n    if (s.isLast) this.setAttribute('data-last', 'true')\n    else this.removeAttribute('data-last')\n  }\n\n  private pushContext() {\n    const slot = this.renderRoot?.querySelector('slot')\n    const itemContext = this.state\n    for (const el of slot?.assignedElements() ?? []) {\n      if (el instanceof UipTimelineMedia || el instanceof UipTimelineSeparator) el.itemContext = itemContext\n    }\n  }\n\n  updated() {\n    this.pushContext()\n  }\n\n  render() {\n    const s = this.state\n    const isCenter = this.timelineContext?.align === 'center'\n    const { direction, side, isLast, density } = s\n    const verticalSpacing = isLast ? '' : verticalSpacingClasses[density]\n    const horizontalSpacing = isLast ? '' : horizontalSpacingClasses[density]\n\n    const classes = cn(\n      'timeline-item-enter relative',\n      enterClasses,\n      s.status === 'current' && 'timeline-item-current',\n      !isCenter && direction === 'vertical' && cn('flex gap-4', side === 'right' && 'flex-row-reverse text-right'),\n      !isCenter && direction === 'horizontal' && cn('flex flex-col gap-2', side === 'bottom' && 'flex-col-reverse'),\n      isCenter && direction === 'vertical' && 'grid grid-cols-[1fr_auto_1fr] items-start gap-x-4',\n      isCenter && direction === 'horizontal' && 'grid grid-rows-[1fr_auto_1fr] items-start gap-y-2',\n    )\n\n    const slotClasses = cn(\n      slottedBase,\n      !isCenter && direction === 'vertical' && verticalSpacing,\n      !isCenter && direction === 'horizontal' && horizontalSpacing,\n      isCenter &&\n        direction === 'vertical' &&\n        cn(\n          '[&::slotted([data-slot=timeline-media])]:col-start-2 [&::slotted([data-slot=timeline-media])]:row-start-1',\n          '[&::slotted([data-slot=timeline-separator])]:col-start-2 [&::slotted([data-slot=timeline-separator])]:row-start-1',\n          '[&::slotted([data-slot=timeline-content])]:row-start-1',\n          side === 'left' &&\n            '[&::slotted([data-slot=timeline-content])]:col-start-1 [&::slotted([data-slot=timeline-content])]:text-right',\n          side === 'right' && '[&::slotted([data-slot=timeline-content])]:col-start-3',\n          verticalSpacing,\n        ),\n      isCenter &&\n        direction === 'horizontal' &&\n        cn(\n          '[&::slotted([data-slot=timeline-media])]:col-start-1 [&::slotted([data-slot=timeline-media])]:row-start-2',\n          '[&::slotted([data-slot=timeline-separator])]:col-start-1 [&::slotted([data-slot=timeline-separator])]:row-start-2',\n          '[&::slotted([data-slot=timeline-content])]:col-start-1',\n          side === 'top' &&\n            '[&::slotted([data-slot=timeline-content])]:row-start-1 [&::slotted([data-slot=timeline-content])]:self-end',\n          side === 'bottom' && '[&::slotted([data-slot=timeline-content])]:row-start-3',\n          horizontalSpacing,\n        ),\n    )\n\n    return html`<div\n      part=\"base\"\n      class=${classes}\n      style=${styleMap({ '--timeline-stagger': `${Math.min(s.index, 12) * 55}ms` })}\n    >\n      <slot class=${slotClasses} @slotchange=${() => this.pushContext()}></slot>\n    </div>`\n  }\n}\n\n/* ------------------------------------------------------------- TimelineMedia */\n\n// React's `[&>svg]` / `[&>img]` / `[&>[data-slot=avatar]]` marker rules as\n// `::slotted()` on the marker's <slot> (the icon/avatar is light DOM).\nconst markerSlotClasses: Record<TimelineMediaVariant, string> = {\n  dot: '[&::slotted(svg)]:shrink-0',\n  icon: '[&::slotted(svg)]:shrink-0 [&::slotted(svg)]:size-4',\n  avatar:\n    '[&::slotted(svg)]:shrink-0 [&::slotted(img)]:size-full [&::slotted(img)]:object-cover [&::slotted([data-slot=avatar])]:size-full',\n  outline: '[&::slotted(svg)]:shrink-0 [&::slotted(svg)]:size-4',\n}\n\n/**\n * <uip-timeline-media> — the marker (dot / icon / avatar / outline) plus the\n * auto connector line to the next item. Slot the icon <svg>, <img> or avatar.\n *\n * Properties: `variant`, `status` (defaults to the item's), `hideConnector`\n * (`hide-connector`), `coloredConnector` (`colored-connector`), `lineStyle`\n * (`line-style`: 'solid' | 'dashed' | 'dotted').\n */\nexport class UipTimelineMedia extends LitElement {\n  // grid: the inner React root fills the host on both axes.\n  static styles = [tailwind, css`:host { display: grid; }`]\n\n  static properties = {\n    variant: { reflect: true },\n    status: { reflect: true },\n    hideConnector: { type: Boolean, attribute: 'hide-connector' },\n    coloredConnector: { type: Boolean, attribute: 'colored-connector' },\n    lineStyle: { attribute: 'line-style' },\n    itemContext: { attribute: false },\n  }\n\n  variant: TimelineMediaVariant = 'dot'\n  status?: TimelineStatus\n  hideConnector = false\n  coloredConnector = false\n  lineStyle: 'solid' | 'dashed' | 'dotted' = 'solid'\n  /** Set by the parent <uip-timeline-item>; not part of the public API. */\n  itemContext?: TimelineItemContextValue\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'timeline-media')\n  }\n\n  willUpdate() {\n    this.setAttribute('data-variant', this.variant)\n  }\n\n  updated() {\n    syncPulse(this.renderRoot.querySelector('[data-slot=timeline-media-marker]'), this.itemContext?.status === 'current')\n  }\n\n  private get connectorBgClass() {\n    const direction = this.itemContext?.direction ?? 'vertical'\n    const effectiveStatus: TimelineStatus = this.status ?? this.itemContext?.status ?? 'default'\n    if (this.lineStyle === 'dashed') {\n      return direction === 'vertical'\n        ? 'border-l-2 border-dashed border-border bg-transparent w-0'\n        : 'border-t-2 border-dashed border-border bg-transparent h-0'\n    }\n    if (this.lineStyle === 'dotted') {\n      return direction === 'vertical'\n        ? 'border-l-2 border-dotted border-border bg-transparent w-0'\n        : 'border-t-2 border-dotted border-border bg-transparent h-0'\n    }\n    if (!this.coloredConnector) return 'bg-border'\n    return {\n      default: 'bg-primary',\n      current: '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]\n  }\n\n  render() {\n    const item = this.itemContext\n    const direction = item?.direction ?? 'vertical'\n    const isLast = item?.isLast ?? true\n    const effectiveStatus: TimelineStatus = this.status ?? item?.status ?? 'default'\n    const showConnector = !this.hideConnector && !isLast\n\n    return html`<div\n      part=\"base\"\n      data-variant=${this.variant}\n      class=${cn(\n        'relative flex shrink-0 items-center',\n        direction === 'vertical' ? 'flex-col self-stretch' : 'flex-row items-center self-stretch',\n      )}\n    >\n      <div\n        data-uipkge=\"\"\n        data-slot=\"timeline-media-marker\"\n        class=${cn(\n          timelineMediaVariants({ variant: this.variant, status: effectiveStatus }),\n          direction === 'vertical' && this.variant === 'dot' && 'mt-1',\n        )}\n      >\n        <slot class=${markerSlotClasses[this.variant] ?? markerSlotClasses.dot}></slot>\n      </div>\n      ${showConnector\n        ? html`<div\n            data-uipkge=\"\"\n            data-slot=\"timeline-media-connector\"\n            aria-hidden=\"true\"\n            class=${cn(direction === 'vertical' ? 'my-1 w-px flex-1' : 'mx-1 h-px flex-1', this.connectorBgClass)}\n          ></div>`\n        : nothing}\n    </div>`\n  }\n}\n\n/* --------------------------------------------------------- TimelineSeparator */\n\n/**\n * <uip-timeline-separator> — the compact filled-dot marker with a connector.\n * Slot `dot` overrides the inner dot (React's `dot` prop).\n *\n * Properties: `hideConnector` (`hide-connector`).\n */\nexport class UipTimelineSeparator extends LitElement {\n  static styles = [tailwind, css`:host { display: grid; }`]\n\n  static properties = {\n    hideConnector: { type: Boolean, attribute: 'hide-connector' },\n    itemContext: { attribute: false },\n  }\n\n  hideConnector = false\n  /** Set by the parent <uip-timeline-item>; not part of the public API. */\n  itemContext?: TimelineItemContextValue\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'timeline-separator')\n  }\n\n  updated() {\n    syncPulse(\n      this.renderRoot.querySelector('[data-slot=timeline-separator-marker]'),\n      this.itemContext?.status === 'current',\n    )\n  }\n\n  render() {\n    const direction = this.itemContext?.direction ?? 'vertical'\n    const isLast = this.itemContext?.isLast ?? true\n    const showConnector = !this.hideConnector && !isLast\n\n    return html`<div\n      part=\"base\"\n      class=${cn(\n        'relative flex shrink-0 items-center',\n        direction === 'vertical' ? 'w-4 flex-col self-stretch' : 'h-4 flex-row items-center self-stretch',\n      )}\n    >\n      <div\n        data-slot=\"timeline-separator-marker\"\n        class=\"bg-primary ring-background relative z-10 flex size-4 items-center justify-center rounded-full shadow-2xs ring-4\"\n      >\n        <slot name=\"dot\"><div class=\"bg-primary-foreground size-1.5 rounded-full\"></div></slot>\n      </div>\n      ${showConnector\n        ? html`<div\n            aria-hidden=\"true\"\n            data-slot=\"timeline-media-connector\"\n            class=${cn('bg-border', direction === 'vertical' ? 'my-1.5 w-0.5 flex-1' : 'mx-1.5 h-0.5 flex-1')}\n          ></div>`\n        : nothing}\n    </div>`\n  }\n}\n\n/* ----------------------------------------------------------- TimelineContent */\n\n/**\n * <uip-timeline-content> — the entry body. React's `space-y-1` targets\n * children, which are slotted here, so it is written as `::slotted()` (with\n * `!`, see the item's slotted rules).\n */\nexport class UipTimelineContent extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'timeline-content')\n  }\n\n  render() {\n    return html`<div part=\"base\" class=\"flex-1 space-y-1\">\n      <slot class=\"[&::slotted(:not(:last-child))]:mb-1!\"></slot>\n    </div>`\n  }\n}\n\n/* ------------------------------------------------------------ TimelineHeader */\n\n/** <uip-timeline-header> — title + trailing meta row (flex, wraps). */\nexport class UipTimelineHeader extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'timeline-header')\n  }\n\n  render() {\n    return html`<div part=\"base\" class=\"flex flex-wrap items-center justify-between gap-2\"><slot></slot></div>`\n  }\n}\n\n/* ------------------------------------------------------------- TimelineTitle */\n\nconst titleTags = new Set(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div'])\n\n/**\n * <uip-timeline-title> — the entry heading. Property `as` ('h1'…'h6' |\n * 'div', default 'h3') picks the inner element, like React's `as`.\n */\nexport class UipTimelineTitle extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    as: {},\n  }\n\n  as: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'div' = 'h3'\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'timeline-title')\n  }\n\n  render() {\n    const tag = unsafeStatic(titleTags.has(this.as) ? this.as : 'h3')\n    return staticHtml`<${tag} part=\"base\" class=\"text-sm leading-none font-semibold tracking-tight\"><slot></slot></${tag}>`\n  }\n}\n\n/* ------------------------------------------------------- TimelineDescription */\n\n/** <uip-timeline-description> — muted supporting paragraph. */\nexport class UipTimelineDescription extends LitElement {\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'timeline-description')\n  }\n\n  render() {\n    return html`<p part=\"base\" class=\"text-muted-foreground text-sm\"><slot></slot></p>`\n  }\n}\n\n/* -------------------------------------------------------------- TimelineDate */\n\n/**\n * <uip-timeline-date> — muted timestamp, an inner <time>. Property\n * `dateTime` (attribute `datetime`) is forwarded to it, like React's\n * `dateTime` on <time>.\n */\nexport class UipTimelineDate extends LitElement {\n  // display: contents so the inner <time> is laid out where React's is (a flex\n  // item in <uip-timeline-header>, inline in text). An inline/block host would\n  // add its own 24px line box around the text-xs <time> and grow the row by 8px.\n  static styles = [tailwind, css`:host { display: contents; }`]\n\n  static properties = {\n    dateTime: { attribute: 'datetime' },\n  }\n\n  dateTime?: string\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'timeline-date')\n  }\n\n  render() {\n    return html`<time part=\"base\" datetime=${this.dateTime ?? nothing} class=\"text-muted-foreground text-xs\"\n      ><slot></slot\n    ></time>`\n  }\n}\n\nconst define = (tag: string, cls: CustomElementConstructor) => customElements.get(tag) || customElements.define(tag, cls)\ndefine('uip-timeline', UipTimeline)\ndefine('uip-timeline-item', UipTimelineItem)\ndefine('uip-timeline-media', UipTimelineMedia)\ndefine('uip-timeline-separator', UipTimelineSeparator)\ndefine('uip-timeline-content', UipTimelineContent)\ndefine('uip-timeline-header', UipTimelineHeader)\ndefine('uip-timeline-title', UipTimelineTitle)\ndefine('uip-timeline-description', UipTimelineDescription)\ndefine('uip-timeline-date', UipTimelineDate)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-timeline': UipTimeline\n    'uip-timeline-item': UipTimelineItem\n    'uip-timeline-media': UipTimelineMedia\n    'uip-timeline-separator': UipTimelineSeparator\n    'uip-timeline-content': UipTimelineContent\n    'uip-timeline-header': UipTimelineHeader\n    'uip-timeline-title': UipTimelineTitle\n    'uip-timeline-description': UipTimelineDescription\n    'uip-timeline-date': UipTimelineDate\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/timeline/timeline.ts"
    },
    {
      "path": "packages/elements-lit/src/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 transition-colors [&>svg]:shrink-0',\n  {\n    variants: {\n      variant: {\n        dot: 'size-2.5',\n        icon: 'size-8 border border-border bg-card text-muted-foreground shadow-2xs [&>svg]:size-4',\n        avatar:\n          'size-8 ring-2 ring-border bg-muted overflow-hidden [&>img]:size-full [&>img]:object-cover [&>[data-slot=avatar]]:size-full',\n        outline: 'size-8 border border-border bg-background text-foreground [&>svg]:size-4',\n      },\n      status: {\n        default: '',\n        current: '',\n        success: '',\n        warning: '',\n        error: '',\n        info: '',\n        muted: '',\n      },\n    },\n    compoundVariants: [\n      // DOT\n      {\n        variant: 'dot',\n        status: 'default',\n        class: 'bg-primary text-primary-foreground',\n      },\n      {\n        variant: 'dot',\n        status: 'current',\n        class: 'bg-primary text-primary-foreground ring-4 ring-primary/25',\n      },\n      {\n        variant: 'dot',\n        status: 'success',\n        class: 'bg-success text-success-foreground',\n      },\n      {\n        variant: 'dot',\n        status: 'warning',\n        class: 'bg-warning text-warning-foreground',\n      },\n      {\n        variant: 'dot',\n        status: 'error',\n        class: 'bg-destructive text-destructive-foreground',\n      },\n      {\n        variant: 'dot',\n        status: 'info',\n        class: 'bg-info text-info-foreground',\n      },\n      {\n        variant: 'dot',\n        status: 'muted',\n        class: 'bg-muted-foreground/30 text-muted-foreground',\n      },\n\n      // ICON\n      {\n        variant: 'icon',\n        status: 'default',\n        class: 'border-border bg-card text-muted-foreground',\n      },\n      {\n        variant: 'icon',\n        status: 'current',\n        class: 'border-primary/50 bg-primary/10 text-primary ring-4 ring-primary/15',\n      },\n      {\n        variant: 'icon',\n        status: 'success',\n        class: 'border-success/40 bg-success/10 text-success ring-4 ring-success/10',\n      },\n      {\n        variant: 'icon',\n        status: 'warning',\n        class: 'border-warning/40 bg-warning/10 text-warning ring-4 ring-warning/10',\n      },\n      {\n        variant: 'icon',\n        status: 'error',\n        class: 'border-destructive/40 bg-destructive/10 text-destructive ring-4 ring-destructive/10',\n      },\n      {\n        variant: 'icon',\n        status: 'info',\n        class: 'border-info/40 bg-info/10 text-info ring-4 ring-info/10',\n      },\n      {\n        variant: 'icon',\n        status: 'muted',\n        class: 'border-border/60 bg-muted/40 text-muted-foreground/60',\n      },\n\n      // OUTLINE\n      {\n        variant: 'outline',\n        status: 'default',\n        class: 'border-border text-foreground bg-background',\n      },\n      {\n        variant: 'outline',\n        status: 'current',\n        class: 'border-primary text-primary bg-background ring-4 ring-primary/25',\n      },\n      {\n        variant: 'outline',\n        status: 'success',\n        class: 'border-success text-success bg-background',\n      },\n      {\n        variant: 'outline',\n        status: 'warning',\n        class: 'border-warning text-warning bg-background',\n      },\n      {\n        variant: 'outline',\n        status: 'error',\n        class: 'border-destructive text-destructive bg-background',\n      },\n      {\n        variant: 'outline',\n        status: 'info',\n        class: 'border-info text-info bg-background',\n      },\n      {\n        variant: 'outline',\n        status: 'muted',\n        class: 'border-border text-muted-foreground bg-background',\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' | 'outline'\n",
      "type": "registry:file",
      "target": "src/uip/components/timeline/timeline.variants.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lit"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
