{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "trace-waterfall",
  "title": "Trace Waterfall",
  "description": "Waterfall timeline showing nested trace spans as a collapsible hierarchy and timed duration bars.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "@drift-ui/use-controllable-state"
  ],
  "files": [
    {
      "path": "registry/default/ui/trace-waterfall.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ChevronRightIcon, TriangleAlertIcon, XIcon } from \"lucide-react\"\n\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\"\nimport {\n  buildSpanTree,\n  calculateTimeTicks,\n  calculateTraceBounds,\n  findParentRowIndex,\n  flattenSpanTree,\n  formatDuration,\n  getServiceColors,\n  stepSpanRow,\n  type Span,\n  type SpanStatus,\n  type TimeTick,\n  type TraceBounds,\n} from \"@/registry/default/lib/trace-waterfall.ts\"\nimport { cn } from \"@/registry/default/lib/utils\"\n\nexport type { Span, SpanStatus }\n\nexport type TraceWaterfallProps = Omit<\n  React.ComponentProps<\"div\">,\n  \"children\" | \"defaultValue\"\n> & {\n  spans: Span[]\n  selectedSpanId?: string | null\n  defaultSelectedSpanId?: string | null\n  onSpanSelect?: (span: Span | null) => void\n  expandedSpanIds?: string[]\n  defaultExpandedSpanIds?: string[]\n  onExpandedChange?: (expandedIds: string[]) => void\n  formatTime?: (ms: number) => string\n  minBarWidth?: number\n  showTimeAxis?: boolean\n  showGridLines?: boolean\n  showSpanDetails?: boolean\n}\n\nconst ERROR_STRIPES =\n  \"bg-[repeating-linear-gradient(135deg,transparent_0_3px,color-mix(in_oklab,var(--color-background)_40%,transparent)_3px_5px)]\"\n\nfunction breakableName(name: string) {\n  return name\n    .split(/(?<=[./_:-])/)\n    .flatMap((part, index) =>\n      index === 0 ? [part] : [<wbr key={index} />, part]\n    )\n}\n\nfunction tickAlignment(tick: TimeTick) {\n  if (tick.percent === 0) return \"translate-x-0\"\n  if (tick.percent > 92) return \"-translate-x-full\"\n  return \"-translate-x-1/2\"\n}\n\nfunction GridLines({ ticks }: { ticks: TimeTick[] }) {\n  return (\n    <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0\">\n      {ticks.map((tick) => (\n        <div\n          key={tick.time}\n          style={{ left: `${tick.percent}%` }}\n          className=\"absolute inset-y-0 border-l border-dashed border-border/60\"\n        />\n      ))}\n    </div>\n  )\n}\n\nfunction SpanDetails({\n  span,\n  bounds,\n  swatchClass,\n  formatTime,\n  onClose,\n}: {\n  span: Span\n  bounds: TraceBounds\n  swatchClass?: string\n  formatTime: (ms: number) => string\n  onClose: () => void\n}) {\n  const offset = Math.max(0, span.start - bounds.minStart)\n  const share = Math.round((span.duration / bounds.totalDuration) * 1000) / 10\n  const attributes = Object.entries(span.attributes ?? {})\n  const fields: [string, React.ReactNode][] = [\n    [\"Duration\", formatTime(span.duration)],\n    [\"Share of trace\", `${share}%`],\n    [\"Starts at\", `+${formatTime(offset)}`],\n    [\"Status\", span.status ?? \"unset\"],\n    [\"Span ID\", span.id],\n  ]\n\n  return (\n    <section\n      aria-label={`Details for ${span.name}`}\n      className=\"border-t border-border bg-muted/30 px-4 py-3\"\n    >\n      <div className=\"flex items-start gap-2\">\n        <div className=\"min-w-0 flex-1\">\n          <p className=\"truncate font-mono text-sm font-medium\">{span.name}</p>\n          {span.service && (\n            <p className=\"mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground\">\n              <span className={cn(\"size-2 rounded-[2px]\", swatchClass)} />\n              {span.service}\n            </p>\n          )}\n        </div>\n        <button\n          type=\"button\"\n          onClick={onClose}\n          aria-label=\"Close span details\"\n          className=\"rounded-sm p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n        >\n          <XIcon className=\"size-3.5\" />\n        </button>\n      </div>\n\n      <dl className=\"mt-3 grid grid-cols-2 gap-x-4 gap-y-2 text-xs sm:grid-cols-5\">\n        {fields.map(([label, value]) => (\n          <div key={label} className=\"min-w-0\">\n            <dt className=\"text-muted-foreground\">{label}</dt>\n            <dd\n              className={cn(\n                \"font-mono wrap-break-word\",\n                label === \"Status\" &&\n                  span.status === \"error\" &&\n                  \"text-destructive\"\n              )}\n            >\n              {value}\n            </dd>\n          </div>\n        ))}\n      </dl>\n\n      {attributes.length > 0 && (\n        <dl className=\"mt-3 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 border-t border-border pt-3 font-mono text-xs\">\n          {attributes.map(([key, value]) => (\n            <React.Fragment key={key}>\n              <dt className=\"text-muted-foreground\">{key}</dt>\n              <dd className=\"min-w-0 break-all\">{String(value)}</dd>\n            </React.Fragment>\n          ))}\n        </dl>\n      )}\n    </section>\n  )\n}\n\nexport function TraceWaterfall({\n  spans,\n  selectedSpanId,\n  defaultSelectedSpanId = null,\n  onSpanSelect,\n  expandedSpanIds,\n  defaultExpandedSpanIds,\n  onExpandedChange,\n  formatTime = formatDuration,\n  minBarWidth = 4,\n  showTimeAxis = true,\n  showGridLines = true,\n  showSpanDetails = true,\n  className,\n  ...props\n}: TraceWaterfallProps) {\n  const initialExpanded = React.useMemo(() => {\n    if (defaultExpandedSpanIds !== undefined) return defaultExpandedSpanIds\n    const parentIds = new Set<string>()\n    for (const span of spans) {\n      if (span.parentId) {\n        parentIds.add(span.parentId)\n      }\n    }\n    return Array.from(parentIds)\n  }, [defaultExpandedSpanIds, spans])\n\n  const expandedState = useControllableState<string[]>({\n    value: expandedSpanIds,\n    defaultValue: initialExpanded,\n    onChange: onExpandedChange,\n  })\n\n  const selectionState = useControllableState<string | null>({\n    value: selectedSpanId,\n    defaultValue: defaultSelectedSpanId,\n    onChange: (id) => {\n      if (!onSpanSelect) return\n      const found = spans.find((s) => s.id === id) ?? null\n      onSpanSelect(found)\n    },\n  })\n\n  const expandedSet = React.useMemo(\n    () => new Set(expandedState.value),\n    [expandedState.value]\n  )\n\n  const tree = React.useMemo(() => buildSpanTree(spans), [spans])\n  const bounds = React.useMemo(() => calculateTraceBounds(spans), [spans])\n  const serviceColors = React.useMemo(() => getServiceColors(spans), [spans])\n  const ticks = React.useMemo(\n    () => calculateTimeTicks(bounds.totalDuration, 5),\n    [bounds.totalDuration]\n  )\n\n  const rows = React.useMemo(\n    () => flattenSpanTree(tree, expandedSet, bounds, serviceColors),\n    [tree, expandedSet, bounds, serviceColors]\n  )\n\n  const errorCount = spans.filter((s) => s.status === \"error\").length\n  const rootSpan = tree[0]?.span\n  const selectedSpan = spans.find((s) => s.id === selectionState.value) ?? null\n\n  const [focusedId, setFocusedId] = React.useState<string | null>(null)\n  const rowRefs = React.useRef(new Map<string, HTMLDivElement>())\n\n  const activeFocusId =\n    focusedId && rows.some((r) => r.span.id === focusedId)\n      ? focusedId\n      : (rows[0]?.span.id ?? null)\n\n  function toggleExpand(id: string, open?: boolean) {\n    const isCurrentlyExpanded = expandedSet.has(id)\n    const shouldExpand = open !== undefined ? open : !isCurrentlyExpanded\n    if (shouldExpand === isCurrentlyExpanded) return\n\n    const next = shouldExpand\n      ? [...expandedState.value, id]\n      : expandedState.value.filter((i) => i !== id)\n    expandedState.setValue(next)\n  }\n\n  function focusRow(index: number) {\n    const target = rows[index]\n    if (!target) return\n    setFocusedId(target.span.id)\n    rowRefs.current.get(target.span.id)?.focus()\n  }\n\n  function toggleSelected(id: string) {\n    selectionState.setValue(selectionState.value === id ? null : id)\n  }\n\n  function handleKeyDown(\n    event: React.KeyboardEvent<HTMLDivElement>,\n    index: number\n  ) {\n    const row = rows[index]\n    if (!row) return\n\n    switch (event.key) {\n      case \"ArrowDown\":\n        event.preventDefault()\n        focusRow(stepSpanRow(rows, index, 1))\n        break\n      case \"ArrowUp\":\n        event.preventDefault()\n        focusRow(stepSpanRow(rows, index, -1))\n        break\n      case \"ArrowRight\":\n        event.preventDefault()\n        if (row.hasChildren) {\n          if (!row.isExpanded) {\n            toggleExpand(row.span.id, true)\n          } else if (index + 1 < rows.length) {\n            focusRow(index + 1)\n          }\n        }\n        break\n      case \"ArrowLeft\":\n        event.preventDefault()\n        if (row.hasChildren && row.isExpanded) {\n          toggleExpand(row.span.id, false)\n        } else {\n          const parentIndex = findParentRowIndex(rows, index)\n          if (parentIndex !== -1) {\n            focusRow(parentIndex)\n          }\n        }\n        break\n      case \"Home\":\n        event.preventDefault()\n        focusRow(0)\n        break\n      case \"End\":\n        event.preventDefault()\n        focusRow(rows.length - 1)\n        break\n      case \"Enter\":\n      case \" \":\n        event.preventDefault()\n        toggleSelected(row.span.id)\n        break\n    }\n  }\n\n  return (\n    <div\n      role=\"region\"\n      aria-label=\"Trace waterfall\"\n      data-slot=\"trace-waterfall\"\n      className={cn(\n        \"flex w-full flex-col overflow-hidden rounded-lg border border-border bg-card text-sm text-card-foreground\",\n        className\n      )}\n      {...props}\n    >\n      {rootSpan && (\n        <div className=\"flex flex-col gap-2 border-b border-border px-4 py-3\">\n          <div className=\"flex flex-wrap items-baseline gap-x-3 gap-y-1\">\n            <span className=\"truncate font-mono text-sm font-medium\">\n              {rootSpan.name}\n            </span>\n            <span className=\"text-xs text-muted-foreground tabular-nums\">\n              {formatTime(bounds.totalDuration)} · {spans.length}{\" \"}\n              {spans.length === 1 ? \"span\" : \"spans\"} · {serviceColors.size}{\" \"}\n              {serviceColors.size === 1 ? \"service\" : \"services\"}\n              {errorCount > 0 && (\n                <span className=\"text-destructive\">\n                  {\" \"}\n                  · {errorCount} {errorCount === 1 ? \"error\" : \"errors\"}\n                </span>\n              )}\n            </span>\n          </div>\n          {serviceColors.size > 0 && (\n            <ul\n              aria-label=\"Services\"\n              className=\"flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground\"\n            >\n              {Array.from(serviceColors, ([service, colorClass]) => (\n                <li key={service} className=\"flex items-center gap-1.5\">\n                  <span\n                    aria-hidden=\"true\"\n                    className={cn(\"size-2 rounded-[2px]\", colorClass)}\n                  />\n                  {service}\n                </li>\n              ))}\n              {errorCount > 0 && (\n                <li className=\"flex items-center gap-1.5\">\n                  <span\n                    aria-hidden=\"true\"\n                    className={cn(\n                      \"h-2 w-3 rounded-[2px] bg-destructive\",\n                      ERROR_STRIPES\n                    )}\n                  />\n                  error\n                </li>\n              )}\n            </ul>\n          )}\n        </div>\n      )}\n\n      {showTimeAxis && (\n        <div className=\"flex border-b border-border bg-muted/40 text-[11px] text-muted-foreground select-none\">\n          <div className=\"w-2/5 shrink-0 border-r border-border px-3 py-1.5 font-medium\">\n            Span\n          </div>\n          <div className=\"min-w-0 flex-1 px-4 py-1.5\">\n            <div className=\"relative h-4\">\n              {ticks.map((tick) => (\n                <span\n                  key={tick.time}\n                  style={{ left: `${tick.percent}%` }}\n                  className={cn(\n                    \"absolute top-0 font-mono whitespace-nowrap tabular-nums\",\n                    tickAlignment(tick)\n                  )}\n                >\n                  {formatTime(tick.time)}\n                </span>\n              ))}\n            </div>\n          </div>\n        </div>\n      )}\n\n      <div role=\"tree\" aria-label=\"Trace spans\">\n        {rows.length === 0 ? (\n          <div className=\"px-4 py-8 text-center text-muted-foreground\">\n            No spans available\n          </div>\n        ) : (\n          rows.map((row, index) => {\n            const isSelected = selectionState.value === row.span.id\n            const isFocused = activeFocusId === row.span.id\n            const isError = row.span.status === \"error\"\n\n            return (\n              <div\n                key={row.span.id}\n                ref={(el) => {\n                  if (el) rowRefs.current.set(row.span.id, el)\n                  else rowRefs.current.delete(row.span.id)\n                }}\n                role=\"treeitem\"\n                aria-level={row.depth + 1}\n                aria-posinset={row.position}\n                aria-setsize={row.siblingCount}\n                aria-expanded={row.hasChildren ? row.isExpanded : undefined}\n                aria-selected={isSelected}\n                aria-label={row.accessibleLabel}\n                tabIndex={isFocused ? 0 : -1}\n                onKeyDown={(e) => handleKeyDown(e, index)}\n                onClick={() => {\n                  setFocusedId(row.span.id)\n                  toggleSelected(row.span.id)\n                }}\n                className={cn(\n                  \"group flex min-h-10 cursor-pointer transition-colors outline-none hover:bg-muted/40 focus-visible:bg-muted/40 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset\",\n                  isSelected &&\n                    \"bg-accent shadow-[inset_2px_0_0_var(--color-primary)] hover:bg-accent\"\n                )}\n              >\n                <div className=\"flex w-2/5 min-w-0 shrink-0 items-start border-r border-border pr-3 pl-2\">\n                  {Array.from({ length: row.depth }, (_, level) => (\n                    <span\n                      key={level}\n                      aria-hidden=\"true\"\n                      className=\"relative w-4 shrink-0 self-stretch before:absolute before:inset-y-0 before:left-2 before:border-l before:border-border\"\n                    />\n                  ))}\n\n                  {row.hasChildren ? (\n                    <button\n                      type=\"button\"\n                      tabIndex={-1}\n                      aria-hidden=\"true\"\n                      onClick={(e) => {\n                        e.stopPropagation()\n                        setFocusedId(row.span.id)\n                        rowRefs.current.get(row.span.id)?.focus()\n                        toggleExpand(row.span.id)\n                      }}\n                      className=\"mt-2 flex size-4 shrink-0 items-center justify-center rounded-sm text-muted-foreground hover:bg-muted hover:text-foreground\"\n                    >\n                      <ChevronRightIcon\n                        className={cn(\n                          \"size-3.5 transition-transform\",\n                          row.isExpanded && \"rotate-90\"\n                        )}\n                      />\n                    </button>\n                  ) : (\n                    <span className=\"size-4 shrink-0\" aria-hidden=\"true\" />\n                  )}\n\n                  <div className=\"ml-1.5 flex min-w-0 flex-1 flex-col py-1.5\">\n                    <span className=\"flex min-w-0 items-center gap-1.5\">\n                      {isError && (\n                        <TriangleAlertIcon\n                          className=\"size-3.5 shrink-0 text-destructive\"\n                          aria-hidden=\"true\"\n                        />\n                      )}\n                      <span\n                        className={cn(\n                          \"min-w-0 font-mono text-xs wrap-break-word\",\n                          isSelected && \"font-medium\",\n                          isError && \"text-destructive\"\n                        )}\n                      >\n                        {breakableName(row.span.name)}\n                      </span>\n                    </span>\n                    <span className=\"flex min-w-0 items-center gap-1.5 text-[11px] text-muted-foreground\">\n                      {row.span.service && (\n                        <>\n                          <span\n                            aria-hidden=\"true\"\n                            className={cn(\n                              \"size-1.5 shrink-0 rounded-full\",\n                              serviceColors.get(row.span.service)\n                            )}\n                          />\n                          <span className=\"truncate\">{row.span.service}</span>\n                        </>\n                      )}\n                      <span className=\"ml-auto shrink-0 pl-2 font-mono tabular-nums\">\n                        {formatTime(row.span.duration)}\n                      </span>\n                    </span>\n                  </div>\n                </div>\n\n                <div className=\"relative min-w-0 flex-1 px-4\">\n                  <div className=\"relative h-full\">\n                    {showGridLines && <GridLines ticks={ticks} />}\n\n                    <div\n                      aria-hidden=\"true\"\n                      title={row.accessibleLabel}\n                      style={{\n                        left: `${row.leftPercent}%`,\n                        width: `max(${row.widthPercent}%, ${minBarWidth}px)`,\n                      }}\n                      className={cn(\n                        \"absolute top-1/2 h-3 -translate-y-1/2 rounded-[3px] transition-[filter] group-hover:brightness-110\",\n                        row.colorClass,\n                        isError && ERROR_STRIPES,\n                        isSelected &&\n                          \"ring-2 ring-foreground/50 ring-offset-1 ring-offset-card\"\n                      )}\n                    />\n                  </div>\n                </div>\n              </div>\n            )\n          })\n        )}\n      </div>\n\n      {showSpanDetails && selectedSpan && (\n        <SpanDetails\n          span={selectedSpan}\n          bounds={bounds}\n          swatchClass={\n            selectedSpan.service\n              ? serviceColors.get(selectedSpan.service)\n              : undefined\n          }\n          formatTime={formatTime}\n          onClose={() => selectionState.setValue(null)}\n        />\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/default/lib/trace-waterfall.ts",
      "content": "export type SpanStatus = \"ok\" | \"error\" | \"warning\" | \"unset\"\n\nexport type Span = {\n  id: string\n  parentId?: string\n  name: string\n  start: number\n  duration: number\n  status?: SpanStatus | string\n  service?: string\n  color?: string\n  attributes?: Record<string, string | number | boolean>\n}\n\nexport type SpanNode = {\n  span: Span\n  children: SpanNode[]\n}\n\nexport type TraceBounds = {\n  minStart: number\n  maxEnd: number\n  totalDuration: number\n}\n\nexport type TimeTick = {\n  time: number\n  percent: number\n  label: string\n}\n\nexport type FlattenedSpanRow = {\n  span: Span\n  depth: number\n  parentId: string | null\n  hasChildren: boolean\n  isExpanded: boolean\n  position: number\n  siblingCount: number\n  leftPercent: number\n  widthPercent: number\n  accessibleLabel: string\n  colorClass: string\n}\n\nconst CHART_COLOR_CLASSES = [\n  \"bg-chart-1 text-chart-1-foreground\",\n  \"bg-chart-2 text-chart-2-foreground\",\n  \"bg-chart-3 text-chart-3-foreground\",\n  \"bg-chart-4 text-chart-4-foreground\",\n  \"bg-chart-5 text-chart-5-foreground\",\n]\n\nexport function formatDuration(ms: number): string {\n  if (!Number.isFinite(ms) || ms <= 0) return \"0 ms\"\n  if (ms < 1) return `${Math.round(ms * 1000)} µs`\n  if (ms < 1000) {\n    const rounded = Number.isInteger(ms) ? ms : Number(ms.toFixed(1))\n    return `${rounded} ms`\n  }\n  const sec = ms / 1000\n  const formatted = Number(sec.toFixed(sec >= 10 ? 1 : 2))\n  return `${formatted} s`\n}\n\nexport function getServiceColors(spans: Span[]): Map<string, string> {\n  const colors = new Map<string, string>()\n  for (const span of spans) {\n    if (span.service && !colors.has(span.service)) {\n      colors.set(\n        span.service,\n        CHART_COLOR_CLASSES[colors.size % CHART_COLOR_CLASSES.length]\n      )\n    }\n  }\n  return colors\n}\n\nexport function getSpanColorClass(\n  span: Span,\n  index = 0,\n  serviceColors?: ReadonlyMap<string, string>\n): string {\n  if (span.status === \"error\") {\n    return \"bg-destructive text-destructive-foreground\"\n  }\n  if (span.color) {\n    return span.color\n  }\n  const assigned = span.service && serviceColors?.get(span.service)\n  if (assigned) return assigned\n  if (span.service) {\n    let hash = 0\n    for (let i = 0; i < span.service.length; i++) {\n      hash = (hash << 5) - hash + span.service.charCodeAt(i)\n      hash |= 0\n    }\n    const idx = Math.abs(hash) % CHART_COLOR_CLASSES.length\n    return CHART_COLOR_CLASSES[idx]\n  }\n  return CHART_COLOR_CLASSES[index % CHART_COLOR_CLASSES.length]\n}\n\nexport function buildSpanTree(spans: Span[]): SpanNode[] {\n  const nodeMap = new Map<string, SpanNode>()\n  const parentMap = new Map<string, string>()\n  for (const span of spans) {\n    nodeMap.set(span.id, { span, children: [] })\n    if (span.parentId && span.parentId !== span.id) {\n      parentMap.set(span.id, span.parentId)\n    }\n  }\n\n  // Detect and break cycles so cycle members safely promote to roots\n  const validParents = new Map<string, string>()\n  for (const [id, parentId] of parentMap.entries()) {\n    let curr: string | undefined = parentId\n    let hasCycle = false\n    const visited = new Set<string>([id])\n    while (curr && nodeMap.has(curr)) {\n      if (visited.has(curr)) {\n        hasCycle = true\n        break\n      }\n      visited.add(curr)\n      curr = parentMap.get(curr)\n    }\n    if (!hasCycle && nodeMap.has(parentId)) {\n      validParents.set(id, parentId)\n    }\n  }\n\n  const roots: SpanNode[] = []\n  for (const span of spans) {\n    const node = nodeMap.get(span.id)!\n    const parentId = validParents.get(span.id)\n    if (parentId && nodeMap.has(parentId)) {\n      nodeMap.get(parentId)!.children.push(node)\n    } else {\n      roots.push(node)\n    }\n  }\n\n  return roots\n}\n\nexport function calculateTraceBounds(spans: Span[]): TraceBounds {\n  if (spans.length === 0) {\n    return { minStart: 0, maxEnd: 0, totalDuration: 1 }\n  }\n  let minStart = Infinity\n  let maxEnd = -Infinity\n  for (const s of spans) {\n    const start = Number.isFinite(s.start) ? s.start : 0\n    const duration = Number.isFinite(s.duration) ? Math.max(0, s.duration) : 0\n    if (start < minStart) minStart = start\n    if (start + duration > maxEnd) maxEnd = start + duration\n  }\n  if (!Number.isFinite(minStart)) minStart = 0\n  if (!Number.isFinite(maxEnd) || maxEnd < minStart) maxEnd = minStart\n  const totalDuration = Math.max(maxEnd - minStart, 1)\n  return { minStart, maxEnd, totalDuration }\n}\n\nexport function niceTickStep(totalDuration: number, maxIntervals = 5) {\n  const raw = Math.max(totalDuration, 1) / Math.max(maxIntervals, 1)\n  const magnitude = 10 ** Math.floor(Math.log10(raw))\n  const multiplier = [1, 2, 2.5, 5, 10].find((m) => m * magnitude >= raw)!\n  return multiplier * magnitude\n}\n\nexport function calculateTimeTicks(\n  totalDuration: number,\n  maxIntervals = 5\n): TimeTick[] {\n  const safeDuration = Math.max(totalDuration, 1)\n  const step = niceTickStep(safeDuration, maxIntervals)\n  const ticks: TimeTick[] = []\n  for (let i = 0; i * step <= safeDuration * (1 + 1e-9); i++) {\n    const time = Number((i * step).toPrecision(12))\n    ticks.push({\n      time,\n      percent: (time / safeDuration) * 100,\n      label: formatDuration(time),\n    })\n  }\n  return ticks\n}\n\nexport function flattenSpanTree(\n  rootNodes: SpanNode[],\n  expandedIds: ReadonlySet<string>,\n  bounds: TraceBounds,\n  serviceColors?: ReadonlyMap<string, string>\n): FlattenedSpanRow[] {\n  const rows: FlattenedSpanRow[] = []\n\n  function traverse(nodes: SpanNode[], depth: number, parentId: string | null) {\n    nodes.forEach((node, index) => {\n      const { span, children } = node\n      const hasChildren = children.length > 0\n      const isExpanded = hasChildren && expandedIds.has(span.id)\n      const start = Number.isFinite(span.start) ? span.start : 0\n      const duration = Number.isFinite(span.duration)\n        ? Math.max(0, span.duration)\n        : 0\n      const leftPercent = Math.max(\n        0,\n        Math.min(100, ((start - bounds.minStart) / bounds.totalDuration) * 100)\n      )\n      const widthPercent = Math.max(\n        0,\n        Math.min(100 - leftPercent, (duration / bounds.totalDuration) * 100)\n      )\n\n      const normalizedStart = Math.max(0, start - bounds.minStart)\n      const accessibleLabel = `${span.name}, ${formatDuration(duration)}, starts at ${formatDuration(normalizedStart)}${span.service ? ` (${span.service})` : \"\"}${span.status ? `, status ${span.status}` : \"\"}`\n\n      const colorClass = getSpanColorClass(span, rows.length, serviceColors)\n\n      rows.push({\n        span,\n        depth,\n        parentId,\n        hasChildren,\n        isExpanded,\n        position: index + 1,\n        siblingCount: nodes.length,\n        leftPercent,\n        widthPercent,\n        accessibleLabel,\n        colorClass,\n      })\n\n      if (isExpanded) {\n        traverse(children, depth + 1, span.id)\n      }\n    })\n  }\n\n  traverse(rootNodes, 0, null)\n  return rows\n}\n\nexport function stepSpanRow(\n  rows: FlattenedSpanRow[],\n  currentIndex: number,\n  step: 1 | -1\n): number {\n  const next = currentIndex + step\n  if (next < 0 || next >= rows.length) return currentIndex\n  return next\n}\n\nexport function findParentRowIndex(\n  rows: FlattenedSpanRow[],\n  currentIndex: number\n): number {\n  const current = rows[currentIndex]\n  if (!current || !current.parentId) return -1\n  return rows.findIndex((r) => r.span.id === current.parentId)\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}