{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"thread-list","type":"registry:block","title":"Thread List","description":"Sidebar conversation list: section headers with action slots, 30px rows with unread dots and time metas that swap to hover actions, and an active pill that glides between rows via shared layout.","author":"UI Lab","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"cssVars":{"light":{"wb-hover":"rgb(0 0 0 / 0.05)","wb-hover-subtle":"rgb(0 0 0 / 0.04)","wb-hover-strong":"rgb(0 0 0 / 0.1)","wb-hover-stronger":"rgb(0 0 0 / 0.1)","wb-inset":"rgb(0 0 0 / 0.05)","wb-inset-strong":"rgb(0 0 0 / 0.06)","wb-inset-subtle":"rgb(0 0 0 / 0.03)","wb-inset-faint":"rgb(0 0 0 / 0.02)","wb-code-inline":"rgb(0 0 0 / 0.07)","wb-code-block":"rgb(0 0 0 / 0.04)","wb-border":"rgb(0 0 0 / 0.12)","wb-border-subtle":"rgb(0 0 0 / 0.1)","wb-border-strong":"rgb(0 0 0 / 0.17)","wb-border-emphasis":"rgb(0 0 0 / 0.22)","wb-divider":"rgb(0 0 0 / 0.07)","wb-hairline":"rgb(0 0 0 / 0.12)","wb-hairline-soft":"rgb(0 0 0 / 0.1)","wb-hairline-subtle":"rgb(0 0 0 / 0.08)","wb-control-hairline":"rgb(0 0 0 / 0.08)","wb-control-tick":"rgb(0 0 0 / 0.2)","wb-resize-handle":"rgb(0 0 0 / 0.2)","wb-surface":"rgb(255 255 255)","wb-surface-translucent":"oklch(94.5% 0.002 250 / 0.75)","wb-overlay-scrim":"rgb(0 0 0 / 0.06)","wb-surface-raised":"rgb(255 255 255 / 0.95)","wb-surface-composer":"rgb(255 255 255 / 0.9)","wb-card":"rgb(0 0 0 / 0.04)","wb-inverse":"oklch(20.5% 0 0)","wb-inverse-fg":"rgb(255 255 255)","wb-accent-fg":"rgb(255 255 255)","wb-solid-control-fg":"oklch(20.5% 0 0)","wb-shimmer-dim":"rgb(0 0 0 / 0.4)","wb-shimmer-bright":"rgb(0 0 0 / 0.85)","wb-accent":"#339cff","wb-success":"oklch(59.6% 0.145 163.225)","wb-success-border":"oklch(69.6% 0.17 162.48)","wb-danger":"oklch(63.7% 0.237 25.331)","wb-danger-strong":"#fa423e","wb-danger-surface":"oklch(63.7% 0.237 25.331)","wb-warning":"#e25507","wb-warning-text":"oklch(64.6% 0.222 41.116)","wb-warning-hover":"oklch(70.5% 0.213 47.604)","wb-warning-surface":"#e25507"},"dark":{"wb-hover":"rgb(255 255 255 / 0.1)","wb-hover-subtle":"rgb(255 255 255 / 0.05)","wb-hover-strong":"rgb(255 255 255 / 0.1)","wb-hover-stronger":"rgb(255 255 255 / 0.15)","wb-inset":"rgb(255 255 255 / 0.05)","wb-inset-strong":"rgb(255 255 255 / 0.1)","wb-inset-subtle":"rgb(255 255 255 / 0.05)","wb-inset-faint":"rgb(255 255 255 / 0.03)","wb-code-inline":"rgb(255 255 255 / 0.1)","wb-code-block":"rgb(255 255 255 / 0.05)","wb-border":"rgb(255 255 255 / 0.1)","wb-border-subtle":"rgb(255 255 255 / 0.1)","wb-border-strong":"rgb(255 255 255 / 0.15)","wb-border-emphasis":"rgb(255 255 255 / 0.2)","wb-divider":"rgb(255 255 255 / 0.06)","wb-hairline":"rgb(255 255 255 / 0.157)","wb-hairline-soft":"rgb(255 255 255 / 0.15)","wb-hairline-subtle":"rgb(255 255 255 / 0.1)","wb-control-hairline":"rgb(255 255 255 / 0.08)","wb-control-tick":"rgb(255 255 255 / 0.3)","wb-resize-handle":"rgb(255 255 255 / 0.25)","wb-surface":"oklch(20.5% 0 0)","wb-surface-translucent":"oklch(20.5% 0 0 / 0.6)","wb-overlay-scrim":"rgb(0 0 0 / 0.24)","wb-surface-raised":"oklch(26.9% 0 0 / 0.95)","wb-surface-composer":"oklch(26.9% 0 0 / 0.9)","wb-card":"oklch(26.9% 0 0 / 0.5)","wb-inverse":"rgb(255 255 255)","wb-inverse-fg":"oklch(20.5% 0 0)","wb-accent-fg":"rgb(255 255 255)","wb-solid-control-fg":"oklch(20.5% 0 0)","wb-shimmer-dim":"rgb(255 255 255 / 0.4)","wb-shimmer-bright":"rgb(255 255 255 / 0.95)","wb-accent":"#339cff","wb-success":"#40c977","wb-success-border":"oklch(69.6% 0.17 162.48)","wb-danger":"#fa423e","wb-danger-strong":"#fa423e","wb-danger-surface":"oklch(63.7% 0.237 25.331)","wb-warning":"#ff8549","wb-warning-text":"#ff8549","wb-warning-hover":"#e25507","wb-warning-surface":"#e25507"}},"files":[{"path":"components/motion/thread-list/index.tsx","type":"registry:component","target":"@components/motion/thread-list/index.tsx","content":"\"use client\";\n// ui-lab-ten.vercel.app/components/blocks/thread-list\n\nimport { LayoutGroup, motion, useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useId } from \"react\";\nimport { SPRING_PANEL } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface ThreadListProps {\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Sidebar conversation/task list root — a tight `flex flex-col` stack of\n * `ThreadListSection`s. Wraps `children` in a `LayoutGroup` scoped to this\n * instance (via `useId`), so the active-row indicator's `layoutId=\"active\"`\n * animation (see `ThreadListItem`) stays local to this list — two\n * `ThreadList`s rendered on the same page never fight over which one owns\n * the moving highlight.\n */\nexport function ThreadList({ className, children }: ThreadListProps) {\n  const groupId = useId();\n\n  return (\n    <LayoutGroup id={groupId}>\n      <div className={cn(\"flex flex-col gap-0.5\", className)}>{children}</div>\n    </LayoutGroup>\n  );\n}\n\nexport interface ThreadListSectionProps {\n  /** Muted section label, e.g. \"Pinned\" or \"Recent\". */\n  title?: ReactNode;\n  /** Trailing slot beside the title, e.g. a \"new\" `ThreadListAction`. */\n  action?: ReactNode;\n  className?: string;\n  children?: ReactNode;\n}\n\n/**\n * Named group of rows inside a `ThreadList`. The header row (label plus an\n * optional trailing action) only renders when `title` or `action` is\n * passed, so a section can also be used as a bare, unlabeled row group.\n */\nexport function ThreadListSection({ title, action, className, children }: ThreadListSectionProps) {\n  return (\n    <div className={cn(\"flex flex-col\", className)}>\n      {title !== undefined || action !== undefined ? (\n        <div className=\"flex items-center justify-between px-2 pt-3 pb-1\">\n          <span className=\"text-xs font-medium text-muted-foreground\">{title}</span>\n          {action}\n        </div>\n      ) : null}\n      {children}\n    </div>\n  );\n}\n\nexport interface ThreadListItemProps {\n  /** Highlights the row and pins the moving indicator here. */\n  active?: boolean;\n  /** Renders a small blue dot after the title. */\n  unread?: boolean;\n  /** 16px leading icon, e.g. `<MessageSquare className=\"h-4 w-4\" />`. */\n  icon?: ReactNode;\n  /** Trailing meta, e.g. a relative timestamp. Hidden on row hover in favor of `actions` when both are passed; stays visible when `actions` is omitted. */\n  meta?: ReactNode;\n  /** Row of `ThreadListAction`s revealed on hover, replacing `meta`. */\n  actions?: ReactNode;\n  onSelect?: () => void;\n  className?: string;\n  /** The row's title — truncates to a single line. */\n  children?: ReactNode;\n}\n\n/**\n * One row in a `ThreadList` — a conversation or task entry. The signature\n * motion is the active indicator: a `layoutId=\"active\"` pill that glides\n * between rows as selection moves (`SPRING_PANEL`), scoped to the enclosing\n * `ThreadList`'s `LayoutGroup` so unrelated lists sharing a page don't\n * collide. Reduced motion swaps the animated pill for a static span.\n *\n * Deliberately *not* a single `<button>` wrapping everything: `actions`\n * renders real `ThreadListAction` buttons (pin, delete, ...), and a\n * `<button>` cannot legally contain another `<button>`. Instead the row is a\n * plain container with the selectable button filling it, and — only when\n * `actions` is passed — a sibling, absolutely-positioned action cluster\n * layered on top at the trailing edge, shown on hover in place of `meta`.\n * Both stay real, independently focusable buttons with no nested-control\n * hit-testing tricks required.\n */\nexport function ThreadListItem({\n  active = false,\n  unread = false,\n  icon,\n  meta,\n  actions,\n  onSelect,\n  className,\n  children,\n}: ThreadListItemProps) {\n  const reduce = useReducedMotion() ?? false;\n  const hasMeta = meta !== undefined && meta !== null;\n  const hasActions = actions !== undefined && actions !== null;\n\n  return (\n    <div className={cn(\"group relative flex h-[30px] w-full\", className)}>\n      <button\n        type=\"button\"\n        onClick={onSelect}\n        aria-current={active || undefined}\n        className={cn(\n          \"relative flex w-full min-w-0 items-center gap-2 rounded-lg px-2 text-left text-sm\",\n          !active && \"hover:bg-[var(--wb-hover-subtle)]\",\n        )}\n      >\n        {active ? (\n          reduce ? (\n            <span aria-hidden className=\"absolute inset-0 rounded-lg bg-[var(--wb-inset-strong)]\" />\n          ) : (\n            <motion.span\n              aria-hidden\n              layoutId=\"active\"\n              className=\"absolute inset-0 rounded-lg bg-[var(--wb-inset-strong)]\"\n              transition={SPRING_PANEL}\n            />\n          )\n        ) : null}\n\n        {icon ? (\n          <span className=\"relative z-10 flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground\">\n            {icon}\n          </span>\n        ) : null}\n\n        <span\n          className={cn(\n            \"relative z-10 min-w-0 flex-1 truncate\",\n            active ? \"text-foreground\" : \"text-muted-foreground group-hover:text-foreground\",\n          )}\n        >\n          {children}\n        </span>\n\n        {unread ? (\n          <span className=\"relative z-10 h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--wb-accent)]\" />\n        ) : null}\n\n        {hasMeta ? (\n          <span\n            className={cn(\n              \"relative z-10 shrink-0 text-xs text-muted-foreground/70\",\n              hasActions && \"group-hover:hidden\",\n            )}\n          >\n            {meta}\n          </span>\n        ) : null}\n      </button>\n\n      {hasActions ? (\n        <span className=\"absolute inset-y-0 right-2 z-10 hidden items-center gap-0.5 group-hover:flex\">\n          {actions}\n        </span>\n      ) : null}\n    </div>\n  );\n}\n\nexport interface ThreadListActionProps {\n  \"aria-label\": string;\n  onClick?: () => void;\n  children?: ReactNode;\n  className?: string;\n}\n\n/** Small 20px icon button for a `ThreadListItem`'s hover-revealed actions\n * (pin, delete, ...) or a `ThreadListSection`'s trailing action slot. */\nexport function ThreadListAction({\n  \"aria-label\": ariaLabel,\n  onClick,\n  children,\n  className,\n}: ThreadListActionProps) {\n  return (\n    <button\n      type=\"button\"\n      aria-label={ariaLabel}\n      onClick={onClick}\n      className={cn(\n        \"flex h-5 w-5 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-[var(--wb-hover-stronger)] hover:text-foreground\",\n        className,\n      )}\n    >\n      {children}\n    </button>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}