{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "needs-you-inbox",
  "title": "NeedsYouInbox",
  "description": "Everything blocked on a person, across agents, most urgent first.",
  "registryDependencies": [
    "https://creance.pages.dev/r/avatar.json",
    "https://creance.pages.dev/r/creance-base.json"
  ],
  "files": [
    {
      "path": "needs-you-inbox/NeedsYouInbox.css",
      "content": ".cr-inbox {\n  display: grid;\n  align-content: start;\n  gap: var(--cr-space-3);\n  color: var(--cr-color-text-default);\n  font-family: var(--cr-font-family-sans);\n  font-size: var(--cr-font-size-md);\n}\n\n.cr-inbox__header {\n  display: grid;\n  gap: var(--cr-space-2);\n}\n\n.cr-inbox__title {\n  display: flex;\n  align-items: center;\n  gap: var(--cr-space-2);\n  margin: 0;\n  font-family: var(--cr-font-family-display);\n  font-size: var(--cr-font-size-2xl);\n  font-weight: var(--cr-font-weight-bold);\n  letter-spacing: var(--cr-font-tracking-title);\n}\n\n.cr-inbox__total {\n  min-width: 1.6em;\n  padding: 0 var(--cr-space-2);\n  border-radius: var(--cr-radius-full);\n  background: var(--cr-color-ink-bg);\n  color: var(--cr-color-ink-fg);\n  font-family: var(--cr-font-family-sans);\n  font-size: var(--cr-font-size-sm);\n  font-weight: var(--cr-font-weight-semibold);\n  text-align: center;\n}\n\n.cr-inbox__counts {\n  display: flex;\n  flex-wrap: wrap;\n  gap: var(--cr-space-2);\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n.cr-inbox__counts li {\n  display: inline-flex;\n  align-items: center;\n  gap: var(--cr-space-1);\n  padding: 2px var(--cr-space-2);\n  border-radius: var(--cr-radius-full);\n  background: var(--cr-color-bg-subtle);\n  color: var(--cr-color-text-muted);\n  font-size: var(--cr-font-size-xs);\n}\n\n.cr-inbox__list {\n  display: grid;\n  gap: var(--cr-space-2);\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n.cr-inbox__item {\n  position: relative;\n  display: grid;\n  gap: var(--cr-space-1);\n  padding: var(--cr-space-3) var(--cr-space-4);\n  border-radius: var(--cr-radius-lg);\n  transition: transform var(--cr-motion-duration-base) var(--cr-motion-easing-spring);\n}\n\n.cr-inbox__item[data-urgency='high'] {\n  box-shadow: var(--cr-shadow-card), inset 3px 0 0 var(--cr-color-danger-text);\n}\n\n.cr-inbox__item:has(button.cr-inbox__item-title:hover) {\n  transform: translateY(-1px);\n}\n\n.cr-inbox__kind {\n  display: inline-flex;\n  align-items: center;\n  gap: var(--cr-space-1);\n  color: var(--cr-color-text-muted);\n  font-size: var(--cr-font-size-xs);\n  font-weight: var(--cr-font-weight-medium);\n}\n\n.cr-inbox__item[data-kind='failed-run'] .cr-inbox__kind { color: var(--cr-color-status-failed-fg); }\n.cr-inbox__item[data-kind='approval'] .cr-inbox__kind { color: var(--cr-color-text-default); }\n\n.cr-inbox__urgent {\n  margin-inline-start: var(--cr-space-1);\n  padding: 0 var(--cr-space-1);\n  border-radius: var(--cr-radius-sm);\n  background: var(--cr-color-risk-high-bg);\n  color: var(--cr-color-risk-high-fg);\n}\n\n.cr-inbox__item-title {\n  padding: 0;\n  border: 0;\n  background: none;\n  color: var(--cr-color-text-default);\n  font: inherit;\n  font-weight: var(--cr-font-weight-semibold);\n  text-align: start;\n}\n\nbutton.cr-inbox__item-title {\n  cursor: pointer;\n}\n\n/* The whole card is the click target; the button keeps the accessible name short. */\nbutton.cr-inbox__item-title::after {\n  content: '';\n  position: absolute;\n  inset: 0;\n  border-radius: var(--cr-radius-lg);\n}\n\nbutton.cr-inbox__item-title:focus-visible {\n  outline: none;\n}\n\nbutton.cr-inbox__item-title:focus-visible::after {\n  outline: var(--cr-focus-width) solid var(--cr-color-focus);\n  outline-offset: var(--cr-focus-offset);\n}\n\n.cr-inbox__detail {\n  color: var(--cr-color-text-muted);\n  font-size: var(--cr-font-size-sm);\n}\n\n.cr-inbox__meta {\n  display: flex;\n  align-items: center;\n  gap: var(--cr-space-1);\n  color: var(--cr-color-text-muted);\n  font-size: var(--cr-font-size-xs);\n}\n\n.cr-inbox__empty {\n  margin: 0;\n  color: var(--cr-color-text-muted);\n}\n",
      "type": "registry:file",
      "target": "@components/creance/needs-you-inbox/NeedsYouInbox.css"
    },
    {
      "path": "needs-you-inbox/NeedsYouInbox.tsx",
      "content": "'use client';\n\nimport { createElement, useId, type ReactNode } from 'react';\nimport { Avatar } from '@/components/creance/avatar/Avatar';\nimport { cx } from '@/lib/creance/cx';\nimport { AlertIcon, CrossIcon, PauseIcon, ShieldIcon } from '@/lib/creance/icons';\nimport { formatDuration } from '@/lib/creance/formatDuration';\nimport { useNow } from '@/lib/creance/useNow';\nimport './NeedsYouInbox.css';\n\nexport type InboxKind = 'approval' | 'question' | 'failed-run' | 'review';\nexport type InboxUrgency = 'high' | 'normal' | 'low';\n\nexport interface InboxItem {\n  id: string;\n  kind: InboxKind;\n  title: string;\n  agentName: string;\n  /** Epoch ms when the agent started waiting. Older items sort first within an urgency. */\n  createdAt: number;\n  urgency: InboxUrgency;\n  /** One short line of context. */\n  detail?: string | undefined;\n}\n\nexport interface NeedsYouInboxProps {\n  items: InboxItem[];\n  onOpen?: (item: InboxItem) => void;\n  /** Reference time for \"waiting 4m\". Defaults to when the inbox mounted; pass it to keep ages current. */\n  now?: number;\n  /** Heading text. The count is added after it. */\n  title?: string;\n  headingLevel?: 2 | 3 | 4;\n  emptyText?: string;\n  className?: string;\n}\n\nconst kinds: Record<InboxKind, { label: string; plural: string; icon: ReactNode }> = {\n  approval: { label: 'Approval', plural: 'approvals', icon: <ShieldIcon /> },\n  question: { label: 'Question', plural: 'questions', icon: <PauseIcon /> },\n  'failed-run': { label: 'Failed run', plural: 'failed runs', icon: <CrossIcon /> },\n  review: { label: 'Review', plural: 'reviews', icon: <AlertIcon /> },\n};\nconst urgencyRank: Record<InboxUrgency, number> = { high: 0, normal: 1, low: 2 };\n\n/** Most urgent first; within an urgency, whoever has waited longest. */\nexport function sortInbox(items: InboxItem[]): InboxItem[] {\n  return [...items].sort((a, b) => urgencyRank[a.urgency] - urgencyRank[b.urgency] || a.createdAt - b.createdAt);\n}\n\nexport function NeedsYouInbox({\n  items,\n  onOpen,\n  now,\n  title = 'Needs you',\n  headingLevel = 2,\n  emptyText = 'Nothing is waiting on you.',\n  className,\n}: NeedsYouInboxProps) {\n  const headingId = useId();\n  // Read once after mount: a clock read while rendering differs between server and browser.\n  const mountedAt = useNow(60_000, false);\n  const reference = now ?? mountedAt;\n  const sorted = sortInbox(items);\n  const countByKind = (Object.keys(kinds) as InboxKind[])\n    .map((kind) => ({ kind, count: items.filter((i) => i.kind === kind).length }))\n    .filter((c) => c.count > 0);\n\n  return (\n    <section className={cx('cr-inbox', className)} aria-labelledby={headingId}>\n      <header className=\"cr-inbox__header\">\n        {createElement(\n          `h${headingLevel}`,\n          { id: headingId, className: 'cr-inbox__title' },\n          title,\n          ' ',\n          <span className=\"cr-inbox__total\">{items.length}</span>,\n        )}\n        {countByKind.length > 0 && (\n          <ul className=\"cr-inbox__counts\" aria-label=\"By type\">\n            {countByKind.map(({ kind, count }) => (\n              <li key={kind} data-kind={kind}>\n                {kinds[kind].icon} {count} {count === 1 ? kinds[kind].label.toLowerCase() : kinds[kind].plural}\n              </li>\n            ))}\n          </ul>\n        )}\n      </header>\n\n      {sorted.length === 0 ? (\n        <p className=\"cr-inbox__empty\">{emptyText}</p>\n      ) : (\n        <ol className=\"cr-inbox__list\">\n          {sorted.map((item) => (\n            <li key={item.id} className=\"cr-inbox__item cr-surface\" data-kind={item.kind} data-urgency={item.urgency}>\n              <span className=\"cr-inbox__kind\">\n                {kinds[item.kind].icon}\n                {kinds[item.kind].label}\n                {item.urgency === 'high' && <span className=\"cr-inbox__urgent\">Urgent</span>}\n              </span>\n              {onOpen ? (\n                <button type=\"button\" className=\"cr-inbox__item-title\" onClick={() => onOpen(item)}>\n                  {item.title}\n                </button>\n              ) : (\n                <span className=\"cr-inbox__item-title\">{item.title}</span>\n              )}\n              {item.detail && <span className=\"cr-inbox__detail\">{item.detail}</span>}\n              <span className=\"cr-inbox__meta\">\n                <Avatar name={item.agentName} size=\"sm\" decorative />\n                {item.agentName}\n                {reference !== undefined && (\n                  <>\n                    <span aria-hidden=\"true\">·</span>\n                    <span>waiting {formatDuration(reference - item.createdAt)}</span>\n                  </>\n                )}\n              </span>\n            </li>\n          ))}\n        </ol>\n      )}\n    </section>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/creance/needs-you-inbox/NeedsYouInbox.tsx"
    }
  ],
  "categories": [
    "organism",
    "agent"
  ],
  "type": "registry:ui"
}