{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "handoff-card",
  "type": "registry:component",
  "title": "Handoff Card",
  "description": "Pass work to a teammate with the summary, open questions and links in one card; when they accept, it flips over to show who owns it now.",
  "categories": [
    "component",
    "Together"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/handoff-card.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport {\n  ArrowRightIcon,\n  BellIcon,\n  CheckIcon,\n  CircleHelpIcon,\n  FileTextIcon,\n  GitPullRequestIcon,\n  type LucideIcon,\n  MessageSquareIcon,\n  Undo2Icon,\n} from \"lucide-react\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\n// Stays false until mounted so the server render and the first client render match.\nfunction useSafeReducedMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nexport interface HandoffPerson {\n  name: string;\n  initials: string;\n  avatarClassName?: string;\n}\n\nexport interface HandoffLink {\n  label: string;\n  meta?: string;\n  href?: string;\n  kind?: \"pr\" | \"doc\" | \"thread\";\n}\n\nexport interface HandoffCardProps {\n  from: HandoffPerson;\n  to: HandoffPerson;\n  title: string;\n  summary: string;\n  questions?: string[];\n  links?: HandoffLink[];\n  sentAt?: string;\n  acceptedAt?: string;\n  defaultAccepted?: boolean;\n  onAccept?: () => void;\n  onUndo?: () => void;\n  onAsk?: () => void;\n  className?: string;\n}\n\nconst linkIcon: Record<NonNullable<HandoffLink[\"kind\"]>, LucideIcon> = {\n  pr: GitPullRequestIcon,\n  doc: FileTextIcon,\n  thread: MessageSquareIcon,\n};\n\nfunction Avatar({ person, className }: { person: HandoffPerson; className?: string }) {\n  return (\n    <span\n      aria-hidden\n      className={cn(\n        \"flex shrink-0 select-none items-center justify-center rounded-full font-medium\",\n        person.avatarClassName ?? \"bg-muted text-foreground\",\n        className,\n      )}\n    >\n      {person.initials}\n    </span>\n  );\n}\n\nfunction first(p: HandoffPerson) {\n  return p.name.split(\" \")[0];\n}\n\nexport function HandoffCard({\n  from,\n  to,\n  title,\n  summary,\n  questions = [],\n  links = [],\n  sentAt = \"Today, 4:10 PM\",\n  acceptedAt,\n  defaultAccepted = false,\n  onAccept,\n  onUndo,\n  onAsk,\n  className,\n}: HandoffCardProps) {\n  const reduce = useSafeReducedMotion();\n  const [accepted, setAccepted] = useState(defaultAccepted);\n  const [stamp, setStamp] = useState(acceptedAt ?? \"\");\n\n  const accept = () => {\n    setStamp(acceptedAt ?? new Date().toLocaleTimeString([], { hour: \"numeric\", minute: \"2-digit\" }));\n    setAccepted(true);\n    onAccept?.();\n  };\n  const undo = () => {\n    setAccepted(false);\n    onUndo?.();\n  };\n\n  const face = \"col-start-1 row-start-1 flex flex-col rounded-2xl border bg-card text-card-foreground shadow-xs/5 [backface-visibility:hidden]\";\n\n  return (\n    <div className={cn(\"w-full max-w-[400px] [perspective:1400px]\", className)}>\n      <motion.div\n        animate={reduce ? undefined : { rotateY: accepted ? 180 : 0 }}\n        className=\"grid [transform-style:preserve-3d]\"\n        initial={false}\n        transition={{ type: \"spring\", stiffness: 220, damping: 28 }}\n      >\n        <motion.section\n          animate={reduce ? { opacity: accepted ? 0 : 1 } : undefined}\n          aria-hidden={accepted}\n          aria-label={`Handoff from ${from.name}: ${title}`}\n          className={cn(face, accepted && \"pointer-events-none\")}\n          inert={accepted}\n          transition={{ duration: 0.2 }}\n        >\n          <header className=\"flex items-center gap-2 px-4 pt-4\">\n            <div className=\"flex items-center\">\n              <Avatar className=\"size-7 text-[10px] ring-2 ring-card\" person={from} />\n              <span className=\"z-10 -mx-1 flex size-4 items-center justify-center rounded-full bg-card text-muted-foreground ring-1 ring-border\">\n                <ArrowRightIcon className=\"size-2.5\" />\n              </span>\n              <Avatar className=\"size-7 text-[10px] ring-2 ring-card\" person={to} />\n            </div>\n            <p className=\"min-w-0 flex-1 truncate text-muted-foreground text-xs\">\n              <span className=\"font-medium text-foreground\">{first(from)}</span> handed this to{\" \"}\n              <span className=\"font-medium text-foreground\">{first(to)}</span>\n            </p>\n            <span className=\"shrink-0 text-muted-foreground text-xs\">{sentAt}</span>\n          </header>\n\n          <div className=\"px-4 pt-3\">\n            <h3 className=\"text-balance font-medium text-base leading-snug tracking-tight\">{title}</h3>\n            <p className=\"mt-1 text-pretty text-muted-foreground text-sm leading-relaxed\">{summary}</p>\n          </div>\n\n          {questions.length > 0 && (\n            <div className=\"px-4 pt-4\">\n              <h4 className=\"mb-1.5 font-medium text-muted-foreground text-xs\">\n                Open questions <span className=\"tabular-nums\">· {questions.length}</span>\n              </h4>\n              <ul className=\"space-y-1.5\">\n                {questions.map((q) => (\n                  <li className=\"flex gap-2 text-sm leading-snug\" key={q}>\n                    <CircleHelpIcon className=\"mt-0.5 size-3.5 shrink-0 text-warning-foreground\" />\n                    <span className=\"text-pretty\">{q}</span>\n                  </li>\n                ))}\n              </ul>\n            </div>\n          )}\n\n          {links.length > 0 && (\n            <div className=\"px-3 pt-4\">\n              <h4 className=\"mb-1 px-1 font-medium text-muted-foreground text-xs\">Links</h4>\n              <ul>\n                {links.map((l) => {\n                  const Icon = linkIcon[l.kind ?? \"doc\"];\n                  return (\n                    <li key={l.label}>\n                      <a\n                        className=\"flex items-center gap-2.5 rounded-lg px-1 py-1.5 text-sm outline-none transition-colors hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring\"\n                        href={l.href ?? \"#\"}\n                        onClick={(e) => !l.href && e.preventDefault()}\n                      >\n                        <span className=\"flex size-7 shrink-0 items-center justify-center rounded-lg border bg-background text-muted-foreground dark:bg-input/32\">\n                          <Icon className=\"size-3.5\" />\n                        </span>\n                        <span className=\"min-w-0 flex-1 truncate\">{l.label}</span>\n                        {l.meta && <span className=\"shrink-0 font-mono text-muted-foreground text-xs\">{l.meta}</span>}\n                      </a>\n                    </li>\n                  );\n                })}\n              </ul>\n            </div>\n          )}\n\n          <footer className=\"mt-auto flex items-center justify-end gap-2 px-4 pt-4 pb-4\">\n            {onAsk && (\n              <Button onClick={onAsk} size=\"sm\" variant=\"ghost\">\n                Ask {first(from)}\n              </Button>\n            )}\n            <Button onClick={accept} size=\"sm\">\n              <CheckIcon />\n              Accept handoff\n            </Button>\n          </footer>\n        </motion.section>\n\n        <motion.section\n          animate={reduce ? { opacity: accepted ? 1 : 0 } : undefined}\n          aria-hidden={!accepted}\n          aria-label={`${title}, now with ${to.name}`}\n          className={cn(face, \"items-center px-4 pt-7 pb-4 text-center\", !accepted && \"pointer-events-none\")}\n          inert={!accepted}\n          initial={false}\n          style={reduce ? undefined : { transform: \"rotateY(180deg)\" }}\n          transition={{ duration: 0.2 }}\n        >\n          <div className=\"relative\">\n            <motion.div\n              animate={accepted ? { scale: 1, opacity: 1 } : { scale: 0.9, opacity: 0 }}\n              initial={false}\n              transition={reduce ? { duration: 0.2 } : { type: \"spring\", stiffness: 300, damping: 20, delay: accepted ? 0.28 : 0 }}\n            >\n              <Avatar className=\"size-16 text-lg ring-4 ring-card\" person={to} />\n            </motion.div>\n            <motion.span\n              animate={accepted ? { scale: 1, opacity: 1 } : { scale: 0.5, opacity: 0 }}\n              className=\"absolute -end-0.5 -bottom-0.5 flex size-6 items-center justify-center rounded-full bg-success text-white ring-[3px] ring-card\"\n              initial={false}\n              transition={reduce ? { duration: 0.2 } : { type: \"spring\", stiffness: 500, damping: 22, delay: accepted ? 0.42 : 0 }}\n            >\n              <CheckIcon className=\"size-3.5\" strokeWidth={3} />\n            </motion.span>\n          </div>\n\n          <p className=\"mt-4 font-medium text-muted-foreground text-xs\">{title}</p>\n          <h3 className=\"mt-1 text-balance font-semibold text-xl tracking-tight\">Now with {first(to)}</h3>\n          <p className=\"mt-1 text-pretty text-muted-foreground text-sm\">\n            Accepted{stamp ? ` at ${stamp}` : \"\"}. {first(to)} owns it from here.\n          </p>\n\n          {(questions.length > 0 || links.length > 0) && (\n            <div className=\"mt-5 w-full rounded-xl border text-left\">\n              <p className=\"border-b px-3 py-2 font-medium text-muted-foreground text-xs\">\n                {first(to)} picked up\n              </p>\n              <ul className=\"space-y-2 px-3 py-2.5\">\n                {questions.map((q, i) => (\n                  <motion.li\n                    animate={accepted ? { opacity: 1, x: 0 } : { opacity: 0, x: -6 }}\n                    className=\"flex gap-2 text-xs leading-snug\"\n                    initial={false}\n                    key={q}\n                    transition={{ duration: 0.25, ease: [0.23, 1, 0.32, 1], delay: accepted && !reduce ? 0.45 + i * 0.04 : 0 }}\n                  >\n                    <CircleHelpIcon className=\"size-3.5 shrink-0 text-warning-foreground\" />\n                    <span className=\"text-pretty\">{q}</span>\n                  </motion.li>\n                ))}\n              </ul>\n              {links.length > 0 && (\n                <div className=\"flex flex-wrap gap-1.5 border-t px-3 py-2.5\">\n                  {links.map((l) => {\n                    const Icon = linkIcon[l.kind ?? \"doc\"];\n                    return (\n                      <span\n                        className=\"flex h-6 max-w-full items-center gap-1 rounded-full bg-muted px-2 text-muted-foreground text-xs\"\n                        key={l.label}\n                      >\n                        <Icon className=\"size-3 shrink-0\" />\n                        <span className=\"truncate\">{l.meta && l.kind === \"pr\" ? l.meta : l.label}</span>\n                      </span>\n                    );\n                  })}\n                </div>\n              )}\n            </div>\n          )}\n\n          <div className=\"mt-auto w-full pt-4\">\n          <div className=\"flex w-full items-center gap-2 rounded-xl bg-muted/60 px-3 py-2 text-left text-xs\">\n            <BellIcon className=\"size-3.5 shrink-0 text-muted-foreground\" />\n            <span className=\"min-w-0 flex-1 text-muted-foreground\">\n              <span className=\"font-medium text-foreground\">{first(from)}</span> was notified and stays in the loop.\n            </span>\n            <Button className=\"-me-1\" onClick={undo} size=\"xs\" variant=\"ghost\">\n              <Undo2Icon />\n              Undo\n            </Button>\n          </div>\n          </div>\n        </motion.section>\n      </motion.div>\n    </div>\n  );\n}\n"
    }
  ]
}