{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-conversation-map",
  "type": "registry:component",
  "title": "Conversation map",
  "description": "A rail of the whole thread: one tick per turn, the turn being read marked and the ones on screen deepened, and a hover preview of what it says.",
  "registryDependencies": [
    "https://r.assistant-ui.com/elements-surfaces.json",
    "https://r.assistant-ui.com/elements-range.json"
  ],
  "dependencies": [
    "@base-ui/react"
  ],
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/elements/conversation-map.tsx",
      "sourcePath": "packages/ui/src/components/react/assistant-ui/elements/conversation-map.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useRef,\n  useState,\n  type ComponentProps,\n  type KeyboardEvent,\n} from \"react\";\nimport { PreviewCard } from \"@base-ui/react/preview-card\";\nimport { cn } from \"@/lib/utils\";\nimport { floating } from \"./surfaces\";\nimport { clamp } from \"../utils/range\";\n\nexport interface ConversationMapEntry {\n  id: string;\n  title: string;\n  preview?: string;\n}\n\nconst TICK = '[data-slot=\"conversation-map-tick\"]';\n\nexport function ConversationMap({\n  entries,\n  activeId,\n  visibleIds,\n  onSelect,\n  side = \"right\",\n  className,\n  onKeyDown,\n  ...props\n}: Omit<ComponentProps<\"nav\">, \"children\" | \"onSelect\"> & {\n  entries: readonly ConversationMapEntry[];\n  activeId?: string | undefined;\n  visibleIds?: readonly string[] | undefined;\n  onSelect?: ((id: string) => void) | undefined;\n  side?: \"left\" | \"right\";\n}) {\n  const railRef = useRef<HTMLElement>(null);\n  const [handle] = useState(() =>\n    PreviewCard.createHandle<ConversationMapEntry>(),\n  );\n  const [focusedIndex, setFocusedIndex] = useState<number | null>(null);\n\n  const inView = new Set(visibleIds);\n  const activeIndex = entries.findIndex((entry) => entry.id === activeId);\n  const tabbableIndex = clamp(\n    focusedIndex ?? Math.max(0, activeIndex),\n    0,\n    Math.max(0, entries.length - 1),\n  );\n\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLElement>) => {\n      onKeyDown?.(event);\n      if (event.defaultPrevented) return;\n\n      const ticks = railRef.current?.querySelectorAll<HTMLElement>(TICK);\n      if (!ticks?.length) return;\n\n      const current = Array.prototype.indexOf.call(ticks, event.target);\n      if (current === -1) return;\n\n      const next = {\n        ArrowUp: current - 1,\n        ArrowDown: current + 1,\n        Home: 0,\n        End: ticks.length - 1,\n      }[event.key];\n      if (next === undefined) return;\n\n      event.preventDefault();\n      ticks[clamp(next, 0, ticks.length - 1)]?.focus();\n    },\n    [onKeyDown],\n  );\n\n  return (\n    <nav\n      data-slot=\"conversation-map\"\n      ref={railRef}\n      aria-label=\"Conversation map\"\n      onKeyDown={handleKeyDown}\n      className={cn(\n        \"group/rail flex h-full w-6 flex-col justify-center\",\n        className,\n      )}\n      {...props}\n    >\n      {entries.map((entry, index) => {\n        const current = index === activeIndex;\n        const onScreen = current || inView.has(entry.id);\n        return (\n          <PreviewCard.Trigger\n            key={entry.id}\n            handle={handle}\n            payload={entry}\n            delay={120}\n            closeDelay={80}\n            render={<button type=\"button\" />}\n            data-slot=\"conversation-map-tick\"\n            data-active={current ? \"\" : undefined}\n            data-in-view={onScreen ? \"\" : undefined}\n            aria-label={entry.title}\n            aria-current={current ? \"true\" : undefined}\n            tabIndex={index === tabbableIndex ? 0 : -1}\n            onFocus={() => setFocusedIndex(index)}\n            onClick={onSelect ? () => onSelect(entry.id) : undefined}\n            // The cap keeps a short thread packed instead of spread over the\n            // whole gutter; a long one outgrows it and the share decides.\n            className=\"group flex max-h-3.5 min-h-0 flex-1 items-center outline-none\"\n          >\n            {/* At rest every tick is the same short length and only weight and\n                depth separate the tiers. Pointing at the rail grows them out by\n                tier, and the one actually under the pointer reaches full length\n                so the rail says which turn the card belongs to. */}\n            <span\n              className={cn(\n                \"w-3 rounded-full transition-[width,height,background-color] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\",\n                current\n                  ? \"bg-foreground/90 h-[3px] group-focus-within/rail:w-6 group-hover/rail:w-6\"\n                  : cn(\n                      \"group-hover:bg-foreground/70 group-focus-visible:bg-foreground/70 h-0.5\",\n                      \"group-hover:w-6! group-focus-visible:w-6!\",\n                      onScreen\n                        ? \"bg-foreground/50 group-focus-within/rail:w-[18px] group-hover/rail:w-[18px]\"\n                        : \"bg-foreground/15\",\n                    ),\n              )}\n            />\n          </PreviewCard.Trigger>\n        );\n      })}\n\n      <PreviewCard.Root handle={handle}>\n        {({ payload }) => (\n          <PreviewCard.Portal>\n            <PreviewCard.Positioner side={side} sideOffset={10}>\n              <PreviewCard.Popup\n                className={cn(\n                  floating,\n                  \"z-50 w-60 origin-(--transform-origin) rounded-2xl p-3.5 outline-none\",\n                  \"transition-[opacity,scale] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none\",\n                  \"data-[starting-style]:scale-[0.97] data-[starting-style]:opacity-0\",\n                  \"data-[ending-style]:scale-[0.97] data-[ending-style]:opacity-0\",\n                )}\n              >\n                <p className=\"line-clamp-2 text-[13px] leading-snug font-medium\">\n                  {payload?.title}\n                </p>\n                {payload?.preview && (\n                  <p className=\"text-foreground/50 mt-1 line-clamp-3 text-[13px] leading-relaxed\">\n                    {payload.preview}\n                  </p>\n                )}\n              </PreviewCard.Popup>\n            </PreviewCard.Positioner>\n          </PreviewCard.Portal>\n        )}\n      </PreviewCard.Root>\n    </nav>\n  );\n}\n"
    }
  ]
}