{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "backseat",
  "type": "registry:component",
  "title": "Backseat",
  "description": "Nudge an agent while it works: type a note and it joins the plan as soon as the current step wraps up, or hit Interrupt to stop it right where it is.",
  "categories": [
    "component",
    "Trust & Control"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://coss.com/ui/r/badge.json",
    "https://coss.com/ui/r/button.json"
  ],
  "files": [
    {
      "path": "registry/devl/backseat.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { CheckIcon, CornerDownLeftIcon, HandIcon, MessageSquareIcon, PlayIcon, XIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { type FormEvent, useEffect, useId, useReducer, useRef, useState } from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface BackseatStep {\n  id: string;\n  title: string;\n  detail?: string;\n}\n\nexport interface BackseatProps {\n  steps: BackseatStep[];\n  agent?: string;\n  goal?: string;\n  autoNote?: string;\n  loop?: boolean;\n  className?: string;\n}\n\ninterface Item {\n  id: string;\n  kind: \"step\" | \"note\";\n  title: string;\n  detail?: string;\n}\n\ninterface State {\n  items: Item[];\n  active: number;\n  typed: number;\n  hold: number;\n  queue: { id: string; text: string }[];\n  paused: boolean;\n  finished: boolean;\n  run: number;\n}\n\ntype Action =\n  | { type: \"tick\" }\n  | { type: \"queue\"; text: string }\n  | { type: \"unqueue\"; id: string }\n  | { type: \"interrupt\" }\n  | { type: \"resume\" }\n  | { type: \"reset\"; steps: BackseatStep[] };\n\nlet noteSeq = 0;\n\nconst press = \"transition-[scale,box-shadow,background-color] duration-150 ease-out active:scale-[0.97]\";\n\nfunction init(steps: BackseatStep[], run = 0): State {\n  return {\n    items: steps.map((s) => ({ id: s.id, kind: \"step\", title: s.title, detail: s.detail })),\n    active: 0,\n    typed: 0,\n    hold: 0,\n    queue: [],\n    paused: false,\n    finished: false,\n    run,\n  };\n}\n\nfunction applyQueue(state: State, at: number): Item[] {\n  const notes: Item[] = state.queue.map((q) => ({ id: q.id, kind: \"note\", title: q.text }));\n  const items = [...state.items];\n  items.splice(at, 0, ...notes);\n  return items;\n}\n\nfunction useReducedMotionAfterMount() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nfunction reducer(state: State, action: Action): State {\n  switch (action.type) {\n    case \"tick\": {\n      if (state.paused || state.finished) return state;\n      const current = state.items[state.active];\n      if (!current) return { ...state, finished: true };\n      const length = current.detail?.length ?? 0;\n      if (state.typed < length) return { ...state, typed: Math.min(length, state.typed + 1) };\n      if (state.hold < 22) return { ...state, hold: state.hold + 1 };\n      const items = state.queue.length ? applyQueue(state, state.active + 1) : state.items;\n      let next = state.active + 1 + state.queue.length;\n      while (next < items.length && items[next].kind === \"note\") next++;\n      return {\n        ...state,\n        items,\n        queue: [],\n        active: next,\n        typed: 0,\n        hold: 0,\n        finished: next >= items.length,\n      };\n    }\n    case \"queue\":\n      return { ...state, queue: [...state.queue, { id: `note-${state.run}-${++noteSeq}`, text: action.text }] };\n    case \"unqueue\":\n      return { ...state, queue: state.queue.filter((q) => q.id !== action.id) };\n    case \"interrupt\":\n      return state.finished ? state : { ...state, paused: true };\n    case \"resume\": {\n      if (!state.queue.length) return { ...state, paused: false };\n      const items = applyQueue(state, state.active);\n      return { ...state, items, queue: [], paused: false, active: state.active + state.queue.length, typed: 0, hold: 0 };\n    }\n    case \"reset\":\n      return init(action.steps, state.run + 1);\n  }\n}\n\nexport function Backseat({\n  steps,\n  agent = \"Agent\",\n  goal,\n  autoNote,\n  loop = true,\n  className,\n}: BackseatProps) {\n  const reduce = useReducedMotionAfterMount();\n  const [state, dispatch] = useReducer(reducer, steps, (s) => init(s));\n  const [draft, setDraft] = useState(\"\");\n  const [focused, setFocused] = useState(false);\n  const inputId = useId();\n  const touched = useRef(false);\n  const autoRun = useRef(-1);\n\n  useEffect(() => {\n    const id = setInterval(() => dispatch({ type: \"tick\" }), 40);\n    return () => clearInterval(id);\n  }, []);\n\n  useEffect(() => {\n    if (!state.finished || !loop) return;\n    const id = setTimeout(() => dispatch({ type: \"reset\", steps }), 4500);\n    return () => clearTimeout(id);\n  }, [state.finished, loop, steps]);\n\n  const timers = useRef<ReturnType<typeof setTimeout>[]>([]);\n  const focusedRef = useRef(false);\n  useEffect(() => {\n    focusedRef.current = focused;\n  }, [focused]);\n  useEffect(() => () => timers.current.forEach(clearTimeout), []);\n  useEffect(() => {\n    if (!autoNote || touched.current || state.active !== 1 || autoRun.current === state.run) return;\n    autoRun.current = state.run;\n    const gap = reduce ? 0 : 35;\n    for (let i = 1; i <= autoNote.length; i++) {\n      timers.current.push(\n        setTimeout(() => {\n          if (touched.current || focusedRef.current) return;\n          setDraft(autoNote.slice(0, i));\n        }, 400 + i * gap),\n      );\n    }\n    timers.current.push(\n      setTimeout(() => {\n        if (touched.current || focusedRef.current) return;\n        dispatch({ type: \"queue\", text: autoNote });\n        setDraft(\"\");\n      }, 800 + autoNote.length * gap),\n    );\n  }, [autoNote, state.active, state.run, reduce]);\n\n  const submit = (e: FormEvent) => {\n    e.preventDefault();\n    const text = draft.trim();\n    if (!text) return;\n    touched.current = true;\n    dispatch({ type: \"queue\", text });\n    setDraft(\"\");\n  };\n\n  const spring = reduce ? { duration: 0 } : { type: \"spring\" as const, stiffness: 340, damping: 30 };\n  const statusLabel = state.finished ? \"Done\" : state.paused ? \"Interrupted\" : \"Working\";\n\n  return (\n    <div className={cn(\"w-full max-w-[440px] rounded-2xl border bg-card text-card-foreground shadow-xs/5\", className)}>\n      <div className=\"flex items-center gap-3 px-4 pt-4\">\n        <div className=\"relative flex size-8 shrink-0 items-center justify-center rounded-full bg-primary font-medium text-primary-foreground text-xs\">\n          {agent.slice(0, 1)}\n        </div>\n        <div className=\"min-w-0 flex-1\">\n          <p className=\"flex items-center gap-2 font-medium text-sm\">\n            {agent}\n            <Badge size=\"sm\" variant={state.finished ? \"success\" : state.paused ? \"warning\" : \"info\"}>\n              {statusLabel}\n            </Badge>\n          </p>\n          {goal && <p className=\"truncate text-muted-foreground text-xs\">{goal}</p>}\n        </div>\n        {state.paused ? (\n          <Button className={press} onClick={() => dispatch({ type: \"resume\" })} size=\"xs\">\n            <PlayIcon />\n            {state.queue.length ? \"Send & resume\" : \"Resume\"}\n          </Button>\n        ) : (\n          <Button\n            className={press}\n            disabled={state.finished}\n            onClick={() => {\n              touched.current = true;\n              dispatch({ type: \"interrupt\" });\n            }}\n            size=\"xs\"\n            variant=\"destructive-outline\"\n          >\n            <HandIcon />\n            Interrupt\n          </Button>\n        )}\n      </div>\n\n      <div>\n        <ol className=\"flex flex-col px-3 pt-3 pb-2\">\n          {state.items.map((item, i) => {\n            const status = i < state.active ? \"done\" : i === state.active && !state.finished ? \"active\" : \"todo\";\n            if (item.kind === \"note\") {\n              return (\n                <motion.li\n                  animate={{ height: \"auto\", opacity: 1, y: 0 }}\n                  className=\"overflow-hidden\"\n                  initial={{ height: 0, opacity: 0, y: reduce ? 0 : 6 }}\n                  key={item.id}\n                  transition={spring}\n                >\n                  <div className=\"my-0.5 ml-8 flex items-start gap-2 rounded-lg border bg-muted/64 py-1.5 pr-2.5 pl-3 text-sm\">\n                    <MessageSquareIcon className=\"mt-0.5 size-3.5 shrink-0 text-muted-foreground\" />\n                    <span className=\"min-w-0 flex-1\">{item.title}</span>\n                    <span className=\"mt-px flex shrink-0 items-center gap-1 text-muted-foreground text-xs\">\n                      <CheckIcon className=\"size-3 text-success\" strokeWidth={3} />\n                      Applied\n                    </span>\n                  </div>\n                </motion.li>\n              );\n            }\n            const shown = status === \"active\" ? item.detail?.slice(0, state.typed) : item.detail;\n            return (\n              <li className=\"flex items-start gap-3 px-1 py-1.5\" key={item.id}>\n                <StepIcon paused={state.paused} reduce={!!reduce} status={status} />\n                <div className=\"min-w-0 flex-1\">\n                  <p\n                    className={cn(\n                      \"text-sm transition-colors duration-300\",\n                      status === \"todo\" && \"text-muted-foreground\",\n                      status === \"done\" && \"text-muted-foreground line-through decoration-muted-foreground/40\",\n                    )}\n                  >\n                    {item.title}\n                  </p>\n                  <AnimatePresence initial={false}>\n                    {status === \"active\" && item.detail && (\n                      <motion.p\n                        animate={{ opacity: 1, height: \"auto\" }}\n                        className=\"overflow-hidden text-muted-foreground text-xs\"\n                        exit={{ opacity: 0, height: 0 }}\n                        initial={{ opacity: 0, height: 0 }}\n                        transition={spring}\n                      >\n                        {shown}\n                        {state.paused ? (\n                          <span className=\"ml-1 font-medium text-warning-foreground\">paused</span>\n                        ) : (\n                          <span className=\"ml-0.5 inline-block h-3 w-px translate-y-0.5 animate-pulse bg-foreground\" />\n                        )}\n                      </motion.p>\n                    )}\n                  </AnimatePresence>\n                </div>\n              </li>\n            );\n          })}\n        </ol>\n\n        <div className=\"border-t p-3\">\n          <AnimatePresence initial={false}>\n            {state.queue.length > 0 && (\n              <motion.div\n                animate={{ opacity: 1, height: \"auto\" }}\n                className=\"overflow-hidden\"\n                exit={{ opacity: 0, height: 0 }}\n                initial={{ opacity: 0, height: 0 }}\n                transition={spring}\n              >\n                <p className=\"pb-1.5 text-[11px] text-muted-foreground\">\n                  {state.paused ? \"Sends when you resume\" : \"Joins the plan when the current step ends\"}\n                </p>\n                <ul className=\"flex flex-col pb-1.5\">\n                  <AnimatePresence initial={false}>\n                    {state.queue.map((q) => (\n                      <motion.li\n                        animate={{ height: \"auto\", opacity: 1 }}\n                        className=\"overflow-hidden\"\n                        exit={{ height: 0, opacity: 0 }}\n                        initial={{ height: 0, opacity: 0 }}\n                        key={q.id}\n                        transition={spring}\n                      >\n                        <div className=\"mb-1 flex items-center gap-2 rounded-lg border border-dashed bg-card py-1.5 pr-1.5 pl-3 text-sm\">\n                          <MessageSquareIcon className=\"size-3.5 shrink-0 text-muted-foreground\" />\n                          <span className=\"min-w-0 flex-1 truncate\">{q.text}</span>\n                          <button\n                            aria-label=\"Remove note\"\n                            className=\"relative flex size-5 cursor-pointer items-center justify-center rounded-md text-muted-foreground outline-none transition-colors duration-150 after:absolute after:-inset-1.5 hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n                            onClick={() => {\n                              touched.current = true;\n                              dispatch({ type: \"unqueue\", id: q.id });\n                            }}\n                            type=\"button\"\n                          >\n                            <XIcon className=\"size-3\" />\n                          </button>\n                        </div>\n                      </motion.li>\n                    ))}\n                  </AnimatePresence>\n                </ul>\n              </motion.div>\n            )}\n          </AnimatePresence>\n          <form className=\"flex items-center gap-2\" onSubmit={submit}>\n            <label className=\"sr-only\" htmlFor={inputId}>\n              Note for {agent}\n            </label>\n            <input\n              className=\"h-8 min-w-0 flex-1 rounded-lg border border-input bg-background px-2.5 text-sm outline-none ring-ring/24 placeholder:text-muted-foreground/72 focus-visible:border-ring focus-visible:ring-[3px] dark:bg-input/32\"\n              disabled={state.finished}\n              id={inputId}\n              onBlur={() => setFocused(false)}\n              onChange={(e) => {\n                touched.current = true;\n                setDraft(e.target.value);\n              }}\n              onFocus={() => setFocused(true)}\n              placeholder={state.finished ? \"Plan finished\" : `Steer ${agent}…`}\n              value={draft}\n            />\n            <Button className={press} disabled={!draft.trim()} size=\"sm\" type=\"submit\" variant=\"outline\">\n              Queue\n              <CornerDownLeftIcon />\n            </Button>\n          </form>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction StepIcon({ status, paused, reduce }: { status: \"done\" | \"active\" | \"todo\"; paused: boolean; reduce: boolean }) {\n  return (\n    <span className=\"relative mt-0.5 flex size-4 shrink-0 items-center justify-center\">\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        <motion.span\n          animate={{ scale: 1, opacity: 1 }}\n          className=\"flex\"\n          exit={{ scale: reduce ? 1 : 0.8, opacity: 0 }}\n          initial={{ scale: reduce ? 1 : 0.8, opacity: 0 }}\n          key={status === \"active\" && paused ? \"paused\" : status}\n          transition={{ duration: reduce ? 0.1 : 0.18, ease: [0.23, 1, 0.32, 1] }}\n        >\n          {status === \"done\" && (\n            <span className=\"flex size-4 items-center justify-center rounded-full bg-success text-white\">\n              <CheckIcon className=\"size-2.5\" strokeWidth={3.5} />\n            </span>\n          )}\n          {status === \"active\" && paused && <span className=\"size-3 rounded-sm bg-warning\" />}\n          {status === \"active\" && !paused && (\n            <svg aria-hidden className={cn(\"size-4 text-info\", !reduce && \"animate-spin\")} viewBox=\"0 0 16 16\">\n              <circle cx=\"8\" cy=\"8\" fill=\"none\" opacity=\"0.2\" r=\"6\" stroke=\"currentColor\" strokeWidth=\"2\" />\n              <path d=\"M8 2a6 6 0 0 1 6 6\" fill=\"none\" stroke=\"currentColor\" strokeLinecap=\"round\" strokeWidth=\"2\" />\n            </svg>\n          )}\n          {status === \"todo\" && <span className=\"size-3 rounded-full border-[1.5px] border-muted-foreground/40\" />}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n"
    }
  ]
}