{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thread-rail",
  "type": "registry:component",
  "title": "ThreadRail",
  "description": "Conversation minimap for scanning and jumping between chat turns.",
  "dependencies": [],
  "registryDependencies": [
    "https://control-ui.dev/r/chat-message.json",
    "https://control-ui.dev/r/core.json"
  ],
  "files": [
    {
      "path": "src/registry/knob-contracts/thread-rail-knobs.ts",
      "target": "@components/control-ui/knob-contracts/thread-rail-knobs.ts",
      "type": "registry:component",
      "content": "// Generated from src/registry/sources/control-ui/recipes/thread-rail.css by scripts/gen-knob-contracts.ts — run `bun run sync:knobs`.\nexport const threadRailKnobs = [\n  \"--cui-thread-rail-item-radius\",\n  \"--cui-thread-rail-item-background\",\n  \"--cui-thread-rail-line-radius\",\n  \"--cui-thread-rail-popover-radius\",\n  \"--cui-thread-rail-popover-background\",\n  \"--cui-thread-rail-popover-border-color\",\n  \"--cui-thread-rail-popover-border-width\",\n  \"--cui-thread-rail-popover-shadow\",\n  \"--cui-thread-rail-popover-backdrop-blur\",\n] as const;\nexport type ThreadRailKnobStyle = Partial<Record<(typeof threadRailKnobs)[number], string>>;\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/thread-rail.css",
      "target": "@components/control-ui/styles/recipes/thread-rail.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"thread-rail\"][data-slot=\"root\"]) {\n    --cui-thread-rail-item-radius: 9999px;\n    --cui-thread-rail-item-background: transparent;\n    --cui-thread-rail-line-radius: 9999px;\n    --cui-thread-rail-popover-radius: var(--radius-popover);\n    --cui-thread-rail-popover-background: var(--popover);\n    --cui-thread-rail-popover-border-color: var(--border);\n    --cui-thread-rail-popover-border-width: 1px;\n    --cui-thread-rail-popover-shadow: var(--shadow-pop);\n    --cui-thread-rail-popover-backdrop-blur: var(--backdrop-blur-popover);\n    --aui-rail-pitch: 9px;\n    --aui-rail-inset: 12.5px;\n    position: relative;\n  }\n\n  :where([data-control-family=\"thread-rail\"][data-slot=\"item\"]) {\n    border-radius: var(--cui-thread-rail-item-radius);\n    background: var(--cui-thread-rail-item-background);\n  }\n\n  :where([data-control-family=\"thread-rail\"][data-slot=\"line\"]) {\n    width: 0.5rem;\n    border-radius: var(--cui-thread-rail-line-radius);\n    background: oklch(from var(--foreground) l c h / 0.2);\n    outline: none;\n    transition:\n      width var(--duration-base) var(--ease-standard),\n      background-color var(--duration-base) var(--ease-standard),\n      box-shadow var(--duration-base) var(--ease-standard);\n  }\n\n  :where([data-control-family=\"thread-rail\"][data-slot=\"item\"][data-in-view] [data-control-family=\"thread-rail\"][data-slot=\"line\"]) {\n    background: oklch(from var(--foreground) l c h / 0.6);\n  }\n\n  :where(\n    [data-control-family=\"thread-rail\"][data-slot=\"item\"]:has(+ [data-control-family=\"thread-rail\"][data-slot=\"item\"]:hover)\n      [data-control-family=\"thread-rail\"][data-slot=\"line\"]\n  ),\n  :where(\n    [data-control-family=\"thread-rail\"][data-slot=\"item\"]:hover\n      + [data-control-family=\"thread-rail\"][data-slot=\"item\"]\n      [data-control-family=\"thread-rail\"][data-slot=\"line\"]\n  ),\n  :where(\n    [data-control-family=\"thread-rail\"][data-slot=\"item\"]:has(+ [data-control-family=\"thread-rail\"][data-slot=\"item\"]:focus-within)\n      [data-control-family=\"thread-rail\"][data-slot=\"line\"]\n  ),\n  :where(\n    [data-control-family=\"thread-rail\"][data-slot=\"item\"]:focus-within\n      + [data-control-family=\"thread-rail\"][data-slot=\"item\"]\n      [data-control-family=\"thread-rail\"][data-slot=\"line\"]\n  ) {\n    width: 0.75rem;\n    background: oklch(from var(--foreground) l c h / 0.45);\n  }\n\n  :where(\n    [data-control-family=\"thread-rail\"][data-slot=\"item\"]:where(:hover, :focus-within) [data-control-family=\"thread-rail\"][data-slot=\"line\"]\n  ) {\n    width: 1rem;\n    background: var(--foreground);\n  }\n\n  :where([data-control-family=\"thread-rail\"][data-slot=\"item\"][data-rail-current] [data-control-family=\"thread-rail\"][data-slot=\"line\"]) {\n    anchor-name: --aui-rail-active;\n  }\n\n  :where([data-control-family=\"thread-rail\"][data-slot=\"popover\"]) {\n    position: absolute;\n    inset-inline-start: 100%;\n    top: 0;\n    translate: 0 calc(var(--aui-rail-inset) + var(--aui-rail-active-index, 0) * var(--aui-rail-pitch) - 50%);\n    scale: 0.98;\n    opacity: 0;\n    border-radius: var(--cui-thread-rail-popover-radius);\n    background: var(--cui-thread-rail-popover-background);\n    padding: 0px;\n    box-shadow:\n      inset 0 0 0 var(--cui-thread-rail-popover-border-width) var(--cui-thread-rail-popover-border-color),\n      var(--cui-thread-rail-popover-shadow);\n    color: var(--popover-foreground);\n    -webkit-backdrop-filter: blur(var(--cui-thread-rail-popover-backdrop-blur));\n    backdrop-filter: blur(var(--cui-thread-rail-popover-backdrop-blur));\n    transition:\n      top var(--duration-slow) var(--ease-standard),\n      translate var(--duration-slow) var(--ease-standard),\n      height var(--duration-slow) var(--ease-standard),\n      opacity var(--duration-base) var(--ease-standard),\n      scale var(--duration-base) var(--ease-standard);\n    will-change: top, translate, height, opacity, scale;\n    text-align: left;\n  }\n\n  :where(\n    [data-control-family=\"thread-rail\"][data-slot=\"root\"]:has([data-control-family=\"thread-rail\"][data-slot=\"item\"]:hover)\n      [data-control-family=\"thread-rail\"][data-slot=\"popover\"]\n  ),\n  :where([data-control-family=\"thread-rail\"][data-slot=\"root\"]:focus-within [data-control-family=\"thread-rail\"][data-slot=\"popover\"]) {\n    scale: 1;\n    opacity: 1;\n  }\n\n  @starting-style {\n    :where(\n        [data-control-family=\"thread-rail\"][data-slot=\"root\"]:has([data-control-family=\"thread-rail\"][data-slot=\"item\"]:hover)\n          [data-control-family=\"thread-rail\"][data-slot=\"popover\"]\n      ),\n    :where([data-control-family=\"thread-rail\"][data-slot=\"root\"]:focus-within [data-control-family=\"thread-rail\"][data-slot=\"popover\"]) {\n      scale: 0.98;\n      opacity: 0;\n    }\n  }\n\n  @supports (top: anchor(--aui-rail-active center)) {\n    :where([data-control-family=\"thread-rail\"][data-slot=\"popover\"]) {\n      top: anchor(--aui-rail-active center);\n      translate: 0 -50%;\n    }\n  }\n\n  :where([data-control-family=\"thread-rail\"][data-slot=\"popover-layer\"]) {\n    position: absolute;\n    inset-inline: 0;\n    top: 0;\n    transform-origin: left center;\n    scale: 0.96;\n    opacity: 0;\n    filter: blur(4px);\n    transition:\n      opacity var(--duration-base) var(--ease-standard),\n      scale var(--duration-base) var(--ease-standard),\n      filter var(--duration-base) var(--ease-standard);\n  }\n\n  :where(\n    [data-control-family=\"thread-rail\"][data-slot=\"root\"]:is(\n        :has([data-control-family=\"thread-rail\"][data-slot=\"item\"]:hover),\n        :focus-within\n      )\n      [data-control-family=\"thread-rail\"][data-slot=\"popover-layer\"][data-active]\n  ) {\n    scale: 1;\n    opacity: 1;\n    filter: blur(0);\n  }\n\n  :where([data-control-family=\"thread-rail\"][data-slot=\"title\"]) {\n    color: var(--foreground);\n    font-size: var(--text-body);\n    font-weight: 500;\n  }\n\n  :where([data-control-family=\"thread-rail\"][data-slot=\"summary\"]),\n  :where([data-control-family=\"thread-rail\"][data-slot=\"file\"]),\n  :where([data-control-family=\"thread-rail\"][data-slot=\"file-icon\"]),\n  :where([data-control-family=\"thread-rail\"][data-slot=\"more\"]) {\n    color: var(--muted-foreground);\n    font-size: var(--text-label);\n  }\n\n  :where([data-control-family=\"thread-rail\"][data-slot=\"file-icon\"]) {\n    opacity: 0.7;\n    line-height: 1;\n  }\n\n  :where([data-control-family=\"thread-rail\"][data-slot=\"more\"]) {\n    font-weight: 500;\n  }\n\n  :where([data-control-family=\"thread-rail\"][data-slot=\"footer\"]) {\n    border-block-start: 1px solid oklch(from var(--border) l c h / 0.6);\n  }\n\n  :where([data-control-family=\"thread-rail\"][data-slot=\"summary\"]) {\n    line-height: calc(var(--spacing) * 5);\n  }\n}\n\n@property --cui-thread-rail-item-radius {\n  syntax: \"<length-percentage>\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-thread-rail-item-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-thread-rail-line-radius {\n  syntax: \"<length-percentage>\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-thread-rail-popover-radius {\n  syntax: \"<length-percentage>\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-thread-rail-popover-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-thread-rail-popover-border-color {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n\n@property --cui-thread-rail-popover-border-width {\n  syntax: \"<length>\";\n  inherits: true;\n  initial-value: 0px;\n}\n\n@property --cui-thread-rail-popover-shadow {\n  syntax: \"*\";\n  inherits: true;\n}\n\n@property --cui-thread-rail-popover-backdrop-blur {\n  syntax: \"<length>\";\n  inherits: true;\n  initial-value: 0px;\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/thread-rail.tsx",
      "target": "@components/control-ui/thread-rail.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport type { ComponentProps, CSSProperties, ReactElement, ReactNode } from \"react\";\nimport { Children, cloneElement, createContext, isValidElement, useContext, useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ChatRole } from \"@/components/control-ui/hooks/use-chat-message\";\nimport type { ThreadRailKnobStyle } from \"@/components/control-ui/knob-contracts/thread-rail-knobs\";\nimport { cn } from \"@/components/control-ui/lib/cn\";\n\nexport type ThreadRailProps = ComponentProps<\"nav\"> & { style?: CSSProperties & ThreadRailKnobStyle };\n\nexport type ThreadRailItemProps = Omit<ComponentProps<\"div\">, \"style\"> & {\n  from?: ChatRole;\n  inView?: boolean;\n  active?: boolean;\n  style?: CSSProperties & ThreadRailKnobStyle;\n};\n\n// thread-rail.css owns fisheye, visibility, slide, and enter/exit. React keeps only current turn index\n// and one measured height, because no stable CSS interpolates one intrinsic size to another.\n\n// plain effect on server so measuring never warns during SSR\nconst useIsomorphicLayoutEffect = typeof window === \"undefined\" ? useEffect : useLayoutEffect;\n\ntype RailContextValue = { current: number; activate: (index: number) => void };\nconst RailContext = createContext<RailContextValue | null>(null);\n\nfunction useRail() {\n  const context = useContext(RailContext);\n  if (!context) throw new Error(\"ThreadRailItem must be rendered inside <ThreadRail>.\");\n  return context;\n}\n\ntype ItemContextValue = { index: number; inView: boolean; activate: (index: number) => void };\nconst ItemContext = createContext<ItemContextValue | null>(null);\n\nfunction useItem() {\n  const context = useContext(ItemContext);\n  if (!context) throw new Error(\"ThreadRail compound parts must be rendered inside <ThreadRailItem>.\");\n  return context;\n}\n\nexport function ThreadRail({ className, children, style, ...props }: ThreadRailProps) {\n  const [hovered, setHovered] = useState(0);\n  const cardRef = useRef<HTMLDivElement>(null);\n  const activeLayerRef = useRef<HTMLDivElement>(null);\n  const [cardHeight, setCardHeight] = useState<number>();\n\n  const items = Children.toArray(children).filter((child): child is ReactElement<ThreadRailItemProps & { index?: number }> =>\n    isValidElement(child),\n  );\n  const popovers = items.map((item) => {\n    const popover = Children.toArray(item.props.children).find((child) => isValidElement(child) && child.type === ThreadRailPopover);\n    return isValidElement<ThreadRailPopoverProps>(popover) ? popover.props : undefined;\n  });\n  const activePopover = popovers[hovered];\n\n  // `transition: height` needs explicit border-box height; offsetHeight ignores cross-fade scale, and offset/client delta re-adds borders whatever skin\n  useIsomorphicLayoutEffect(() => {\n    const card = cardRef.current;\n    const activeLayer = activeLayerRef.current;\n    if (!card || !activeLayer) return;\n    setCardHeight(activeLayer.offsetHeight + card.offsetHeight - card.clientHeight);\n  }, [hovered]);\n\n  // fallback for browsers without anchor positioning; anchor path ignores it\n  const railStyle: CSSProperties & Record<\"--aui-rail-active-index\", number> = {\n    ...style,\n    \"--aui-rail-active-index\": hovered,\n  };\n\n  return (\n    <RailContext.Provider value={{ current: hovered, activate: setHovered }}>\n      <nav\n        data-control-ui=\"thread-rail\"\n        data-control-family=\"thread-rail\"\n        data-slot=\"root\"\n        aria-label=\"Conversation timeline\"\n        style={railStyle}\n        // widest fisheye tick gets — fixed column stops card drifting as ticks resize\n        className={cn(\"flex w-4 flex-col\", className)}\n        {...props}\n      >\n        {items.map((item, index) => cloneElement(item, { index }))}\n\n        <div\n          ref={cardRef}\n          data-control-ui=\"thread-rail\"\n          data-control-family=\"thread-rail\"\n          data-slot=\"popover\"\n          data-surface=\"floating\"\n          style={{ ...activePopover?.style, height: cardHeight }}\n          className={cn(\"pointer-events-none z-20 ms-3 w-72 overflow-hidden\", activePopover?.className)}\n        >\n          {items.map((item, index) => (\n            <div\n              data-control-ui=\"thread-rail\"\n              data-control-family=\"thread-rail\"\n              data-slot=\"popover-layer\"\n              key={item.key ?? index}\n              ref={index === hovered ? activeLayerRef : undefined}\n              data-active={index === hovered ? \"true\" : undefined}\n              className=\"p-3.5\"\n            >\n              {popovers[index]?.children ?? null}\n            </div>\n          ))}\n        </div>\n      </nav>\n    </RailContext.Provider>\n  );\n}\n\nexport function ThreadRailItem({\n  from = \"user\",\n  inView = false,\n  active = false,\n  index = -1,\n  className,\n  children,\n  ...props\n}: ThreadRailItemProps & { index?: number }) {\n  const rail = useRail();\n  const isInView = inView || active;\n\n  return (\n    <ItemContext.Provider value={{ index, inView: isInView, activate: rail.activate }}>\n      <div\n        data-control-ui=\"thread-rail\"\n        data-control-family=\"thread-rail\"\n        data-slot=\"item\"\n        data-from={from}\n        data-in-view={isInView ? \"true\" : undefined}\n        data-active={isInView ? \"true\" : undefined}\n        // driven by state, not :hover, so card's anchor survives pointer leaving\n        data-rail-current={index === rail.current ? \"true\" : undefined}\n        // tiles rows with no gap, so :hover travels tick to tick without ever losing anchor\n        className={cn(\"relative flex items-center py-1\", className)}\n        {...props}\n      >\n        {children}\n      </div>\n    </ItemContext.Provider>\n  );\n}\n\nexport type ThreadRailLineProps = Omit<ComponentProps<\"button\">, \"style\"> & {\n  style?: CSSProperties & ThreadRailKnobStyle;\n};\n\nexport function ThreadRailLine({ className, ...props }: ThreadRailLineProps) {\n  const { index, inView, activate } = useItem();\n\n  return (\n    <button\n      data-control-ui=\"thread-rail\"\n      data-control-family=\"thread-rail\"\n      data-slot=\"line\"\n      type=\"button\"\n      aria-current={inView ? \"location\" : undefined}\n      onMouseEnter={() => activate(index)}\n      onFocus={() => activate(index)}\n      className={cn(\"relative block h-px cursor-pointer before:absolute before:-inset-y-1 before:inset-x-0 before:content-['']\", className)}\n      {...props}\n    />\n  );\n}\n\nexport type ThreadRailPopoverProps = Omit<ComponentProps<\"div\">, \"style\"> & {\n  children?: ReactNode;\n  style?: CSSProperties & ThreadRailKnobStyle;\n};\n\nexport function ThreadRailPopover(_props: ThreadRailPopoverProps) {\n  return null;\n}\n\nexport type ThreadRailTitleProps = Omit<ComponentProps<\"div\">, \"style\"> & {\n  style?: CSSProperties & ThreadRailKnobStyle;\n};\n\nexport function ThreadRailTitle({ className, ...props }: ThreadRailTitleProps) {\n  return (\n    <div\n      data-control-ui=\"thread-rail\"\n      data-control-family=\"thread-rail\"\n      data-slot=\"title\"\n      className={cn(\"truncate\", className)}\n      {...props}\n    />\n  );\n}\nexport type ThreadRailSummaryProps = Omit<ComponentProps<\"p\">, \"style\"> & {\n  style?: CSSProperties & ThreadRailKnobStyle;\n};\n\nexport function ThreadRailSummary({ className, ...props }: ThreadRailSummaryProps) {\n  return (\n    <p\n      data-control-ui=\"thread-rail\"\n      data-control-family=\"thread-rail\"\n      data-slot=\"summary\"\n      className={cn(\"mt-1.5 line-clamp-3\", className)}\n      {...props}\n    />\n  );\n}\n\nexport type ThreadRailFooterProps = Omit<ComponentProps<\"div\">, \"style\"> & {\n  style?: CSSProperties & ThreadRailKnobStyle;\n};\n\nexport function ThreadRailFooter({ className, ...props }: ThreadRailFooterProps) {\n  return (\n    <div\n      data-control-ui=\"thread-rail\"\n      data-control-family=\"thread-rail\"\n      data-slot=\"footer\"\n      className={cn(\"mt-3 flex flex-wrap items-center gap-2 pt-2.5\", className)}\n      {...props}\n    />\n  );\n}\n\nexport type ThreadRailFileProps = Omit<ComponentProps<\"span\">, \"style\"> & {\n  style?: CSSProperties & ThreadRailKnobStyle;\n};\n\nexport function ThreadRailFile({ className, ...props }: ThreadRailFileProps) {\n  return (\n    <span\n      data-control-ui=\"thread-rail\"\n      data-control-family=\"thread-rail\"\n      data-slot=\"file\"\n      className={cn(\"inline-flex max-w-[180px] items-center gap-1.5 truncate\", className)}\n      {...props}\n    />\n  );\n}\n\nexport type ThreadRailFileIconProps = Omit<ComponentProps<\"span\">, \"style\"> & {\n  style?: CSSProperties & ThreadRailKnobStyle;\n};\n\nexport function ThreadRailFileIcon({ className, children, ...props }: ThreadRailFileIconProps) {\n  return (\n    <span\n      data-control-ui=\"thread-rail\"\n      data-control-family=\"thread-rail\"\n      data-slot=\"file-icon\"\n      className={cn(\"inline-flex size-3.5 shrink-0 items-center justify-center\", className)}\n      aria-hidden\n      {...props}\n    >\n      {children ?? (\n        <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"size-3\" role=\"presentation\">\n          <path\n            d=\"M4 1.5h5L13 5.5V14a.5.5 0 0 1-.5.5h-8A.5.5 0 0 1 4 14V2a.5.5 0 0 1 .5-.5Z\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.1\"\n            strokeLinejoin=\"round\"\n          />\n          <path d=\"M9 1.5V5.5h4\" stroke=\"currentColor\" strokeWidth=\"1.1\" strokeLinejoin=\"round\" />\n        </svg>\n      )}\n    </span>\n  );\n}\nexport type ThreadRailMoreProps = Omit<ComponentProps<\"span\">, \"style\"> & {\n  style?: CSSProperties & ThreadRailKnobStyle;\n};\n\nexport function ThreadRailMore({ className, ...props }: ThreadRailMoreProps) {\n  return <span data-control-ui=\"thread-rail\" data-control-family=\"thread-rail\" data-slot=\"more\" className={className} {...props} />;\n}\n"
    }
  ],
  "css": {
    "@import \"../components/control-ui/styles/recipes/thread-rail.css\"": {}
  },
  "meta": {}
}
