{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"decision-inbox","type":"registry:ui","title":"Decision Inbox","description":"Open decisions with two quick answers each – answered ones confirm and fold away","dependencies":[],"files":[{"path":"registry/decision-inbox/decision-inbox.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useState } from \"react\";\n\n/*\n * Decision Inbox – open decisions with two quick actions each, in a gray tray with a white card, in a single self-contained file.\n * Answering a decision confirms it briefly, then folds it away and the count goes down.\n * The styles are injected below, so no extra files are needed.\n */\n\nexport type Decision = {\n  id: string;\n  title: string;\n  /** Short context under the title */\n  detail?: string;\n  /** Main answer, shown as the dark button */\n  primary: string;\n  /** Other answer, shown as the light button */\n  secondary: string;\n  /** Starts folded to one muted line – click to open */\n  collapsed?: boolean;\n};\n\nexport type DecisionInboxProps = {\n  decisions: Decision[];\n  /** Called with the decision and the chosen answer */\n  onDecide?: (decision: Decision, answer: \"primary\" | \"secondary\") => void;\n  className?: string;\n};\n\ntype State = { open: boolean; answered?: string; gone?: boolean };\n\nexport function DecisionInbox({ decisions, onDecide, className }: DecisionInboxProps) {\n  const [state, setState] = useState<Record<string, State>>(() =>\n    Object.fromEntries(decisions.map((d) => [d.id, { open: !d.collapsed }])),\n  );\n  const patch = (id: string, next: Partial<State>) => setState((s) => ({ ...s, [id]: { ...s[id], ...next } }));\n\n  const open = decisions.filter((d) => !state[d.id]?.answered).length;\n  // 1284 → \"1,284\"\n  const count = new Intl.NumberFormat(\"en\").format(open);\n\n  const decide = (d: Decision, answer: \"primary\" | \"secondary\") => {\n    patch(d.id, { answered: answer === \"primary\" ? d.primary : d.secondary });\n    onDecide?.(d, answer);\n    // Show the confirmation for a moment, then fold the row away and open the next folded one\n    window.setTimeout(() => {\n      setState((s) => {\n        const next = { ...s, [d.id]: { ...s[d.id], gone: true } };\n        const folded = decisions.find((x) => x.id !== d.id && !next[x.id]?.open && !next[x.id]?.answered);\n        if (folded) next[folded.id] = { ...next[folded.id], open: true };\n        return next;\n      });\n    }, 1100);\n  };\n\n  return (\n    <>\n      <style href=\"ui-decision-inbox\" precedence=\"default\">\n        {css}\n      </style>\n      <section className={[\"ui-di\", className].filter(Boolean).join(\" \")} data-clear={open === 0 || undefined}>\n        <header className=\"ui-di__head\" aria-live=\"polite\">\n          {open > 0 ? (\n            <>\n              <AlertIcon />\n              <span>\n                {count} {open === 1 ? \"needs\" : \"need\"} your input\n              </span>\n            </>\n          ) : (\n            <>\n              <DoneIcon />\n              <span>All caught up</span>\n            </>\n          )}\n        </header>\n\n        <div className=\"ui-di__card\">\n          {decisions.map((d) => {\n            const s = state[d.id] ?? { open: true };\n            return (\n              <div key={d.id} className=\"ui-di__row\" data-gone={s.gone || undefined} inert={s.gone}>\n                <div className=\"ui-di__row-inner\">\n                  <div className=\"ui-di__item\" data-open={s.open || undefined} data-answered={s.answered ? true : undefined}>\n                    {s.open ? (\n                      <p className=\"ui-di__title\" dir=\"auto\">\n                        {d.title}\n                      </p>\n                    ) : (\n                      <button type=\"button\" className=\"ui-di__title ui-di__title--folded\" dir=\"auto\" title={d.title} onClick={() => patch(d.id, { open: true })}>\n                        {d.title}\n                      </button>\n                    )}\n                    <div className=\"ui-di__body\" data-open={s.open || undefined}>\n                      <div className=\"ui-di__body-inner\">\n                        {d.detail && (\n                          <p className=\"ui-di__detail\" dir=\"auto\">\n                            {d.detail}\n                          </p>\n                        )}\n                        <div className=\"ui-di__actions\">\n                          {s.answered ? (\n                            <span className=\"ui-di__done\" role=\"status\">\n                              <DoneIcon />\n                              <span dir=\"auto\">{s.answered}</span>\n                            </span>\n                          ) : (\n                            <>\n                              <button type=\"button\" className=\"ui-di__btn ui-di__btn--primary\" dir=\"auto\" onClick={() => decide(d, \"primary\")}>\n                                {d.primary}\n                              </button>\n                              <button type=\"button\" className=\"ui-di__btn\" dir=\"auto\" onClick={() => decide(d, \"secondary\")}>\n                                {d.secondary}\n                              </button>\n                            </>\n                          )}\n                        </div>\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            );\n          })}\n          <p className=\"ui-di__empty\">Nothing waiting on you – new decisions show up here.</p>\n        </div>\n      </section>\n    </>\n  );\n}\n\n/* ---------- Icons ---------- */\n\nconst AlertIcon = () => (\n  <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\">\n    <path d=\"M6.7 2.3a1.5 1.5 0 0 1 2.6 0l5.4 9.4A1.5 1.5 0 0 1 13.4 14H2.6a1.5 1.5 0 0 1-1.3-2.3Z\" fill=\"currentColor\" />\n    <path d=\"M8 5.6v3.6\" stroke=\"#fff\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n    <circle cx=\"8\" cy=\"11.4\" r=\"0.85\" fill=\"#fff\" />\n  </svg>\n);\n\nconst DoneIcon = () => (\n  <svg viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden=\"true\">\n    <circle cx=\"8\" cy=\"8\" r=\"7\" fill=\"currentColor\" />\n    <path d=\"m5 8.2 2 2 4-4.2\" fill=\"none\" stroke=\"#fff\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n  </svg>\n);\n\n/* ---------- Styles ---------- */\n\nconst css = /* css */ `\n.ui-di {\n  --ui-di-tray: var(--aui-subtle, #f0f0f0);\n  --ui-di-card: var(--aui-surface, #ffffff);\n  --ui-di-line: var(--aui-border, #f0f0f0);\n  --ui-di-ink: var(--aui-ink, #171717);\n  --ui-di-text: var(--aui-ink, #404040);\n  --ui-di-muted: var(--aui-muted, #8f8f8f);\n  --ui-di-faint: var(--aui-placeholder, #b5b5b5);\n  --ui-di-alert: #e6730e;\n  --ui-di-ok: var(--aui-success, #1e8a4f);\n  --ui-di-focus: var(--aui-focus, rgba(0, 96, 226, 0.7));\n  --ui-di-ease: var(--aui-ease, cubic-bezier(0.22, 1, 0.36, 1));\n  box-sizing: border-box;\n  width: 100%;\n  max-width: 440px;\n  /* Like the Chat Composer: a gray tray with the heading, the white card sits on top of it */\n  background: var(--ui-di-tray);\n  /* The gray tray frames the white card on every side */\n  padding: 6px;\n  border-radius: calc(var(--aui-radius-lg, 16px) + 6px);\n  font-family: var(--aui-font, \"Inter\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n  color: var(--ui-di-ink);\n  -webkit-font-smoothing: antialiased;\n}\n.ui-di *, .ui-di *::before, .ui-di *::after { box-sizing: border-box; }\n.ui-di button { margin: 0; font: inherit; cursor: pointer; }\n.ui-di button:focus-visible { outline: 2px solid var(--ui-di-focus); outline-offset: 2px; }\n\n.ui-di__head {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  padding: 11px 18px 10px;\n  font-size: 14px;\n  font-weight: 500;\n  line-height: 20px;\n  color: var(--ui-di-alert);\n  transition: color 300ms ease;\n}\n.ui-di[data-clear] .ui-di__head { color: var(--ui-di-ok); }\n\n.ui-di__card {\n  overflow: hidden;\n  background: var(--ui-di-card);\n  border: 1px solid var(--aui-border, #e5e5e5);\n  border-radius: var(--aui-radius-lg, 16px);\n  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);\n}\n\n/* Rows fold away with a height transition once answered */\n.ui-di__row {\n  display: grid;\n  grid-template-rows: 1fr;\n  transition: grid-template-rows 460ms var(--ui-di-ease), opacity 300ms ease;\n}\n.ui-di__row[data-gone] { grid-template-rows: 0fr; opacity: 0; }\n.ui-di__row-inner { min-height: 0; overflow: hidden; }\n.ui-di__row + .ui-di__row .ui-di__item { border-top: 1px solid var(--ui-di-line); }\n.ui-di__row[data-gone] + .ui-di__row .ui-di__item { border-top-color: transparent; }\n\n.ui-di__item { padding: 16px 20px 14px; }\n.ui-di__item:not([data-open]) { padding-bottom: 16px; }\n.ui-di__title {\n  display: block;\n  width: 100%;\n  margin: 0;\n  padding: 0;\n  border: 0;\n  background: none;\n  font-size: 14px;\n  font-weight: 500;\n  line-height: 20px;\n  text-align: start;\n  color: var(--ui-di-ink);\n  overflow-wrap: anywhere;\n}\n.ui-di__title--folded {\n  /* Still one line however long the title – it opens in full on click */\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n  color: var(--ui-di-faint);\n  transition: color 160ms ease;\n}\n.ui-di__title--folded:hover { color: var(--ui-di-muted); }\n\n.ui-di__body {\n  display: grid;\n  grid-template-rows: 0fr;\n  transition: grid-template-rows 420ms var(--ui-di-ease);\n}\n.ui-di__body[data-open] { grid-template-rows: 1fr; }\n/* Room around the buttons so the clipping for the fold never cuts their ring and shadow */\n.ui-di__body-inner {\n  min-height: 0;\n  overflow: hidden;\n  margin: 0 -4px -4px;\n  padding: 0 4px 4px;\n}\n.ui-di__detail {\n  margin: 4px 0 0;\n  font-size: 13px;\n  line-height: 20px;\n  color: var(--ui-di-muted);\n  white-space: pre-line;\n  overflow-wrap: anywhere;\n}\n.ui-di__actions {\n  display: flex;\n  align-items: center;\n  /* Long or translated answers go on a second line instead of out of the card */\n  flex-wrap: wrap;\n  gap: 8px;\n  min-height: 30px;\n  margin-top: 12px;\n}\n\n.ui-di__btn {\n  max-width: 100%;\n  min-height: 30px;\n  padding: 5px 12px;\n  border: 0;\n  border-radius: calc(var(--aui-radius, 10px) - 1px);\n  background: var(--aui-surface, #fff);\n  box-shadow: 0 0 0 1px var(--aui-border, #e5e5e5), 0 1px 2px rgba(0, 0, 0, 0.04);\n  font-size: 13px !important;\n  font-weight: 500;\n  line-height: 20px;\n  color: var(--ui-di-ink);\n  overflow-wrap: anywhere;\n  transition: background-color 160ms ease, transform 160ms ease;\n}\n.ui-di__btn:hover { background: #f7f7f7; }\n.ui-di__btn:active { transform: scale(0.98); }\n.ui-di__btn--primary {\n  background: linear-gradient(#323137, #201e25);\n  box-shadow: 0 0 0 1px #0d0d0d, 0 2px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.12);\n  color: #fff;\n}\n.ui-di__btn--primary:hover { background: linear-gradient(#3b3a41, #26242b); }\n\n.ui-di__done {\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  font-size: 13px;\n  font-weight: 500;\n  line-height: 20px;\n  max-width: 100%;\n  color: var(--ui-di-ok);\n  overflow-wrap: anywhere;\n  animation: ui-di-in 360ms var(--ui-di-ease);\n}\n.ui-di__done > span { min-width: 0; }\n\n.ui-di__empty {\n  display: none;\n  margin: 0;\n  padding: 18px 20px;\n  font-size: 13px;\n  line-height: 20px;\n  color: var(--ui-di-muted);\n}\n.ui-di[data-clear] .ui-di__empty { display: block; animation: ui-di-in 420ms var(--ui-di-ease) 300ms both; }\n\n@keyframes ui-di-in {\n  from { opacity: 0; transform: translateY(3px); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .ui-di__row, .ui-di__body { transition: none; }\n  .ui-di__done, .ui-di__empty { animation: none !important; }\n}\n\n/* Dark mode: follows a .dark class or data-theme=\"dark\" on any ancestor, e.g. <html> */\n:where(.dark, [data-theme=\"dark\"]) .ui-di {\n  --ui-di-focus: var(--aui-focus, rgba(77, 141, 255, 0.8));\n  --ui-di-tray: var(--aui-subtle, #161618);\n  --ui-di-card: var(--aui-surface, #232326);\n  --ui-di-line: var(--aui-border, rgba(255, 255, 255, 0.08));\n  --ui-di-ink: var(--aui-ink, #ededed);\n  --ui-di-text: var(--aui-ink, #c4c4c4);\n  --ui-di-muted: var(--aui-muted, #8f8f8f);\n  --ui-di-faint: var(--aui-placeholder, #6b6b70);\n  --ui-di-alert: var(--aui-orange, #f28a2e);\n  --ui-di-ok: #41bd74;\n}\n:where(.dark, [data-theme=\"dark\"]) .ui-di__card {\n  border-color: var(--aui-border, rgba(255, 255, 255, 0.08));\n  box-shadow: var(--aui-shadow-card, 0 1px 2px rgba(0, 0, 0, 0.4));\n}\n:where(.dark, [data-theme=\"dark\"]) .ui-di__btn {\n  background: var(--aui-surface, #2a2a2d);\n  box-shadow: 0 0 0 1px var(--aui-border, rgba(255, 255, 255, 0.1)), 0 1px 2px rgba(0, 0, 0, 0.4);\n}\n:where(.dark, [data-theme=\"dark\"]) .ui-di__btn:hover { background: #313135; }\n/* The dark primary button inverts, like on the site */\n:where(.dark, [data-theme=\"dark\"]) .ui-di__btn--primary {\n  background: linear-gradient(#f4f4f5, #e4e4e7);\n  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.6), 0 2px 4px rgba(0, 0, 0, 0.3), inset 0 1px 0 #ffffff;\n  color: #121213;\n}\n:where(.dark, [data-theme=\"dark\"]) .ui-di__btn--primary:hover { background: linear-gradient(#ffffff, #ececef); }\n`;\n"}]}