{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "departures",
  "type": "registry:component",
  "title": "Departures",
  "description": "An airport departures board for your agents: each row flips letter by letter as work boards, runs, gets delayed or lands, hand-offs show as connecting flights, and the one waiting on you glows amber with an Approve button.",
  "categories": [
    "component",
    "Mission Control"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/departures.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { ArrowRightIcon, CheckIcon, PlaneTakeoffIcon } from \"lucide-react\";\nimport { AnimatePresence, MotionConfig, motion, useReducedMotion } from \"motion/react\";\nimport { type CSSProperties, Fragment, useEffect, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport type DepartureStatus = \"boarding\" | \"running\" | \"delayed\" | \"landed\" | \"needs-you\";\n\nexport interface Departure {\n  id: string;\n  agent: string;\n  task: string;\n  gate: string;\n  eta: string;\n  status: DepartureStatus;\n  remark?: string;\n  connectingTo?: string;\n  question?: string;\n}\n\nexport interface DeparturesProps {\n  departures: Departure[];\n  title?: string;\n  time?: string;\n  onApprove?: (id: string) => void;\n  className?: string;\n}\n\nconst statusMeta: Record<DepartureStatus, { label: string; text: string }> = {\n  boarding: { label: \"Boarding\", text: \"text-info-foreground\" },\n  running: { label: \"Running\", text: \"text-success-foreground\" },\n  delayed: { label: \"Delayed\", text: \"text-destructive-foreground\" },\n  landed: { label: \"Landed\", text: \"text-muted-foreground\" },\n  \"needs-you\": { label: \"Needs you\", text: \"text-warning-foreground\" },\n};\n\nconst DRUM = \" ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:.-#/\";\nconst FLIP_MS = 62;\nconst RIPPLE_MS = 12;\nconst MAX_TURNS = 4;\n\nconst W = { eta: 5, agent: 6, task: 14, gate: 5, status: 9 } as const;\nconst START = { status: 0, eta: 1, agent: 3, task: 5, gate: 9 } as const;\n\nfunction route(from: string, to: string) {\n  const a = DRUM.indexOf(from);\n  const b = DRUM.indexOf(to);\n  if (from === to) return [];\n  if (a < 0 || b < 0) return [to];\n  const n = Math.min((b - a + DRUM.length) % DRUM.length, MAX_TURNS);\n  return Array.from({ length: n }, (_, i) => DRUM[(b - n + 1 + i + DRUM.length) % DRUM.length]);\n}\n\nfunction Half({ c, top, tone }: { c: string; top?: boolean; tone?: string }) {\n  return (\n    <span className={cn(\"absolute inset-x-0 h-1/2 overflow-hidden bg-(--flap)\", top ? \"top-0 rounded-t-[3px]\" : \"bottom-0 rounded-b-[3px]\", tone)}>\n      <span className={cn(\"absolute inset-x-0 h-[18px] text-center leading-[18px]\", top ? \"top-0\" : \"bottom-0\")}>{c === \" \" ? \"\\u00a0\" : c}</span>\n    </span>\n  );\n}\n\nfunction Flap({ char, tone, delay }: { char: string; tone: string; delay: number }) {\n  const reduce = useReducedMotion();\n  const [s, setS] = useState({ cur: char, prev: char, tone, prevTone: tone, n: 0 });\n  const cur = useRef({ char, tone });\n\n  useEffect(() => {\n    const seq = reduce ? [] : route(cur.current.char, char);\n    if (!seq.length) {\n      cur.current = { char, tone };\n      setS((p) => (p.cur === char && p.tone === tone ? p : { cur: char, prev: char, tone, prevTone: tone, n: p.n }));\n      return;\n    }\n    let i = 0;\n    let t = 0;\n    const flip = () => {\n      const prev = cur.current;\n      const next = { char: seq[i++], tone };\n      cur.current = next;\n      setS((p) => ({ cur: next.char, prev: prev.char, tone, prevTone: prev.tone, n: p.n + 1 }));\n      if (i < seq.length) t = window.setTimeout(flip, FLIP_MS);\n    };\n    t = window.setTimeout(flip, delay);\n    return () => window.clearTimeout(t);\n  }, [char, tone, delay, reduce]);\n\n  return (\n    <span aria-hidden className=\"relative h-[18px] w-3 shrink-0 [perspective:80px]\">\n      <Half c={s.cur} tone={s.tone} top />\n      <Half c={s.n ? s.prev : s.cur} tone={s.n ? s.prevTone : s.tone} />\n      {s.n > 0 && !reduce && (\n        <Fragment key={s.n}>\n          <motion.span animate={{ rotateX: -90 }} className=\"absolute inset-0\" initial={{ rotateX: 0 }} transition={{ duration: 0.055, ease: [0.55, 0, 1, 0.45] }}>\n            <Half c={s.prev} tone={s.prevTone} top />\n            <motion.span animate={{ opacity: 0.28 }} className=\"absolute inset-x-0 top-0 h-1/2 rounded-t-[3px] bg-black\" initial={{ opacity: 0 }} transition={{ duration: 0.055, ease: \"easeIn\" }} />\n          </motion.span>\n          <motion.span animate={{ rotateX: 0 }} className=\"absolute inset-0\" initial={{ rotateX: 90 }} transition={{ delay: 0.05, duration: 0.11, ease: [0.3, 1.45, 0.55, 1] }}>\n            <Half c={s.cur} tone={s.tone} />\n            <motion.span animate={{ opacity: 0 }} className=\"absolute inset-x-0 bottom-0 h-1/2 rounded-b-[3px] bg-black\" initial={{ opacity: 0.22 }} transition={{ delay: 0.05, duration: 0.11, ease: \"easeOut\" }} />\n          </motion.span>\n        </Fragment>\n      )}\n      <span className=\"pointer-events-none absolute inset-x-0 top-1/2 h-px -translate-y-1/2 bg-card/80\" />\n    </span>\n  );\n}\n\nfunction Word({ text, size, start, tone, className }: { text: string; size: number; start: number; tone: string; className?: string }) {\n  const chars = text.toUpperCase().padEnd(size).slice(0, size).split(\"\");\n  return (\n    <span className={cn(\"flex gap-px\", className)}>\n      {chars.map((c, i) => (\n        <Flap char={c} delay={(start + i) * RIPPLE_MS} key={i} tone={tone} />\n      ))}\n    </span>\n  );\n}\n\nconst ease = [0.23, 1, 0.32, 1] as const;\nconst rowGrid =\n  \"grid grid-cols-[auto_1fr_auto] gap-x-3 [grid-template-areas:'eta_agent_status'_'task_task_gate'_'rm_rm_rm'] @min-[560px]:grid-cols-[auto_auto_1fr_auto_auto] @min-[560px]:[grid-template-areas:'eta_agent_task_gate_status'_'._rm_rm_rm_rm']\";\n\nfunction Remark({ d, onApprove }: { d: Departure; onApprove?: (id: string) => void }) {\n  const key = d.status === \"needs-you\" ? `q:${d.question}` : d.connectingTo ? `c:${d.connectingTo}:${d.remark}` : `r:${d.remark}`;\n  return (\n    <div className=\"flex min-h-7 items-center gap-2 font-normal font-sans text-muted-foreground text-xs [grid-area:rm]\">\n      <AnimatePresence initial={false} mode=\"wait\">\n        <motion.div\n          animate={{ opacity: 1, y: 0 }}\n          className=\"flex min-w-0 flex-1 items-center gap-1.5\"\n          exit={{ opacity: 0, y: -2, transition: { duration: 0.09, ease: \"easeOut\" } }}\n          initial={{ opacity: 0, y: 3 }}\n          key={key}\n          transition={{ duration: 0.2, ease }}\n        >\n          {d.status === \"needs-you\" ? (\n            <span className=\"truncate font-medium text-foreground\">{d.question}</span>\n          ) : d.connectingTo ? (\n            <>\n              <ArrowRightIcon aria-hidden className=\"size-3.5 shrink-0 text-info-foreground\" />\n              <span className=\"truncate\">\n                Connecting to <span className=\"font-medium text-foreground\">{d.connectingTo}</span>\n                {d.remark && <span> · {d.remark}</span>}\n              </span>\n            </>\n          ) : (\n            <span className=\"truncate\">{d.remark}</span>\n          )}\n        </motion.div>\n      </AnimatePresence>\n      <AnimatePresence initial={false}>\n        {d.status === \"needs-you\" && onApprove && (\n          <motion.div\n            animate={{ opacity: 1, scale: 1 }}\n            className=\"shrink-0\"\n            exit={{ opacity: 0, scale: 0.95, transition: { duration: 0.12 } }}\n            initial={{ opacity: 0, scale: 0.95 }}\n            style={{ transformOrigin: \"right center\" }}\n            transition={{ duration: 0.18, ease }}\n          >\n            <Button aria-label={`Approve ${d.agent}: ${d.question ?? d.task}`} className=\"transition-[scale,box-shadow] duration-150 ease-out active:scale-[0.97]\" onClick={() => onApprove(d.id)} size=\"xs\">\n              <CheckIcon />\n              Approve\n            </Button>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport function Departures({ departures, title = \"Departures\", time, onApprove, className }: DeparturesProps) {\n  const counts = { flying: 0, waiting: 0 };\n  for (const d of departures) {\n    if (d.status === \"running\" || d.status === \"boarding\" || d.status === \"delayed\") counts.flying++;\n    if (d.status === \"needs-you\") counts.waiting++;\n  }\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <section\n        aria-label={title}\n        className={cn(\"@container w-full max-w-150 rounded-2xl border bg-card text-card-foreground shadow-xs/5\", className)}\n        style={{ \"--flap\": \"color-mix(in srgb, var(--color-card) 92%, var(--color-foreground))\" } as CSSProperties}\n      >\n        <header className=\"flex items-center justify-between gap-3 border-b px-4 py-3\">\n          <div className=\"min-w-0\">\n            <h3 className=\"flex items-center gap-2 font-medium text-sm\">\n              <PlaneTakeoffIcon aria-hidden className=\"size-4 text-muted-foreground\" />\n              <span className=\"truncate\">{title}</span>\n            </h3>\n            <p className=\"mt-0.5 text-muted-foreground text-xs tabular-nums\">\n              {counts.flying} in the air\n              {counts.waiting > 0 && (\n                <>\n                  {\" · \"}\n                  <span className=\"text-warning-foreground\">{counts.waiting} waiting on you</span>\n                </>\n              )}\n            </p>\n          </div>\n          {time && (\n            <div className=\"flex items-center gap-2\">\n              <span className=\"sr-only\">Time {time}</span>\n              <Word className=\"font-medium font-mono text-[13px]\" size={5} start={0} text={time} tone=\"text-foreground\" />\n            </div>\n          )}\n        </header>\n\n        {departures.length === 0 ? (\n          <div className=\"px-4 py-10 text-center\">\n            <div className=\"font-medium text-sm\">No departures</div>\n            <div className=\"mt-1 text-muted-foreground text-xs\">When an agent picks up work it shows up here.</div>\n          </div>\n        ) : (\n          <div className=\"px-2 pt-2 pb-2\">\n            <div aria-hidden className={cn(rowGrid, \"hidden px-2 pb-1 font-medium text-[10px] text-muted-foreground uppercase tracking-wider @min-[560px]:grid\")}>\n              <span className=\"[grid-area:eta]\">Eta</span>\n              <span className=\"[grid-area:agent]\">Agent</span>\n              <span className=\"[grid-area:task]\">Task</span>\n              <span className=\"[grid-area:gate]\">Gate</span>\n              <span className=\"[grid-area:status]\">Status</span>\n            </div>\n            <ol className=\"flex flex-col gap-0.5\">\n              {departures.map((d) => {\n                const m = statusMeta[d.status];\n                const ink = d.status === \"landed\" ? \"text-muted-foreground\" : \"text-foreground\";\n                return (\n                  <li\n                    className={cn(\n                      rowGrid,\n                      \"gap-y-1.5 rounded-xl px-2 pt-2 pb-0.5 font-medium font-mono text-[12px] transition-colors duration-300 ease-out\",\n                      d.status === \"needs-you\" ? \"bg-warning/8 dark:bg-warning/10\" : \"bg-transparent\",\n                    )}\n                    key={d.id}\n                  >\n                    <span className=\"sr-only\">\n                      {d.agent}, {d.task}, via {d.gate}, {d.status === \"landed\" ? \"landed\" : \"due\"} {d.eta}, {m.label}.\n                    </span>\n                    <Word className=\"[grid-area:eta]\" size={W.eta} tone={ink} start={START.eta} text={d.eta} />\n                    <Word className=\"[grid-area:agent]\" size={W.agent} tone={ink} start={START.agent} text={d.agent} />\n                    <Word className=\"[grid-area:task]\" size={W.task} tone={ink} start={START.task} text={d.task} />\n                    <Word className=\"justify-self-end [grid-area:gate] @min-[560px]:justify-self-start\" size={W.gate} tone=\"text-muted-foreground\" start={START.gate} text={d.gate} />\n                    <Word className=\"justify-self-end [grid-area:status]\" size={W.status} tone={m.text} start={START.status} text={m.label} />\n                    <Remark d={d} onApprove={onApprove} />\n                  </li>\n                );\n              })}\n            </ol>\n          </div>\n        )}\n      </section>\n    </MotionConfig>\n  );\n}\n"
    }
  ]
}