{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thread",
  "type": "registry:component",
  "title": "Thread",
  "description": "A comment and its replies in one tidy card, where @mentions stand out and an assistant's reply opens up to show each step it took.",
  "categories": [
    "component",
    "Together"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/thread.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { CheckIcon, ChevronDownIcon, CircleSlashIcon, CornerDownLeftIcon, SparklesIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type FormEvent, Fragment, type ReactNode, useId, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ThreadTone = \"info\" | \"success\" | \"warning\" | \"destructive\" | \"muted\";\n\nexport interface ThreadAuthor {\n  name: string;\n  initials: string;\n  tone?: ThreadTone;\n  assistant?: boolean;\n}\n\nexport interface ThreadStep {\n  label: string;\n  detail?: string;\n  status?: \"done\" | \"skipped\";\n}\n\nexport interface ThreadMessage {\n  id: string;\n  author: ThreadAuthor;\n  time: string;\n  body: string;\n  steps?: ThreadStep[];\n}\n\nexport interface ThreadProps {\n  comment: ThreadMessage;\n  replies?: ThreadMessage[];\n  currentUser?: ThreadAuthor;\n  mentionable?: string[];\n  className?: string;\n}\n\nconst toneClass: Record<ThreadTone, string> = {\n  info: \"bg-info/12 text-info-foreground\",\n  success: \"bg-success/12 text-success-foreground\",\n  warning: \"bg-warning/14 text-warning-foreground\",\n  destructive: \"bg-destructive/10 text-destructive-foreground\",\n  muted: \"bg-muted text-foreground\",\n};\n\nconst spring = { type: \"spring\", stiffness: 420, damping: 32 } as const;\n\nfunction Initials({ author, size = \"md\" }: { author: ThreadAuthor; size?: \"sm\" | \"md\" }) {\n  return (\n    <span\n      aria-hidden\n      className={cn(\n        \"relative flex shrink-0 select-none items-center justify-center rounded-full font-medium\",\n        size === \"md\" ? \"size-8 text-xs\" : \"size-6 text-[10px]\",\n        author.assistant ? \"bg-primary text-primary-foreground\" : toneClass[author.tone ?? \"muted\"],\n      )}\n    >\n      {author.assistant ? <SparklesIcon className={size === \"md\" ? \"size-4\" : \"size-3\"} /> : author.initials}\n    </span>\n  );\n}\n\nfunction renderBody(text: string, mentionable: string[]): ReactNode {\n  const names = mentionable.map((n) => n.toLowerCase());\n  return text.split(/(@[\\p{L}\\d_-]+)/u).map((part, i) => {\n    const isMention = part.startsWith(\"@\") && (names.length === 0 || names.includes(part.slice(1).toLowerCase()));\n    if (!isMention) return <Fragment key={i}>{part}</Fragment>;\n    return (\n      <span\n        className=\"rounded-md bg-info/10 px-1 py-px font-medium text-info-foreground [box-decoration-break:clone]\"\n        key={i}\n      >\n        {part}\n      </span>\n    );\n  });\n}\n\nfunction AssistantSteps({ steps }: { steps: ThreadStep[] }) {\n  const [open, setOpen] = useState(false);\n  const reduce = useReducedMotion();\n  const panelId = useId();\n  const done = steps.filter((s) => s.status !== \"skipped\").length;\n\n  return (\n    <div className=\"mt-2 overflow-hidden rounded-xl border bg-muted/40\">\n      <button\n        aria-controls={panelId}\n        aria-expanded={open}\n        className=\"flex min-h-9 w-full cursor-pointer items-center gap-2 px-3 py-2 text-left text-muted-foreground text-xs outline-none transition-colors hover:bg-accent/50 hover:text-foreground focus-visible:bg-accent/50 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset\"\n        onClick={() => setOpen((o) => !o)}\n        type=\"button\"\n      >\n        <span className=\"flex size-4 items-center justify-center rounded-full bg-success/16 text-success-foreground\">\n          <CheckIcon className=\"size-2.5\" strokeWidth={3} />\n        </span>\n        <span className=\"font-medium text-foreground\">\n          Did {done} {done === 1 ? \"thing\" : \"things\"}\n        </span>\n        {steps.length > done && <span className=\"tabular-nums\">· {steps.length - done} skipped</span>}\n        <motion.span\n          animate={{ rotate: open ? 180 : 0 }}\n          className=\"ms-auto\"\n          transition={reduce ? { duration: 0 } : spring}\n        >\n          <ChevronDownIcon className=\"size-3.5\" />\n        </motion.span>\n      </button>\n      <AnimatePresence initial={false}>\n        {open && (\n          <motion.div\n            animate={{ height: \"auto\", opacity: 1 }}\n            exit={{ height: 0, opacity: 0 }}\n            id={panelId}\n            initial={{ height: 0, opacity: 0 }}\n            transition={reduce ? { duration: 0 } : { ...spring, opacity: { duration: 0.15 } }}\n          >\n            <ol className=\"relative space-y-2.5 border-t px-3 py-3\">\n              {steps.map((step, i) => {\n                const skipped = step.status === \"skipped\";\n                return (\n                  <motion.li\n                    animate={{ opacity: 1, x: 0 }}\n                    className=\"relative flex gap-2.5 text-xs\"\n                    initial={reduce ? false : { opacity: 0, x: -6 }}\n                    key={step.label}\n                    transition={{ ...spring, delay: reduce ? 0 : 0.04 * i }}\n                  >\n                    {i < steps.length - 1 && (\n                      <span aria-hidden className=\"absolute top-5 bottom-[-12px] left-[7px] w-px bg-border\" />\n                    )}\n                    <span\n                      className={cn(\n                        \"relative mt-0.5 flex size-[15px] shrink-0 items-center justify-center rounded-full\",\n                        skipped ? \"bg-muted text-muted-foreground\" : \"bg-success text-white\",\n                      )}\n                    >\n                      {skipped ? (\n                        <CircleSlashIcon className=\"size-2.5\" />\n                      ) : (\n                        <CheckIcon className=\"size-2.5\" strokeWidth={3} />\n                      )}\n                    </span>\n                    <span className=\"min-w-0\">\n                      <span className={cn(\"block font-medium\", skipped && \"text-muted-foreground line-through\")}>\n                        {step.label}\n                      </span>\n                      {step.detail && <span className=\"block text-muted-foreground tabular-nums\">{step.detail}</span>}\n                    </span>\n                  </motion.li>\n                );\n              })}\n            </ol>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nfunction Message({\n  message,\n  mentionable,\n  root,\n  last,\n}: {\n  message: ThreadMessage;\n  mentionable: string[];\n  root?: boolean;\n  last?: boolean;\n}) {\n  return (\n    <div className=\"relative flex gap-3\">\n      {!last && <span aria-hidden className=\"absolute top-10 bottom-0 left-4 w-px -translate-x-1/2 bg-border\" />}\n      <Initials author={message.author} />\n      <div className={cn(\"min-w-0 flex-1\", !last && \"pb-4\")}>\n        <div className=\"flex items-baseline gap-2\">\n          <span className=\"truncate font-medium text-sm\">{message.author.name}</span>\n          {message.author.assistant && (\n            <span className=\"rounded-full bg-muted px-1.5 py-px font-medium text-[10px] text-muted-foreground uppercase tracking-wide\">\n              AI\n            </span>\n          )}\n          <span className=\"shrink-0 text-muted-foreground text-xs\">{message.time}</span>\n        </div>\n        <p className={cn(\"mt-0.5 text-pretty text-sm leading-relaxed\", !root && \"text-foreground/90\")}>\n          {renderBody(message.body, mentionable)}\n        </p>\n        {message.steps && message.steps.length > 0 && <AssistantSteps steps={message.steps} />}\n      </div>\n    </div>\n  );\n}\n\nexport function Thread({\n  comment,\n  replies = [],\n  currentUser = { name: \"You\", initials: \"YO\", tone: \"muted\" },\n  mentionable = [],\n  className,\n}: ThreadProps) {\n  const reduce = useReducedMotion();\n  const inputId = useId();\n  const [items, setItems] = useState<ThreadMessage[]>(replies);\n  const [draft, setDraft] = useState(\"\");\n  const [resolved, setResolved] = useState(false);\n\n  const submit = (e: FormEvent) => {\n    e.preventDefault();\n    const body = draft.trim();\n    if (!body) return;\n    setItems((prev) => [...prev, { id: `r-${Date.now()}`, author: currentUser, time: \"now\", body }]);\n    setDraft(\"\");\n  };\n\n  const all = [comment, ...items];\n\n  return (\n    <section\n      aria-label={`Thread started by ${comment.author.name}`}\n      className={cn(\"w-full max-w-[440px] rounded-2xl border bg-card text-card-foreground shadow-xs/5\", className)}\n    >\n      <header className=\"flex items-center justify-between gap-3 border-b px-4 py-2.5\">\n        <span className=\"text-muted-foreground text-xs\">\n          <span className=\"font-medium text-foreground tabular-nums\">{all.length}</span> messages\n        </span>\n        <button\n          aria-pressed={resolved}\n          className={cn(\n            \"relative inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-full px-2.5 font-medium text-xs outline-none transition-[color,background-color,scale] duration-150 ease-out after:absolute after:-inset-x-0.5 after:-inset-y-1 focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97]\",\n            resolved ? \"bg-success/12 text-success-foreground\" : \"text-muted-foreground hover:bg-accent hover:text-foreground\",\n          )}\n          onClick={() => setResolved((r) => !r)}\n          type=\"button\"\n        >\n          <motion.span\n            animate={resolved && !reduce ? { scale: [1, 1.25, 1] } : { scale: 1 }}\n            className=\"flex\"\n            transition={{ duration: 0.28, ease: [0.23, 1, 0.32, 1] }}\n          >\n            <CheckIcon className=\"size-3.5\" />\n          </motion.span>\n          <span className=\"grid text-start\">\n            <span aria-hidden className=\"invisible col-start-1 row-start-1\">\n              Resolved\n            </span>\n            <span className=\"col-start-1 row-start-1\">{resolved ? \"Resolved\" : \"Resolve\"}</span>\n          </span>\n        </button>\n      </header>\n\n      <div className={cn(\"px-4 pt-4 transition-opacity\", resolved && \"opacity-60\")}>\n        <AnimatePresence initial={false}>\n          {all.map((m, i) => (\n            <motion.div\n              animate={{ opacity: 1, y: 0, height: \"auto\" }}\n              className=\"overflow-hidden\"\n              initial={reduce ? false : { opacity: 0, y: 8, height: 0 }}\n              key={m.id}\n              transition={spring}\n            >\n              <Message last={i === all.length - 1} mentionable={mentionable} message={m} root={i === 0} />\n            </motion.div>\n          ))}\n        </AnimatePresence>\n      </div>\n\n      <form className=\"flex items-center gap-3 px-4 pt-3 pb-4\" onSubmit={submit}>\n        <span className=\"flex w-8 shrink-0 justify-center\">\n          <Initials author={currentUser} size=\"sm\" />\n        </span>\n        <label className=\"sr-only\" htmlFor={inputId}>\n          Reply\n        </label>\n        <div className=\"relative flex min-w-0 flex-1 items-center rounded-full border bg-background ps-3 pe-1 transition-shadow focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/24 dark:bg-input/32\">\n          <input\n            autoComplete=\"off\"\n            className=\"h-8 min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground/72\"\n            disabled={resolved}\n            id={inputId}\n            onChange={(e) => setDraft(e.target.value)}\n            placeholder={resolved ? \"Thread resolved\" : \"Reply, @mention someone…\"}\n            value={draft}\n          />\n          <motion.button\n            animate={{ scale: draft.trim() ? 1 : 0.85, opacity: draft.trim() ? 1 : 0.4 }}\n            aria-label=\"Send reply\"\n            className=\"relative flex size-6 cursor-pointer items-center justify-center rounded-full bg-primary text-primary-foreground outline-none transition-[scale] duration-150 ease-out after:absolute after:-inset-1 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background active:scale-[0.97] disabled:cursor-default\"\n            disabled={!draft.trim()}\n            transition={reduce ? { duration: 0 } : spring}\n            type=\"submit\"\n          >\n            <CornerDownLeftIcon className=\"size-3.5\" />\n          </motion.button>\n        </div>\n      </form>\n    </section>\n  );\n}\n"
    }
  ]
}