{
  "id": "team-inbox",
  "name": "Team inbox workspace",
  "description": "A complete team messaging page with a workspace sidebar, searchable conversations, unread filters, responsive message threads, and a host-controlled composer, adapted from Shadcn Admin.",
  "category": "Application",
  "blockIds": [],
  "style": "clean-saas",
  "featured": true,
  "searchCategories": [
    "chat"
  ],
  "tags": [
    "inbox",
    "team chat",
    "messaging",
    "customer support",
    "shared inbox",
    "团队消息",
    "客服",
    "聊天工作台",
    "消息中心",
    "团队协作"
  ],
  "source": {
    "name": "Shadcn Admin",
    "url": "https://github.com/satnaing/shadcn-admin",
    "author": "Sat Naing",
    "license": "MIT",
    "revision": "e16c87f213a5ba5e45964e9b67c792105ec74d26",
    "licenseFile": "licenses/third-party/shadcn-admin-MIT.txt",
    "files": [
      "src/features/chats/index.tsx"
    ],
    "modifications": [
      "Adapted the complete responsive chats page and its list/thread/composer hierarchy to portable React and existing Tailwind theme tokens.",
      "Removed application routing, auth/configuration contexts, external profile images, date parsing, and disconnected call/attachment actions.",
      "Added typed live conversations, host message history, controlled selection/search, pending/rejected send and archive callbacks, and per-conversation drafts.",
      "Kept fictional contacts and local-only messages behind explicit demo mode; no backend, automatic replies, or delivery status is simulated."
    ]
  },
  "registryId": "recipe-team-inbox",
  "componentName": "TeamInboxRecipe",
  "componentSource": "content/recipe-components/team-inbox.tsx",
  "installUsage": "import { useState } from \"react\";\nimport TeamInboxRecipe from \"@/components/uiblocks/recipe-team-inbox/recipe-team-inbox\";\n\nexport default function InboxPage() {\n  const [selected, setSelected] = useState<string | null>(null);\n  return (\n    <TeamInboxRecipe\n      mode=\"controlled\"\n      brand=\"Your workspace\"\n      conversations={[]}\n      messages={[]}\n      selectedConversationId={selected}\n      onConversationChange={setSelected}\n    />\n  );\n}",
  "demoUsage": "import TeamInboxRecipe from \"@/components/uiblocks/recipe-team-inbox/recipe-team-inbox\";\n\nexport default function InboxDemo() {\n  return <TeamInboxRecipe mode=\"demo\" brand=\"Forma Studio\" />;\n}",
  "propsContract": [
    {
      "name": "mode, conversations, messages",
      "description": "Default controlled mode contains no people or message history. Supply conversations={id,name,subtitle?,preview,updatedAt,unread?,online?,label?} and messages={id,conversationId,body,time,direction:'incoming'|'outgoing',author?}. Times/presence remain exactly host-reported. Explicit demo mode seeds fictional records only if both arrays are omitted; explicit empty arrays remain empty."
    },
    {
      "name": "selectedConversationId, onConversationChange, searchQuery, onSearchChange",
      "description": "Supply selection and search together with their callbacks, or allow local view state over the supplied records. Read-only controlled values do not change internally. Search and unread filtering only affect navigation, never message delivery. Mobile thread back restores keyboard focus to its conversation button."
    },
    {
      "name": "onSendMessage, onArchive",
      "description": "Send receives {conversationId,body}; archive receives the conversation ID. Both await the host handler, prevent duplicate requests and show actual rejection text. Controlled send never appends messages or removes conversations; update authoritative props when your service accepts them. Drafts survive rejection and are stored separately per conversation. Actions without a handler are disabled. Fictional local demo records never invoke host business callbacks."
    },
    {
      "name": "loading, error, onRetry",
      "description": "Supply actual loading/errors from your service. Retry delegates to the host and exposes rejection without inventing a successful reload. This template includes no messaging server, websocket client, authentication or automatic response."
    },
    {
      "name": "brand, title, description, navigation, headerSlot, footer, theme, className",
      "description": "Provide your real identity, copy, normal host navigation URLs and account controls. The layout uses supplied message text without HTML injection; long text wraps within bubbles. Inherit the host theme or enable its class-based dark variant with theme='dark'."
    }
  ],
  "contentGuidelines": [
    "Preserve the conversation-list/thread hierarchy, neutral surfaces, blue message accent and readable spacing. Keep preview snippets short while retaining full messages in the thread.",
    "Supply actual names, message times and presence. Do not infer that a person is online or that a message was delivered/read when your service does not report it.",
    "Connect send, archive and retry callbacks to your own messaging service; authoritative messages and conversation removal remain host-owned.",
    "Use mode='demo' only for fictional local inspection. A demo reply stays local and never receives an automatic fabricated response.",
    "Check long Chinese names/messages, empty/loading/error states, failed sends and keyboard navigation at desktop and mobile widths."
  ],
  "prompt": "Build a complete team messaging workspace using UIBlocks team-inbox, adapted from Shadcn Admin by Sat Naing. Read its get_recipe/get_preview responses, install recipe-team-inbox in one command, and mount TeamInboxRecipe in an existing host route. Keep the sidebar, searchable conversations, unread tabs, responsive thread and blue message bubbles. Use controlled mode with authoritative conversations/messages, real timestamps/presence and explicit send/archive/retry service callbacks. Do not fabricate delivery, read receipts, automatic replies or authentication. Retain MIT attribution and pinned source revision. Verify rejected callbacks, retained drafts, conversation switching, mobile Back focus, long Chinese content and host aliases.",
  "origin": "external",
  "preview": "/preview/recipes/team-inbox",
  "screenshot": "/previews/recipe-team-inbox.png",
  "screenshotMetadata": {
    "renderer": "Chromium",
    "id": "recipe-team-inbox",
    "image": "recipe-team-inbox.png",
    "sourceFiles": [
      "app/globals.css",
      "components/brand.tsx",
      "components/copy-button.tsx",
      "components/footer.tsx",
      "components/header.tsx",
      "components/recipes/recipe-preview.tsx",
      "components/registry-setup.tsx",
      "components/seo-jsonld.tsx",
      "components/theme-toggle.tsx",
      "components/ui/button.tsx",
      "components/ui/chart.tsx",
      "components/ui/dialog.tsx",
      "components/ui/dropdown-menu.tsx",
      "components/ui/input-group.tsx",
      "components/ui/input.tsx",
      "components/ui/separator.tsx",
      "components/ui/sheet.tsx",
      "components/ui/sidebar.tsx",
      "components/ui/skeleton.tsx",
      "components/ui/sonner.tsx",
      "components/ui/table.tsx",
      "components/ui/textarea.tsx",
      "components/ui/tooltip.tsx",
      "config.ts",
      "content/blocks-metadata.ts",
      "content/catalog.ts",
      "content/community-blocks.ts",
      "content/components/dashboard/dashboard-01/customers-table.tsx",
      "content/components/dashboard/dashboard-01/dashboard-charts.tsx",
      "content/components/marketing/faq-01.tsx",
      "content/components/marketing/features-01.tsx",
      "content/components/marketing/footer-01.tsx",
      "content/components/marketing/hero-01.tsx",
      "content/components/marketing/logo-cloud-01.tsx",
      "content/components/marketing/navigation-01.tsx",
      "content/components/marketing/pricing-01.tsx",
      "content/components/workspace/results-gallery-01.tsx",
      "content/components/workspace/task-queue-01.tsx",
      "content/components/workspace/upload-workflow-01.tsx",
      "content/components/workspace/usage-panel-01.tsx",
      "content/components/workspace/workspace-header-01.tsx",
      "content/declarations.ts",
      "content/product-launch-recipe.ts",
      "content/recipe-components/product-launch-art.tsx",
      "content/recipe-components/product-launch.tsx",
      "content/recipe-components/saas-dashboard.tsx",
      "content/recipe-components/studio-agency.tsx",
      "content/recipe-components/studio-agency/demo-assets.ts",
      "content/recipe-components/studio-agency/demo.ts",
      "content/recipe-components/studio-agency/styles.ts",
      "content/recipe-components/studio-agency/types.ts",
      "content/recipe-components/team-inbox.tsx",
      "content/recipe-components/tool-landing.tsx",
      "content/recipe-components/tool-workspace.tsx",
      "content/recipes.ts",
      "content/studio-agency-recipe.ts",
      "content/template-recipes.ts",
      "hooks/use-mobile.ts",
      "hooks/use-registry-origin.ts",
      "lib/google-analytics.ts",
      "lib/utils.ts",
      "package-lock.json",
      "package.json",
      "postcss.config.mjs",
      "public/icon.svg",
      "public/opengraph-image.png",
      "scripts/capture-previews.mjs",
      "scripts/preview-provenance.mjs",
      "src/lib/head.ts",
      "src/routes/__root.tsx",
      "src/routes/preview.recipes.$recipeId.tsx",
      "vite.config.ts"
    ],
    "sourceHash": "01fd37545f84698ba8028b0a5cd67386a1f04bd721fedd2d5c6238345310f82c",
    "imageHash": "8bba45eadc871730de51065e023d19d2b0176400dd39cfbfdf431b659b00c33a",
    "capturedAt": "2026-10-30T09:46:29.727Z",
    "viewport": {
      "width": 1200,
      "height": 800
    },
    "dimensions": {
      "width": 1200,
      "height": 952
    },
    "state": "demo"
  },
  "url": "/recipes/team-inbox",
  "registry": "/r/recipe-team-inbox.json",
  "install": {
    "direct": "npx shadcn@latest add https://uiblocks.org/r/recipe-team-inbox.json",
    "namespaced": "npx shadcn@latest add @uiblocks/recipe-team-inbox",
    "registryConfig": {
      "registries": {
        "@uiblocks": "https://uiblocks.org/r/{name}.json"
      }
    },
    "entry": "@components/uiblocks/recipe-team-inbox/recipe-team-inbox.tsx",
    "exportName": "default",
    "dependencies": [
      "cn@0.2.6",
      "lucide-react@0.577.0"
    ],
    "requirements": {
      "react": ">=19",
      "tailwind": ">=4"
    },
    "setup": [
      {
        "id": "project",
        "title": "Open your existing React project",
        "description": "Use React 19+, TypeScript/TSX, and a working Tailwind CSS 4 build. Keep the project's existing router, dependencies, and global stylesheet."
      },
      {
        "id": "shadcn",
        "title": "Check components.json before installing",
        "description": "Reuse an existing components.json. Its aliases.components must resolve in TypeScript and the bundler, and tailwind.css must point to the stylesheet your app loads. Do not reinitialize an already configured project.",
        "command": "npx shadcn@latest init",
        "when": "Only if components.json is missing; run inside the existing app and follow its framework setup."
      },
      {
        "id": "styles",
        "title": "Include the installed sources in Tailwind",
        "description": "Keep the host's standard shadcn theme variables, Tailwind @theme mappings, and base border styles loaded (background, foreground, border, sidebar, and chart tokens). Ensure Tailwind scans the actual installed components directory. Restricted source(none), ignored paths, and monorepos may need an @source path relative to the stylesheet. For theme='dark', use a class-based dark variant in the host CSS."
      },
      {
        "id": "import",
        "title": "Import through your configured components alias",
        "description": "The @components/ entry is a registry target placeholder, not a JavaScript import. Replace the example's @/components prefix with components.json aliases.components, verify the file printed by the CLI, and mount it in an existing host route."
      }
    ],
    "usage": "import { useState } from \"react\";\nimport TeamInboxRecipe from \"@/components/uiblocks/recipe-team-inbox/recipe-team-inbox\";\n\nexport default function InboxPage() {\n  const [selected, setSelected] = useState<string | null>(null);\n  return (\n    <TeamInboxRecipe\n      mode=\"controlled\"\n      brand=\"Your workspace\"\n      conversations={[]}\n      messages={[]}\n      selectedConversationId={selected}\n      onConversationChange={setSelected}\n    />\n  );\n}",
    "demoUsage": "import TeamInboxRecipe from \"@/components/uiblocks/recipe-team-inbox/recipe-team-inbox\";\n\nexport default function InboxDemo() {\n  return <TeamInboxRecipe mode=\"demo\" brand=\"Forma Studio\" />;\n}"
  },
  "compositionSource": {
    "language": "tsx",
    "componentName": "TeamInboxRecipe",
    "referenceOnly": false,
    "installedEntry": "@components/uiblocks/recipe-team-inbox/recipe-team-inbox.tsx",
    "content": "\"use client\";\n\n/**\n * Adapted from Shadcn Admin's complete Chats page by Sat Naing (MIT).\n * https://github.com/satnaing/shadcn-admin/blob/e16c87f213a5ba5e45964e9b67c792105ec74d26/src/features/chats/index.tsx\n * Retains the searchable conversation list, responsive thread, message bubbles,\n * and composer. Replaces application contexts/router, remote portraits, date\n * parsing and fake actions with portable host props and explicit local demo.\n */\nimport {\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport {\n  Archive,\n  ArrowLeft,\n  ArrowUpRight,\n  CheckCheck,\n  ChevronDown,\n  CircleHelp,\n  Inbox,\n  LayoutGrid,\n  MessageSquare,\n  Search,\n  Send,\n  Settings2,\n  Users,\n} from \"lucide-react\";\nimport { cn } from \"./shared/lib/utils\";\n\nexport type InboxConversation = {\n  id: string;\n  name: string;\n  subtitle?: string;\n  preview: string;\n  updatedAt: string;\n  unread?: number;\n  /** Presence is displayed only when the host explicitly reports it. */\n  online?: boolean;\n  label?: string;\n};\nexport type InboxMessage = {\n  id: string;\n  conversationId: string;\n  body: string;\n  time: string;\n  direction: \"incoming\" | \"outgoing\";\n  author?: string;\n};\nexport type TeamInboxRecipeProps = {\n  mode?: \"controlled\" | \"demo\";\n  brand?: string;\n  title?: string;\n  description?: string;\n  conversations?: readonly InboxConversation[];\n  messages?: readonly InboxMessage[];\n  selectedConversationId?: string | null;\n  onConversationChange?: (id: string | null) => void;\n  searchQuery?: string;\n  onSearchChange?: (query: string) => void;\n  onSendMessage?: (message: {\n    conversationId: string;\n    body: string;\n  }) => void | Promise<void>;\n  onArchive?: (conversationId: string) => void | Promise<void>;\n  onRetry?: () => void | Promise<void>;\n  loading?: boolean;\n  error?: string;\n  navigation?: readonly { label: string; href: string }[];\n  headerSlot?: ReactNode;\n  footer?: ReactNode;\n  theme?: \"inherit\" | \"dark\";\n  className?: string;\n};\n\nconst demoConversations: InboxConversation[] = [\n  {\n    id: \"maya\",\n    name: \"Maya Chen\",\n    subtitle: \"Product designer\",\n    preview: \"The new workspace is ready for your review.\",\n    updatedAt: \"09:42\",\n    unread: 2,\n    online: true,\n    label: \"Design\",\n  },\n  {\n    id: \"alex\",\n    name: \"Alex Morgan\",\n    subtitle: \"Frontend engineer\",\n    preview: \"You: That interaction feels much better now.\",\n    updatedAt: \"09:18\",\n    online: true,\n    label: \"Engineering\",\n  },\n  {\n    id: \"nora\",\n    name: \"Nora Williams\",\n    subtitle: \"Project lead\",\n    preview: \"Can we move the review to Thursday?\",\n    updatedAt: \"Yesterday\",\n    unread: 1,\n    label: \"Planning\",\n  },\n  {\n    id: \"jules\",\n    name: \"Jules Park\",\n    subtitle: \"Brand designer\",\n    preview: \"I added a few directions to the moodboard.\",\n    updatedAt: \"Yesterday\",\n    label: \"Brand\",\n  },\n  {\n    id: \"sam\",\n    name: \"Sam Rivera\",\n    subtitle: \"Customer experience\",\n    preview: \"Thanks for walking through the updated flow.\",\n    updatedAt: \"Tuesday\",\n    label: \"Research\",\n  },\n  {\n    id: \"riley\",\n    name: \"Riley Ellis\",\n    subtitle: \"Content strategist\",\n    preview: \"The final copy is in the shared document.\",\n    updatedAt: \"Monday\",\n    label: \"Content\",\n  },\n];\nconst demoMessages: InboxMessage[] = [\n  {\n    id: \"m1\",\n    conversationId: \"maya\",\n    body: \"Morning! I've been working on a quieter direction for the team workspace.\",\n    time: \"09:30\",\n    direction: \"incoming\",\n    author: \"Maya\",\n  },\n  {\n    id: \"m2\",\n    conversationId: \"maya\",\n    body: \"Less noise, more room for the work. I think the navigation finally has the right rhythm.\",\n    time: \"09:31\",\n    direction: \"incoming\",\n    author: \"Maya\",\n  },\n  {\n    id: \"m3\",\n    conversationId: \"maya\",\n    body: \"Love that direction. Could we bring the conversation and project context a little closer together?\",\n    time: \"09:35\",\n    direction: \"outgoing\",\n  },\n  {\n    id: \"m4\",\n    conversationId: \"maya\",\n    body: \"Exactly what I was thinking. The new workspace is ready for your review.\",\n    time: \"09:42\",\n    direction: \"incoming\",\n    author: \"Maya\",\n  },\n  {\n    id: \"a1\",\n    conversationId: \"alex\",\n    body: \"The focus states are in. Everything works with the keyboard now.\",\n    time: \"09:12\",\n    direction: \"incoming\",\n    author: \"Alex\",\n  },\n  {\n    id: \"a2\",\n    conversationId: \"alex\",\n    body: \"That interaction feels much better now.\",\n    time: \"09:18\",\n    direction: \"outgoing\",\n  },\n];\nconst emptyConversations: readonly InboxConversation[] = [];\nconst emptyMessages: readonly InboxMessage[] = [];\nconst focusClass =\n  \"outline-offset-4 focus-visible:outline-2 focus-visible:outline-blue-500\";\n\nfunction Initials({\n  name,\n  online,\n  size = \"normal\",\n}: {\n  name: string;\n  online?: boolean;\n  size?: \"normal\" | \"large\";\n}) {\n  return (\n    <span\n      className={cn(\n        \"relative inline-flex shrink-0 items-center justify-center rounded-full border border-blue-100 bg-blue-50 font-medium text-blue-700 dark:border-blue-900 dark:bg-blue-950 dark:text-blue-200\",\n        size === \"large\" ? \"size-12 text-sm\" : \"size-10 text-xs\"\n      )}\n    >\n      <span aria-hidden=\"true\">\n        {Array.from(name.trim()).slice(0, 2).join(\"\").toUpperCase()}\n      </span>\n      {online === true && (\n        <span className=\"absolute bottom-0 right-0 size-3 rounded-full border-2 border-background bg-emerald-500\">\n          <span className=\"sr-only\">Online</span>\n        </span>\n      )}\n    </span>\n  );\n}\n\nexport default function TeamInboxRecipe({\n  mode = \"controlled\",\n  brand = \"Your workspace\",\n  title = \"Inbox\",\n  description = \"A little less noise. A little more connection.\",\n  conversations,\n  messages,\n  selectedConversationId,\n  onConversationChange,\n  searchQuery,\n  onSearchChange,\n  onSendMessage,\n  onArchive,\n  onRetry,\n  loading = false,\n  error,\n  navigation = [],\n  headerSlot,\n  footer,\n  theme = \"inherit\",\n  className,\n}: TeamInboxRecipeProps) {\n  const localDemo =\n    mode === \"demo\" && conversations === undefined && messages === undefined;\n  const [localConversations, setLocalConversations] =\n    useState(demoConversations);\n  const [localMessages, setLocalMessages] = useState(demoMessages);\n  const records =\n    conversations ?? (localDemo ? localConversations : emptyConversations);\n  const history = messages ?? (localDemo ? localMessages : emptyMessages);\n  const [localSelection, setLocalSelection] = useState<string | null>(\n    localDemo ? \"maya\" : null\n  );\n  const selectedId =\n    selectedConversationId === undefined\n      ? localSelection\n      : selectedConversationId;\n  const selected = records.find((item) => item.id === selectedId);\n  const [mobileThread, setMobileThread] = useState(false);\n  const [localSearch, setLocalSearch] = useState(\"\");\n  const query = searchQuery ?? localSearch;\n  const [unreadOnly, setUnreadOnly] = useState(false);\n  const [drafts, setDrafts] = useState<Record<string, string>>({});\n  const draft = selected ? (drafts[selected.id] ?? \"\") : \"\";\n  const [pending, setPending] = useState<string | null>(null);\n  const [actionError, setActionError] = useState<{\n    id: string | null;\n    message: string;\n  } | null>(null);\n  const pendingRef = useRef(false);\n  const sequence = useRef(0);\n  const composer = useRef<HTMLTextAreaElement>(null);\n  const sendButton = useRef<HTMLButtonElement>(null);\n  const threadHeading = useRef<HTMLHeadingElement>(null);\n  const focusOpenedThread = useRef(false);\n  const opener = useRef<HTMLButtonElement | null>(null);\n  const restoreListFocus = useRef(false);\n  const listRef = useRef<HTMLDivElement>(null);\n  const previousSelection = useRef(selected?.id);\n  const currentThread = useRef(selected?.id);\n  currentThread.current = selected?.id;\n  const mounted = useRef(true);\n  useEffect(() => {\n    mounted.current = true;\n    return () => {\n      mounted.current = false;\n    };\n  }, []);\n  useLayoutEffect(() => {\n    if (!mobileThread && restoreListFocus.current) {\n      restoreListFocus.current = false;\n      const target =\n        opener.current?.isConnected && !opener.current.disabled\n          ? opener.current\n          : listRef.current?.querySelector<HTMLButtonElement>(\n              \"button:not(:disabled)\"\n            );\n      (target ?? listRef.current)?.focus({ preventScroll: true });\n    }\n  }, [mobileThread]);\n  useLayoutEffect(() => {\n    if (mobileThread && selected && focusOpenedThread.current) {\n      focusOpenedThread.current = false;\n      if (window.matchMedia(\"(max-width: 767px)\").matches)\n        threadHeading.current?.focus({ preventScroll: true });\n    }\n  }, [mobileThread, selected?.id]);\n  useLayoutEffect(() => {\n    const removed = previousSelection.current;\n    previousSelection.current = selected?.id;\n    if (\n      removed &&\n      !records.some((item) => item.id === removed) &&\n      document.activeElement === document.body\n    ) {\n      const target = listRef.current?.querySelector<HTMLButtonElement>(\n        \"button:not(:disabled)\"\n      );\n      (target ?? listRef.current)?.focus({ preventScroll: true });\n    }\n  }, [records, selected?.id]);\n  const filtered = records.filter(\n    (item) =>\n      (!unreadOnly || (item.unread ?? 0) > 0) &&\n      (!query.trim() ||\n        `${item.name} ${item.subtitle ?? \"\"} ${item.preview}`\n          .toLocaleLowerCase()\n          .includes(query.trim().toLocaleLowerCase()))\n  );\n  const thread = selected\n    ? history.filter((item) => item.conversationId === selected.id)\n    : [];\n  const canSelect =\n    selectedConversationId === undefined || Boolean(onConversationChange);\n  const canSend =\n    localDemo || (conversations !== undefined && Boolean(onSendMessage));\n  const canArchive =\n    localDemo || (conversations !== undefined && Boolean(onArchive));\n  const choose = (id: string, button: HTMLButtonElement) => {\n    opener.current = button;\n    focusOpenedThread.current = true;\n    if (selectedConversationId === undefined) setLocalSelection(id);\n    onConversationChange?.(id);\n    setMobileThread(true);\n    setActionError(null);\n  };\n  const perform = async (\n    kind: string,\n    id: string | null,\n    action: () => void | Promise<void>\n  ) => {\n    if (pendingRef.current) return false;\n    pendingRef.current = true;\n    setPending(kind);\n    setActionError(null);\n    try {\n      await action();\n      return true;\n    } catch (reason) {\n      if (mounted.current)\n        setActionError({\n          id,\n          message:\n            reason instanceof Error\n              ? reason.message\n              : \"This action could not be completed. Try again.\",\n        });\n      return false;\n    } finally {\n      pendingRef.current = false;\n      if (mounted.current) setPending(null);\n    }\n  };\n  const send = async () => {\n    if (!selected || !canSend || !draft.trim() || loading || pendingRef.current)\n      return;\n    const id = selected.id;\n    const body = draft.trim();\n    const submittedDraft = draft;\n    const submittedFromComposer =\n      document.activeElement === composer.current ||\n      document.activeElement === sendButton.current;\n    const accepted = await perform(\"send\", id, async () => {\n      if (localDemo) {\n        setLocalMessages((current) => [\n          ...current,\n          {\n            id: `local-${++sequence.current}`,\n            conversationId: id,\n            body,\n            time: \"Just now · local demo\",\n            direction: \"outgoing\",\n          },\n        ]);\n        setLocalConversations((current) =>\n          current.map((item) =>\n            item.id === id\n              ? { ...item, preview: `You: ${body}`, updatedAt: \"Just now\" }\n              : item\n          )\n        );\n      } else await onSendMessage?.({ conversationId: id, body });\n    });\n    if (accepted && mounted.current) {\n      setDrafts((current) =>\n        current[id] === submittedDraft ? { ...current, [id]: \"\" } : current\n      );\n    }\n    const active = document.activeElement;\n    if (\n      mounted.current &&\n      currentThread.current === id &&\n      document.hasFocus() &&\n      (active === composer.current ||\n        active === sendButton.current ||\n        (active === document.body && submittedFromComposer))\n    )\n      composer.current?.focus({ preventScroll: true });\n  };\n  const archive = async () => {\n    if (!selected || !canArchive || loading || pendingRef.current) return;\n    const id = selected.id;\n    await perform(\"archive\", id, async () => {\n      if (localDemo) {\n        setLocalConversations((current) =>\n          current.filter((item) => item.id !== id)\n        );\n        setLocalSelection(null);\n        setMobileThread(false);\n      } else await onArchive?.(id);\n    });\n  };\n\n  return (\n    <div\n      data-inbox-mode={mode}\n      className={cn(\n        theme === \"dark\" && \"dark\",\n        \"w-full bg-background text-foreground\",\n        className\n      )}\n    >\n      <div className=\"flex min-h-[880px] min-w-0 flex-col bg-background lg:flex-row\">\n        <aside className=\"flex items-center justify-between gap-4 border-b border-border bg-zinc-950 px-5 py-4 text-zinc-100 lg:w-60 lg:shrink-0 lg:flex-col lg:items-stretch lg:justify-start lg:border-b-0 lg:border-r lg:px-5 lg:py-7\">\n          <div className=\"flex min-w-0 items-center gap-2.5\">\n            <span className=\"grid size-8 shrink-0 place-items-center rounded-lg bg-blue-500\">\n              <LayoutGrid aria-hidden=\"true\" className=\"size-4\" />\n            </span>\n            <span className=\"min-w-0 break-words text-sm font-semibold\">\n              {brand}\n            </span>\n            <ChevronDown\n              aria-hidden=\"true\"\n              className=\"ml-auto hidden size-3 shrink-0 text-zinc-500 lg:block\"\n            />\n          </div>\n          <div className=\"hidden lg:mt-11 lg:block\">\n            <p className=\"mb-3 px-2 text-[10px] uppercase tracking-[.2em] text-zinc-500\">\n              Workspace\n            </p>\n            <div\n              aria-current=\"page\"\n              className=\"flex items-center gap-3 rounded-lg bg-white/10 px-3 py-2.5 text-xs\"\n            >\n              <Inbox aria-hidden=\"true\" className=\"size-4 text-blue-300\" />\n              Inbox\n              <span className=\"ml-auto rounded bg-white/10 px-1.5 py-0.5 text-[10px]\">\n                {records.length}\n              </span>\n            </div>\n            {navigation.length > 0 && (\n              <nav aria-label=\"Workspace navigation\" className=\"mt-2 space-y-1\">\n                {navigation.map((item, index) => (\n                  <a\n                    key={`${item.href}-${index}`}\n                    href={item.href}\n                    className={cn(\n                      \"flex min-w-0 items-center gap-3 rounded-lg px-3 py-2.5 text-xs text-zinc-400 hover:bg-white/5 hover:text-white\",\n                      focusClass\n                    )}\n                  >\n                    {index % 2 ? (\n                      <Settings2\n                        aria-hidden=\"true\"\n                        className=\"size-4 shrink-0\"\n                      />\n                    ) : (\n                      <Users aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n                    )}\n                    <span className=\"break-words\">{item.label}</span>\n                    <ArrowUpRight\n                      aria-hidden=\"true\"\n                      className=\"ml-auto size-3 shrink-0\"\n                    />\n                  </a>\n                ))}\n              </nav>\n            )}\n          </div>\n          <div className=\"hidden lg:mt-auto lg:block\">\n            <div className=\"rounded-xl border border-white/10 p-4\">\n              <MessageSquare\n                aria-hidden=\"true\"\n                className=\"size-5 text-blue-300\"\n              />\n              <p className=\"mt-3 text-xs font-medium\">\n                Keep the conversation close.\n              </p>\n              <p className=\"mt-2 text-[11px] leading-5 text-zinc-500\">\n                A shared place for the details that move work forward.\n              </p>\n            </div>\n            <div className=\"mt-5 flex items-center gap-2 text-[11px] text-zinc-500\">\n              <CircleHelp aria-hidden=\"true\" className=\"size-3.5\" />\n              {localDemo\n                ? \"Illustrative workspace\"\n                : \"Host-owned conversation data\"}\n            </div>\n          </div>\n          <span className=\"shrink-0 text-[10px] text-zinc-500 lg:hidden\">\n            {records.length} conversations\n          </span>\n        </aside>\n\n        <div className=\"flex min-w-0 flex-1 flex-col\">\n          <header className=\"flex min-w-0 flex-wrap items-center justify-between gap-3 border-b border-border px-5 py-5 sm:px-7\">\n            <div className=\"min-w-0\">\n              <p className=\"text-[10px] uppercase tracking-[.18em] text-muted-foreground\">\n                Team workspace\n              </p>\n              <h1 className=\"mt-1 break-words text-xl font-semibold tracking-tight\">\n                {title}\n              </h1>\n              <p className=\"mt-1 text-xs leading-5 text-muted-foreground [overflow-wrap:anywhere]\">\n                {description}\n              </p>\n            </div>\n            {headerSlot}\n          </header>\n          {mode === \"demo\" && (\n            <p className=\"border-b border-border bg-blue-50/60 px-5 py-2 text-[10px] leading-5 text-blue-700 dark:bg-blue-950/30 dark:text-blue-200 sm:px-7\">\n              {localDemo\n                ? \"Demo workspace · Fictional conversations. Messages you send stay in this local preview; no delivery or automatic reply is simulated.\"\n                : \"Demo view · Supplied records and service actions remain owned by the host.\"}\n            </p>\n          )}\n          {(loading || error) && (\n            <div className=\"flex flex-wrap items-center gap-3 border-b border-border px-5 py-3 text-xs sm:px-7\">\n              {loading && <p role=\"status\">Loading conversations…</p>}\n              {error && (\n                <p role=\"alert\" className=\"text-red-600 dark:text-red-400\">\n                  {error}\n                </p>\n              )}\n              {error && onRetry && (\n                <button\n                  type=\"button\"\n                  disabled={Boolean(pending)}\n                  onClick={() => {\n                    void perform(\"retry\", null, onRetry);\n                  }}\n                  className={cn(\n                    \"rounded-md border border-border px-3 py-1.5 disabled:opacity-50\",\n                    focusClass\n                  )}\n                >\n                  {pending === \"retry\" ? \"Retrying…\" : \"Retry loading\"}\n                </button>\n              )}\n            </div>\n          )}\n          {actionError &&\n            (actionError.id === null || actionError.id === selected?.id) && (\n              <p\n                role=\"alert\"\n                className=\"border-b border-red-200 bg-red-50 px-5 py-3 text-xs text-red-700 dark:border-red-900 dark:bg-red-950/30 dark:text-red-300\"\n              >\n                {actionError.message}\n              </p>\n            )}\n          <div className=\"grid min-w-0 flex-1 md:grid-cols-[minmax(230px,300px)_minmax(0,1fr)] xl:grid-cols-[320px_minmax(0,1fr)]\">\n            <section\n              aria-label=\"Conversations\"\n              className={cn(\n                \"min-w-0 border-border md:border-r\",\n                mobileThread && selected ? \"hidden md:block\" : \"block\"\n              )}\n            >\n              <div className=\"space-y-4 border-b border-border px-5 py-5\">\n                <label className=\"flex min-w-0 items-center gap-2 rounded-lg border border-border bg-muted/30 px-3 py-2.5\">\n                  <Search\n                    aria-hidden=\"true\"\n                    className=\"size-3.5 shrink-0 text-muted-foreground\"\n                  />\n                  <span className=\"sr-only\">Search conversations</span>\n                  <input\n                    type=\"search\"\n                    value={query}\n                    onChange={(event) => {\n                      if (searchQuery === undefined)\n                        setLocalSearch(event.target.value);\n                      onSearchChange?.(event.target.value);\n                    }}\n                    readOnly={searchQuery !== undefined && !onSearchChange}\n                    placeholder=\"Find a conversation…\"\n                    className=\"min-w-0 w-full bg-transparent text-xs outline-none\"\n                  />\n                </label>\n                <div className=\"flex gap-1 rounded-lg bg-muted/50 p-1\">\n                  {[\n                    { label: \"All messages\", value: false },\n                    { label: \"Unread\", value: true },\n                  ].map((item) => (\n                    <button\n                      key={item.label}\n                      type=\"button\"\n                      aria-pressed={unreadOnly === item.value}\n                      onClick={() => setUnreadOnly(item.value)}\n                      className={cn(\n                        \"flex-1 rounded-md px-2 py-2 text-[11px]\",\n                        focusClass,\n                        unreadOnly === item.value\n                          ? \"bg-background font-medium shadow-sm\"\n                          : \"text-muted-foreground\"\n                      )}\n                    >\n                      {item.label}\n                    </button>\n                  ))}\n                </div>\n              </div>\n              <div\n                ref={listRef}\n                tabIndex={-1}\n                aria-label=\"Conversation list\"\n                className=\"max-h-[640px] overflow-y-auto p-2 outline-none\"\n              >\n                {filtered.map((item) => (\n                  <button\n                    key={item.id}\n                    type=\"button\"\n                    disabled={!canSelect || loading}\n                    aria-pressed={selected?.id === item.id}\n                    aria-label={`Open conversation with ${item.name}`}\n                    onClick={(event) => choose(item.id, event.currentTarget)}\n                    className={cn(\n                      \"flex w-full min-w-0 items-start gap-3 rounded-xl border border-transparent p-3 text-left transition-colors hover:bg-muted/60 disabled:opacity-50\",\n                      focusClass,\n                      selected?.id === item.id &&\n                        \"border-blue-100 bg-blue-50/60 dark:border-blue-900 dark:bg-blue-950/30\"\n                    )}\n                  >\n                    <Initials name={item.name} online={item.online} />\n                    <span className=\"min-w-0 flex-1\">\n                      <span className=\"flex min-w-0 items-start justify-between gap-2\">\n                        <span\n                          className=\"min-w-0 break-words text-xs font-semibold leading-5\"\n                          title={item.name}\n                        >\n                          {item.name}\n                        </span>\n                        <span className=\"shrink-0 pt-0.5 text-[9px] text-muted-foreground\">\n                          {item.updatedAt}\n                        </span>\n                      </span>\n                      <span className=\"mt-1 block line-clamp-2 text-[11px] leading-5 text-muted-foreground [overflow-wrap:anywhere]\">\n                        {item.preview}\n                      </span>\n                      <span className=\"mt-2 flex min-w-0 items-center justify-between gap-2\">\n                        {item.label ? (\n                          <span\n                            className=\"truncate rounded border border-border bg-background/70 px-1.5 py-0.5 text-[9px] text-muted-foreground\"\n                            title={item.label}\n                          >\n                            {item.label}\n                          </span>\n                        ) : (\n                          <span />\n                        )}\n                        {(item.unread ?? 0) > 0 && (\n                          <span\n                            aria-label={`${item.unread} unread messages`}\n                            className=\"rounded-full bg-blue-600 px-1.5 py-0.5 text-[9px] text-white\"\n                          >\n                            {item.unread}\n                          </span>\n                        )}\n                      </span>\n                    </span>\n                  </button>\n                ))}\n                {!filtered.length && (\n                  <p className=\"px-4 py-14 text-center text-xs leading-6 text-muted-foreground\">\n                    {records.length\n                      ? \"No conversations match these filters.\"\n                      : \"No conversations supplied. Connect your team's message feed.\"}\n                  </p>\n                )}\n              </div>\n            </section>\n\n            <section\n              aria-label=\"Conversation thread\"\n              className={cn(\n                \"min-w-0 flex-col bg-muted/10\",\n                mobileThread && selected ? \"flex\" : \"hidden md:flex\"\n              )}\n            >\n              {selected ? (\n                <>\n                  <div className=\"flex min-w-0 flex-wrap items-center justify-between gap-3 border-b border-border bg-background px-4 py-5 sm:px-6\">\n                    <div className=\"flex min-w-0 flex-1 items-center gap-3\">\n                      <button\n                        type=\"button\"\n                        aria-label=\"Back to conversations\"\n                        onClick={() => {\n                          restoreListFocus.current = true;\n                          setMobileThread(false);\n                        }}\n                        className={cn(\n                          \"grid size-9 shrink-0 place-items-center rounded-lg border border-border md:hidden\",\n                          focusClass\n                        )}\n                      >\n                        <ArrowLeft aria-hidden=\"true\" className=\"size-4\" />\n                      </button>\n                      <Initials name={selected.name} online={selected.online} />\n                      <div className=\"min-w-0\">\n                        <h2\n                          ref={threadHeading}\n                          tabIndex={-1}\n                          className=\"break-words text-sm font-semibold outline-offset-4 focus-visible:outline-2 focus-visible:outline-blue-500\"\n                        >\n                          {selected.name}\n                        </h2>\n                        <p className=\"mt-1 break-words text-[11px] text-muted-foreground\">\n                          {selected.subtitle ?? \"Conversation\"}\n                          {selected.online === true ? \" · Online\" : \"\"}\n                        </p>\n                      </div>\n                    </div>\n                    <button\n                      type=\"button\"\n                      aria-label=\"Archive conversation\"\n                      disabled={!canArchive || loading || Boolean(pending)}\n                      onClick={() => {\n                        void archive();\n                      }}\n                      className={cn(\n                        \"inline-flex min-h-9 items-center gap-2 rounded-lg border border-border px-3 text-[10px] text-muted-foreground hover:bg-muted disabled:opacity-40\",\n                        focusClass\n                      )}\n                    >\n                      <Archive aria-hidden=\"true\" className=\"size-3.5\" />\n                      {pending === \"archive\" ? \"Archiving…\" : \"Archive\"}\n                    </button>\n                  </div>\n                  <div\n                    role=\"log\"\n                    aria-label={`Messages with ${selected.name}`}\n                    aria-live=\"polite\"\n                    aria-relevant=\"additions text\"\n                    className=\"flex min-h-80 max-h-[520px] flex-1 flex-col gap-4 overflow-y-auto px-4 py-7 sm:px-6\"\n                  >\n                    <p className=\"mb-2 self-center rounded-full border border-border bg-background px-3 py-1 text-[9px] text-muted-foreground\">\n                      {localDemo\n                        ? \"Fictional design conversation\"\n                        : \"Host-provided conversation history\"}\n                    </p>\n                    {thread.map((message) => (\n                      <div\n                        key={message.id}\n                        className={cn(\n                          \"max-w-[88%] min-w-0 sm:max-w-[80%]\",\n                          message.direction === \"outgoing\"\n                            ? \"self-end\"\n                            : \"self-start\"\n                        )}\n                      >\n                        {message.direction === \"incoming\" && message.author && (\n                          <p className=\"mb-1.5 ml-1 text-[9px] text-muted-foreground\">\n                            {message.author}\n                          </p>\n                        )}\n                        <p\n                          className={cn(\n                            \"whitespace-pre-wrap rounded-2xl px-4 py-3 text-xs leading-6 [overflow-wrap:anywhere]\",\n                            message.direction === \"outgoing\"\n                              ? \"rounded-br-sm bg-blue-600 text-white\"\n                              : \"rounded-bl-sm border border-border bg-background shadow-xs\"\n                          )}\n                        >\n                          {message.body}\n                        </p>\n                        <p\n                          className={cn(\n                            \"mt-1.5 px-1 text-[9px] text-muted-foreground\",\n                            message.direction === \"outgoing\" && \"text-right\"\n                          )}\n                        >\n                          {message.time}\n                        </p>\n                      </div>\n                    ))}\n                    {!thread.length && (\n                      <p className=\"m-auto max-w-xs py-12 text-center text-xs leading-6 text-muted-foreground\">\n                        No messages supplied for this conversation.\n                      </p>\n                    )}\n                  </div>\n                  <form\n                    className=\"border-t border-border bg-background p-4 sm:p-6\"\n                    onSubmit={(event) => {\n                      event.preventDefault();\n                      void send();\n                    }}\n                  >\n                    <label className=\"block\">\n                      <span className=\"sr-only\">\n                        Message to {selected.name}\n                      </span>\n                      <textarea\n                        ref={composer}\n                        rows={3}\n                        value={draft}\n                        onChange={(event) =>\n                          setDrafts((current) => ({\n                            ...current,\n                            [selected.id]: event.target.value,\n                          }))\n                        }\n                        placeholder={\n                          canSend\n                            ? \"Write a thoughtful reply…\"\n                            : \"Connect onSendMessage to enable replies\"\n                        }\n                        disabled={!canSend || loading || pending === \"send\"}\n                        className=\"min-h-24 w-full resize-y rounded-xl border border-border bg-muted/20 p-3 text-xs leading-6 outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-100 disabled:opacity-50 dark:focus:ring-blue-950\"\n                      />\n                    </label>\n                    <div className=\"mt-3 flex flex-wrap items-center justify-between gap-3\">\n                      <p className=\"min-w-0 text-[9px] leading-5 text-muted-foreground\">\n                        <CheckCheck\n                          aria-hidden=\"true\"\n                          className=\"mr-1 inline size-3\"\n                        />\n                        {localDemo\n                          ? \"Local demo only · No delivery service\"\n                          : \"Replies are sent through your application's handler\"}\n                      </p>\n                      <button\n                        ref={sendButton}\n                        type=\"submit\"\n                        disabled={\n                          !canSend ||\n                          !draft.trim() ||\n                          loading ||\n                          Boolean(pending)\n                        }\n                        className={cn(\n                          \"inline-flex min-h-9 items-center gap-2 rounded-lg bg-blue-600 px-4 text-[11px] font-medium text-white hover:bg-blue-700 disabled:opacity-40\",\n                          focusClass\n                        )}\n                      >\n                        {pending === \"send\" ? \"Sending…\" : \"Send message\"}\n                        <Send aria-hidden=\"true\" className=\"size-3.5\" />\n                      </button>\n                    </div>\n                  </form>\n                </>\n              ) : (\n                <div className=\"m-auto flex max-w-sm flex-col items-center px-6 py-24 text-center\">\n                  <span className=\"grid size-16 place-items-center rounded-2xl border border-border bg-background shadow-xs\">\n                    <MessageSquare\n                      aria-hidden=\"true\"\n                      className=\"size-7 text-blue-500\"\n                    />\n                  </span>\n                  <h2 className=\"mt-6 text-lg font-semibold\">\n                    Room for a real conversation.\n                  </h2>\n                  <p className=\"mt-3 text-xs leading-6 text-muted-foreground\">\n                    Select a conversation to see its messages. Your application\n                    supplies the people, history, and delivery service.\n                  </p>\n                </div>\n              )}\n            </section>\n          </div>\n          {footer && (\n            <footer className=\"border-t border-border px-5 py-4 text-xs text-muted-foreground\">\n              {footer}\n            </footer>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n",
    "guidance": "Install this complete recipe with its single shadcn command, then import the component using your configured components alias and meta.entry. Matching blocks, helpers, and MIT notices are bundled. Pass your own copy, navigation, data, and callbacks through the published props. Recipes with meta.demoUsage require explicit demo mode for sample data or simulation; controlled mode delegates service work to your application."
  }
}
