{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"timeline","title":"Timeline","description":"Ordered event history with semantic and shaped markers.","dependencies":["class-variance-authority@0.7.1"],"registryDependencies":["@siteplane/base","@siteplane/motion"],"files":[{"path":"packages/ui/src/components/timeline/timeline.tsx","content":"import { cva } from \"class-variance-authority\";\nimport type * as React from \"react\";\n\nimport { cn } from \"../../system/utils/cn\";\nimport { slotClass } from \"../../system/utils/slot-class\";\nimport { MotionItem, MotionList } from \"../motion\";\n\n/**\n * Events that are added, removed or reordered animate on their own: a new\n * event expands and fades in, a removed one fades and collapses, and the events\n * around it glide. Give every `TimelineItem` a stable `key`. The 20 px between\n * two events is the list's `gap`, with the connector drawn in it, so it opens\n * and closes with the event after it.\n */\nexport function Timeline({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"ol\">): React.ReactElement {\n  return (\n    <MotionList\n      gap={5}\n      render={\n        <ol\n          className={cn(\n            slotClass(\"timeline\", props),\n            \"flex flex-col\",\n            className,\n          )}\n          data-slot=\"timeline\"\n          role=\"list\"\n          {...props}\n        />\n      }\n      // The connector is positioned against the event, so it runs the full\n      // length of it, and it fades with the space. It sits on the centre of the\n      // lead column, where every marker is centred (`--timeline-lead`,\n      // design-rework.css).\n      separator={\n        <span\n          aria-hidden=\"true\"\n          className=\"sp-timeline-connector absolute top-4 bottom-0 left-[calc(var(--timeline-lead)/2)] w-px -translate-x-1/2\"\n          data-slot=\"timeline-connector\"\n        />\n      }\n    >\n      {children}\n    </MotionList>\n  );\n}\n\n// Marker colors keep at least 3:1 against the page in light and dark themes.\n// The halo, the fill of a ring marker and the icon of a filled marker all take\n// the surface the timeline sits on (`--timeline-surface`).\nconst timelineDotVariants = cva(\n  \"relative z-10 size-2.5 shrink-0 rounded-full bg-(--timeline-marker) ring-4 ring-(--timeline-surface) [--timeline-surface:var(--sp-rw-surface-color,var(--sp-rw-bg-base))]\",\n  {\n    defaultVariants: { tone: \"neutral\" },\n    variants: {\n      tone: {\n        accent: \"[--timeline-marker:var(--sp-rw-fg-accent)]\",\n        error: \"[--timeline-marker:--theme(--color-destructive-foreground)]\",\n        info: \"[--timeline-marker:--theme(--color-info-foreground)]\",\n        neutral: \"[--timeline-marker:--theme(--color-muted-foreground)]\",\n        success: \"[--timeline-marker:--theme(--color-success-foreground)]\",\n        warning: \"[--timeline-marker:--theme(--color-warning-foreground)]\",\n      },\n    },\n  },\n);\n\n/** The marker color: the kit's status values, `accent` and `neutral`. */\nexport type TimelineTone =\n  \"accent\" | \"error\" | \"info\" | \"neutral\" | \"success\" | \"warning\";\n/**\n * @deprecated The earlier marker prop. Use `tone`; its `default` is now\n * `neutral`.\n */\nexport type TimelineItemVariant =\n  \"accent\" | \"default\" | \"error\" | \"info\" | \"success\" | \"warning\";\n\nexport type TimelineMarkerShape = \"bar\" | \"diamond\" | \"dot\" | \"ring\";\nconst timelineMarkerShapeClassNames = {\n  bar: \"h-4 w-1.5 rounded-full\",\n  diamond: \"rotate-45 rounded-[2px]\",\n  dot: \"\",\n  ring: \"border-2 border-(--timeline-marker) bg-(--timeline-surface)\",\n} as const satisfies Record<TimelineMarkerShape, string>;\n\nexport interface TimelineItemProps extends Omit<\n  React.ComponentProps<\"li\">,\n  // Motion owns these handlers; React's versions have a different signature.\n  \"onAnimationStart\" | \"onDrag\" | \"onDragEnd\" | \"onDragStart\"\n> {\n  /** The marker color. Defaults to `neutral`. */\n  tone?: TimelineTone | undefined;\n  /**\n   * @deprecated Use `tone`, the kit's name where color is the whole\n   * variation; `variant=\"default\"` is `tone=\"neutral\"`.\n   */\n  variant?: TimelineItemVariant | null | undefined;\n  marker?: React.ReactNode;\n  markerLabel?: string;\n  markerShape?: TimelineMarkerShape;\n}\n\nexport function TimelineItem({\n  className,\n  tone,\n  variant,\n  marker,\n  markerLabel,\n  markerShape = \"dot\",\n  children,\n  ...props\n}: TimelineItemProps): React.ReactElement {\n  const resolvedTone: TimelineTone =\n    tone ?? (variant === \"default\" ? \"neutral\" : (variant ?? \"neutral\"));\n  return (\n    // The item is the clipping box while it enters and leaves, so it carries no\n    // padding or margin; the list adds the space to the next event inside it.\n    // Every marker is centred in the lead column of the timeline, which a\n    // custom marker (`data-marker`) widens for all events.\n    <MotionItem\n      className={cn(\"group/timeline-item relative\", className)}\n      data-marker={marker ? \"\" : undefined}\n      data-marker-shape={marker ? undefined : markerShape}\n      data-slot=\"timeline-item\"\n      {...props}\n    >\n      <div className=\"flex gap-3\">\n        {/* The lead column is one width for every event (rulebook I9), and a\n            box one line high at the top of the text centres every marker\n            shape and size on the first line: the title's, or the\n            description's when there is no title (rulebook I5). */}\n        <span\n          className=\"sp-timeline-marker-box -mt-0.5 flex h-(--sp-rw-line-18) w-(--timeline-lead) shrink-0 items-center justify-center group-has-data-[slot=timeline-title]/timeline-item:h-(--sp-rw-line-21)\"\n          data-slot=\"timeline-marker-box\"\n        >\n          <span\n            aria-hidden={markerLabel ? undefined : true}\n            aria-label={markerLabel}\n            className={cn(\n              \"sp-timeline-marker\",\n              timelineDotVariants({ tone: resolvedTone }),\n              timelineMarkerShapeClassNames[markerShape],\n              marker && \"grid size-5 place-items-center rounded-full ring-2\",\n              // Icons take the surface color on a filled marker, so they keep\n              // the marker's contrast; a ring shows its icon in the marker color.\n              marker &&\n                (markerShape === \"ring\"\n                  ? \"text-(--timeline-marker)\"\n                  : \"text-(--timeline-surface)\"),\n            )}\n            data-slot=\"timeline-marker\"\n            role={markerLabel ? \"img\" : undefined}\n          >\n            {marker ? (\n              <span\n                className={cn(\n                  \"grid size-full place-items-center leading-none [&>svg]:block [&>svg]:size-3\",\n                  markerShape === \"diamond\" && \"-rotate-45\",\n                )}\n              >\n                {marker}\n              </span>\n            ) : null}\n          </span>\n        </span>\n        <div className=\"-mt-0.5 min-w-0 flex-1 pb-1\">{children}</div>\n      </div>\n    </MotionItem>\n  );\n}\n\nexport interface TimelineNowProps extends Omit<\n  TimelineItemProps,\n  \"children\" | \"marker\" | \"markerLabel\" | \"markerShape\" | \"tone\" | \"variant\"\n> {\n  /**\n   * The text of the marker, shown beside its dot and read by assistive\n   * technology. Defaults to \"Now\"; pass the word in the page's language.\n   */\n  label?: string;\n}\n\n/**\n * Marks the present between two events: the accent dot of an event, its label\n * and a line in the accent across the rest of the row. It is a row of the\n * timeline, `aria-current=\"time\"`, so assistive technology reads it in its\n * place among the events. Give it a stable `key`, as the events have: when the\n * present moves past an event, the marker glides to its new place.\n */\nexport function TimelineNow({\n  label = \"Now\",\n  ...props\n}: TimelineNowProps): React.ReactElement {\n  return (\n    <TimelineItem\n      aria-current=\"time\"\n      {...props}\n      data-slot=\"timeline-now\"\n      tone=\"accent\"\n    >\n      <span\n        className={slotClass(\"timeline-now-label\")}\n        data-slot=\"timeline-now-label\"\n      >\n        {label}\n      </span>\n    </TimelineItem>\n  );\n}\n\nexport function TimelineTitle({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">): React.ReactElement {\n  return (\n    <div\n      className={cn(\n        slotClass(\"timeline-title\", props),\n        \"wrap-anywhere\",\n        className,\n      )}\n      data-slot=\"timeline-title\"\n      {...props}\n    />\n  );\n}\n\nexport function TimelineDescription({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">): React.ReactElement {\n  return (\n    <div\n      className={cn(\n        slotClass(\"timeline-description\", props),\n        \"wrap-anywhere\",\n        className,\n      )}\n      data-slot=\"timeline-description\"\n      {...props}\n    />\n  );\n}\n","type":"registry:ui","target":"~/src/siteplane/components/timeline/timeline.tsx"},{"path":"packages/ui/src/components/timeline/index.ts","content":"export * from \"./timeline\";\n","type":"registry:ui","target":"~/src/siteplane/components/timeline/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/timeline.md","categories":["Data display"],"meta":{"avoidWhen":["Avoid for steps that people move through; use Stepper.","Avoid for history that people sort or filter; use Table."],"description":"Ordered event history with semantic and shaped markers.","knownLimitations":["Added, removed and reordered events animate on their own. Give every event a stable key: an index as the key makes a removed event leave as its neighbor.","Events must be direct children of the timeline, not wrapped in a fragment; a fragment around several events is treated as one.","A leaving event is inert, so focus inside it is lost when it leaves. Move focus before removing it."],"providerNotes":["Depends on the motion component, which the registry installs with the timeline.","SiteplaneMotionProvider preset=\"reduced\" and the system setting make every change instant."],"releaseStatus":"stable","searchTerms":["timeline"],"useWhen":["Use to present events in chronological order with markers, such as an order history or an audit trail."]}}