{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tickets-1",
  "title": "Tickets 1: support ticket",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "One support ticket as an agent sees it: replies and private notes, a reply timer, a composer that switches between Reply and Internal note with canned replies, and a side panel for the customer, assignee, status and priority.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/cover-art.json",
    "https://ui.ballmac.com/r/select.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json",
    "https://ui.ballmac.com/r/textarea.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/tickets-1/tickets-1.tsx",
      "content": "// Ballmac UI: Tickets 1. https://ui.ballmac.com/blocks/tickets-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { CircleAlert, Clock, Loader2, Lock, Mail, MapPin, RotateCw, Send, Sparkles, TicketX } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { CoverArt } from \"@/components/ballmac/cover-art\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ballmac/select\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { Textarea } from \"@/components/ballmac/textarea\"\nimport { cn } from \"@/lib/utils\"\n\ntype Tickets1Status = \"open\" | \"pending\" | \"solved\"\ntype Tickets1Priority = \"low\" | \"normal\" | \"high\" | \"urgent\"\n\ntype Tickets1Message = {\n  id: string\n  from: \"customer\" | \"agent\" | \"note\"\n  author: string\n  /** \"Today, 09:42\", already written for people. */\n  time: string\n  body: string\n}\n\ntype Tickets1Ticket = {\n  id: string\n  number: string\n  subject: string\n  status: Tickets1Status\n  priority: Tickets1Priority\n  assignee: string\n  tags?: string[]\n  /** ISO date and time in UTC by which the next reply is due. */\n  replyDueAt?: string\n  requester: { name: string; email: string; plan?: string; location?: string; since?: string }\n  messages: Tickets1Message[]\n}\n\ntype Tickets1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    ticket?: Tickets1Ticket\n    agents?: string[]\n    cannedReplies?: { id: string; title: string; body: string }[]\n    /** An ISO date and time in UTC for \"now\", used for the reply timer. The block never reads the clock. */\n    now?: string\n    /** Name shown on messages you send. */\n    me?: string\n    /** Called to send a reply or a note. Return a promise; reject and the message stays with Retry. */\n    onSend?: (message: { kind: \"reply\" | \"note\"; body: string }) => void | Promise<void>\n    onStatusChange?: (status: Tickets1Status) => void\n    onPriorityChange?: (priority: Tickets1Priority) => void\n    onAssign?: (agent: string) => void\n  }\n\nconst sample: Tickets1Ticket = {\n  id: \"t1\",\n  number: \"T-48213\",\n  subject: \"I was charged twice for my October invoice\",\n  status: \"open\",\n  priority: \"high\",\n  assignee: \"Sam Okafor\",\n  tags: [\"billing\", \"refund\"],\n  replyDueAt: \"2026-10-18T11:30:00Z\",\n  requester: { name: \"Hannah Brooks\", email: \"hannah@brooks.studio\", plan: \"Team · yearly\", location: \"London\", since: \"Customer since March 2024\" },\n  messages: [\n    { id: \"m1\", from: \"customer\", author: \"Hannah Brooks\", time: \"Today, 09:12\", body: \"Hi, I just noticed two charges of $1,440 for October on my card, on the 1st and on the 3rd. I only have one subscription. Could you check and refund the extra one? Thank you.\" },\n    { id: \"m2\", from: \"note\", author: \"Sam Okafor\", time: \"Today, 09:31\", body: \"Checked Stripe: the second charge is a retry after a webhook timeout on our side. Confirmed duplicate, safe to refund in full.\" },\n    { id: \"m3\", from: \"agent\", author: \"Sam Okafor\", time: \"Today, 09:38\", body: \"Hi Hannah, thank you for flagging it, and I am sorry for the trouble. You are right: the second charge was a duplicate on our side. I have started a full refund of $1,440. It usually shows on your card in 3 to 5 working days.\" },\n    { id: \"m4\", from: \"customer\", author: \"Hannah Brooks\", time: \"Today, 10:20\", body: \"Thanks Sam, that was quick. Can you send me a receipt for the refund for our accountant?\" },\n  ],\n}\n\nconst canned = [\n  { id: \"c1\", title: \"Refund started\", body: \"I have started a full refund. It usually shows on your card in 3 to 5 working days, and you will get an email when it is sent.\" },\n  { id: \"c2\", title: \"Need more details\", body: \"Thanks for writing in. To look into this, could you share a screenshot or the exact time it happened?\" },\n  { id: \"c3\", title: \"Closing the loop\", body: \"I will mark this as solved for now. If anything is still off, just reply here and it will reopen.\" },\n]\n\nconst statusMeta: Record<Tickets1Status, { label: string; dot: string }> = {\n  open: { label: \"Open\", dot: \"bg-chart-1\" },\n  pending: { label: \"Waiting on customer\", dot: \"bg-amber-400\" },\n  solved: { label: \"Solved\", dot: \"bg-chart-2\" },\n}\nconst priorityMeta: Record<Tickets1Priority, { label: string; tone: string }> = {\n  low: { label: \"Low\", tone: \"bg-muted\" },\n  normal: { label: \"Normal\", tone: \"bg-muted\" },\n  high: { label: \"High\", tone: \"bg-amber-500/15 border-amber-500/40\" },\n  urgent: { label: \"Urgent\", tone: \"bg-destructive/10 border-destructive/40\" },\n}\n\nconst left = (ms: number) => {\n  const m = Math.round(Math.abs(ms) / 60_000)\n  return m >= 60 ? `${Math.floor(m / 60)} h ${m % 60 ? `${m % 60} min` : \"\"}`.trim() : `${m} min`\n}\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n      <div className=\"grid gap-4\">\n        <Skeleton className=\"h-24 rounded-2xl\" />\n        {[0, 1, 2].map((i) => (\n          <Skeleton key={i} className=\"h-32 rounded-2xl\" />\n        ))}\n      </div>\n      <Skeleton className=\"h-96 rounded-2xl\" />\n    </div>\n  )\n}\n\n/**\n * One support ticket as an agent sees it: the conversation with customer messages, replies and private notes (which look different so a note is\n * never sent by mistake), a reply timer that says how long is left or how late you are, a composer that switches between Reply and Internal note\n * with canned replies, and a side panel with the customer, the assignee, status and priority. A message that fails to send stays in the thread with Retry.\n */\nfunction Tickets1({ ticket = sample, agents = [\"Sam Okafor\", \"Mia Laurent\", \"Alex Kim\"], cannedReplies = canned, now = \"2026-10-18T10:42:00Z\", me = \"Sam Okafor\", onSend, onStatusChange, onPriorityChange, onAssign, state = \"ready\", onRetry, emptyState, errorState, className, ...props }: Tickets1Props) {\n  const id = React.useId()\n  const [messages, setMessages] = React.useState<(Tickets1Message & { pending?: boolean; failed?: boolean; kind?: \"reply\" | \"note\" })[]>(ticket.messages)\n  const [kind, setKind] = React.useState<\"reply\" | \"note\">(\"reply\")\n  const [draft, setDraft] = React.useState(\"\")\n  const [status, setStatus] = React.useState(ticket.status)\n  const [priority, setPriority] = React.useState(ticket.priority)\n  const [assignee, setAssignee] = React.useState(ticket.assignee)\n  const endRef = React.useRef<HTMLLIElement>(null)\n  const empty = state === \"empty\"\n\n  const due = ticket.replyDueAt ? Date.parse(ticket.replyDueAt) - Date.parse(now) : null\n  const lastFromCustomer = [...messages].reverse().find((m) => m.from !== \"note\")?.from === \"customer\"\n  const timer = due === null || status === \"solved\" || !lastFromCustomer ? null : due >= 0 ? { late: false, text: `Reply due in ${left(due)}`, warn: due < 3_600_000 } : { late: true, text: `Reply is ${left(due)} late`, warn: true }\n\n  async function deliver(m: Tickets1Message & { kind: \"reply\" | \"note\" }) {\n    try {\n      await onSend?.({ kind: m.kind, body: m.body })\n      setMessages((ms) => ms.map((x) => (x.id === m.id ? { ...x, pending: false, failed: false } : x)))\n      if (m.kind === \"reply\") {\n        setStatus(\"pending\")\n        onStatusChange?.(\"pending\")\n      }\n    } catch {\n      setMessages((ms) => ms.map((x) => (x.id === m.id ? { ...x, pending: false, failed: true } : x)))\n    }\n  }\n  function send() {\n    const body = draft.trim()\n    if (!body) return\n    const m = { id: `n${Date.now()}`, from: (kind === \"note\" ? \"note\" : \"agent\") as \"note\" | \"agent\", author: me, time: \"Just now\", body, pending: true, kind }\n    setMessages((ms) => [...ms, m])\n    setDraft(\"\")\n    requestAnimationFrame(() => endRef.current?.scrollIntoView({ block: \"nearest\" }))\n    void deliver(m)\n  }\n  function retry(m: (typeof messages)[number]) {\n    setMessages((ms) => ms.map((x) => (x.id === m.id ? { ...x, pending: true, failed: false } : x)))\n    void deliver({ ...m, kind: m.kind ?? (m.from === \"note\" ? \"note\" : \"reply\") })\n  }\n\n  const content = empty ? (\n    <div className=\"bg-card rounded-2xl border px-6 py-16 text-center\">\n      <span aria-hidden=\"true\" className=\"bg-muted mx-auto flex size-12 items-center justify-center rounded-full\">\n        <TicketX className=\"size-6\" />\n      </span>\n      <h2 className=\"mt-5 text-lg font-semibold tracking-tight\">{emptyState?.title ?? \"This ticket is not here\"}</h2>\n      <p className=\"text-muted-foreground mx-auto mt-1.5 max-w-xs text-sm text-pretty\">{emptyState?.description ?? \"It may have been merged or deleted. Go back to the list to find it.\"}</p>\n      {emptyState?.action}\n    </div>\n  ) : (\n    <div className=\"grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]\">\n      <div className=\"min-w-0\">\n        <header className=\"bg-card rounded-2xl border p-5 sm:p-6\">\n          <p className=\"text-muted-foreground text-sm tabular-nums\">Ticket {ticket.number}</p>\n          <h1 id={`${id}-title`} className=\"mt-1 text-2xl font-semibold tracking-[-0.03em] text-balance\">\n            {ticket.subject}\n          </h1>\n          <div className=\"mt-4 flex flex-wrap items-center gap-2.5\">\n            <span className=\"inline-flex items-center gap-2 rounded-full border px-3 py-1 text-sm font-medium\">\n              <span aria-hidden=\"true\" className={cn(\"size-2 rounded-full\", statusMeta[status].dot)} />\n              {statusMeta[status].label}\n            </span>\n            <span className={cn(\"rounded-full border px-3 py-1 text-sm font-medium\", priorityMeta[priority].tone)}>{priorityMeta[priority].label} priority</span>\n            {timer ? (\n              <span role=\"status\" className={cn(\"inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-sm font-medium tabular-nums\", timer.late ? \"bg-destructive/10 border-destructive/40\" : timer.warn ? \"bg-amber-500/15 border-amber-500/40\" : \"\")}>\n                {timer.late ? <CircleAlert aria-hidden=\"true\" className=\"size-4\" /> : <Clock aria-hidden=\"true\" className=\"size-4\" />}\n                {timer.text}\n              </span>\n            ) : null}\n          </div>\n        </header>\n\n        <ol aria-label=\"Conversation\" className=\"mt-5 grid gap-4\">\n          {messages.map((m) => {\n            const mine = m.from === \"agent\"\n            const note = m.from === \"note\"\n            return (\n              <li key={m.id} className={cn(\"flex gap-3\", mine && \"flex-row-reverse\")}>\n                <span aria-hidden=\"true\" className=\"relative mt-1 flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-full border text-xs font-semibold\">\n                  <CoverArt seed={m.author} variant=\"mesh\" className=\"absolute inset-0\" />\n                  <span className=\"relative\">{m.author.split(\" \").map((w) => w[0]).join(\"\").slice(0, 2)}</span>\n                </span>\n                <div className={cn(\"min-w-0 max-w-[85%] rounded-2xl border p-4\", mine && \"bg-foreground text-background border-foreground\", note && \"border-dashed bg-amber-500/10 border-amber-500/50\", !mine && !note && \"bg-card\")}>\n                  <p className={cn(\"flex flex-wrap items-center gap-x-2 text-xs\", mine ? \"text-background/70\" : \"text-muted-foreground\")}>\n                    <span className={cn(\"font-semibold\", mine ? \"text-background\" : \"text-foreground\")}>{m.author}</span>\n                    {note ? (\n                      <span className=\"text-foreground inline-flex items-center gap-1 font-medium\">\n                        <Lock aria-hidden=\"true\" className=\"size-3\" />\n                        Internal note, only your team sees this\n                      </span>\n                    ) : null}\n                    <span className=\"tabular-nums\">{m.time}</span>\n                  </p>\n                  <p className=\"mt-2 text-sm leading-relaxed text-pretty whitespace-pre-line\">{m.body}</p>\n                  {m.pending ? (\n                    <p className=\"mt-2 flex items-center gap-1.5 text-xs opacity-80\">\n                      <Loader2 aria-hidden=\"true\" className=\"size-3 animate-spin motion-reduce:animate-none\" />\n                      Sending\n                    </p>\n                  ) : null}\n                  {m.failed ? (\n                    <p role=\"alert\" className=\"mt-2 flex items-center gap-2 text-xs\">\n                      <CircleAlert aria-hidden=\"true\" className=\"size-3.5\" />\n                      Not sent.\n                      <button type=\"button\" onClick={() => retry(m)} className=\"focus-visible:ring-ring/50 inline-flex items-center gap-1 rounded-sm font-semibold underline underline-offset-2 outline-none focus-visible:ring-[3px]\">\n                        <RotateCw aria-hidden=\"true\" className=\"size-3\" />\n                        Retry\n                      </button>\n                    </p>\n                  ) : null}\n                </div>\n              </li>\n            )\n          })}\n          <li ref={endRef} aria-hidden=\"true\" />\n        </ol>\n\n        <form\n          onSubmit={(e) => {\n            e.preventDefault()\n            send()\n          }}\n          className={cn(\"bg-card mt-5 rounded-2xl border p-3 transition-colors motion-reduce:transition-none\", kind === \"note\" && \"border-amber-500/50 bg-amber-500/5\")}\n        >\n          <div className=\"flex flex-wrap items-center justify-between gap-2 px-1 pb-2\">\n            <div role=\"tablist\" aria-label=\"Message type\" className=\"bg-muted flex rounded-lg p-0.5\">\n              {([\"reply\", \"note\"] as const).map((k) => (\n                <button key={k} type=\"button\" role=\"tab\" aria-selected={kind === k} onClick={() => setKind(k)} className={cn(\"focus-visible:ring-ring/50 rounded-md px-3 py-1.5 text-sm font-medium outline-none focus-visible:ring-[3px]\", kind === k ? \"bg-background shadow-sm\" : \"text-muted-foreground\")}>\n                  {k === \"reply\" ? \"Reply to customer\" : \"Internal note\"}\n                </button>\n              ))}\n            </div>\n            <Select value=\"\" onValueChange={(v) => setDraft((d) => (d ? `${d}\\n\\n` : \"\") + (cannedReplies.find((c) => c.id === v)?.body ?? \"\"))}>\n              <SelectTrigger aria-label=\"Insert a canned reply\" className=\"w-48\">\n                <Sparkles aria-hidden=\"true\" className=\"size-4\" />\n                <SelectValue placeholder=\"Canned replies\" />\n              </SelectTrigger>\n              <SelectContent>\n                {cannedReplies.map((c) => (\n                  <SelectItem key={c.id} value={c.id}>\n                    {c.title}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          </div>\n          <Textarea aria-label={kind === \"note\" ? \"Write an internal note\" : \"Write a reply to the customer\"} value={draft} onChange={(e) => setDraft(e.target.value)} onKeyDown={(e) => { if ((e.metaKey || e.ctrlKey) && e.key === \"Enter\") { e.preventDefault(); send() } }} rows={4} placeholder={kind === \"note\" ? \"Only your team will see this…\" : `Reply to ${ticket.requester.name.split(\" \")[0]}…`} className=\"border-0 shadow-none focus-visible:ring-0\" />\n          <div className=\"flex flex-wrap items-center justify-between gap-3 px-1 pt-2\">\n            <p className=\"text-muted-foreground flex items-center gap-1.5 text-xs\">\n              {kind === \"note\" ? (\n                <>\n                  <Lock aria-hidden=\"true\" className=\"size-3\" />\n                  Not sent to the customer\n                </>\n              ) : (\n                <>\n                  <Mail aria-hidden=\"true\" className=\"size-3\" />\n                  Sent to {ticket.requester.email}\n                </>\n              )}\n            </p>\n            <Button type=\"submit\" size=\"sm\" disabled={!draft.trim()}>\n              <Send aria-hidden=\"true\" className=\"rtl:-scale-x-100\" />\n              {kind === \"note\" ? \"Add note\" : \"Send reply\"}\n              <span aria-hidden=\"true\" className=\"hidden text-xs opacity-60 sm:inline\">⌘↵</span>\n            </Button>\n          </div>\n        </form>\n      </div>\n\n      <aside aria-label=\"Ticket details\" className=\"grid gap-4 lg:sticky lg:top-6\">\n        <div className=\"bg-card rounded-2xl border p-5\">\n          <div className=\"flex items-center gap-3.5\">\n            <span aria-hidden=\"true\" className=\"relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded-full border font-semibold\">\n              <CoverArt seed={ticket.requester.name} variant=\"mesh\" className=\"absolute inset-0\" />\n              <span className=\"relative\">{ticket.requester.name.split(\" \").map((w) => w[0]).join(\"\").slice(0, 2)}</span>\n            </span>\n            <div className=\"min-w-0\">\n              <h2 className=\"font-semibold tracking-tight\">{ticket.requester.name}</h2>\n              <a href={`mailto:${ticket.requester.email}`} className=\"text-muted-foreground focus-visible:ring-ring/50 block truncate rounded-sm text-sm underline-offset-4 outline-none hover:underline focus-visible:ring-[3px]\">\n                {ticket.requester.email}\n              </a>\n            </div>\n          </div>\n          <dl className=\"mt-4 grid gap-2 text-sm\">\n            {ticket.requester.plan ? (\n              <div className=\"flex justify-between gap-3\">\n                <dt className=\"text-muted-foreground\">Plan</dt>\n                <dd className=\"font-medium\">{ticket.requester.plan}</dd>\n              </div>\n            ) : null}\n            {ticket.requester.location ? (\n              <div className=\"flex justify-between gap-3\">\n                <dt className=\"text-muted-foreground flex items-center gap-1.5\">\n                  <MapPin aria-hidden=\"true\" className=\"size-3.5\" />\n                  Location\n                </dt>\n                <dd className=\"font-medium\">{ticket.requester.location}</dd>\n              </div>\n            ) : null}\n          </dl>\n          {ticket.requester.since ? <p className=\"text-muted-foreground mt-3 text-xs\">{ticket.requester.since}</p> : null}\n        </div>\n\n        <div className=\"bg-card grid gap-4 rounded-2xl border p-5\">\n          {[\n            { key: \"status\", label: \"Status\", value: status, set: (v: string) => { setStatus(v as Tickets1Status); onStatusChange?.(v as Tickets1Status) }, options: (Object.keys(statusMeta) as Tickets1Status[]).map((k) => [k, statusMeta[k].label] as const) },\n            { key: \"priority\", label: \"Priority\", value: priority, set: (v: string) => { setPriority(v as Tickets1Priority); onPriorityChange?.(v as Tickets1Priority) }, options: (Object.keys(priorityMeta) as Tickets1Priority[]).map((k) => [k, priorityMeta[k].label] as const) },\n            { key: \"assignee\", label: \"Assigned to\", value: assignee, set: (v: string) => { setAssignee(v); onAssign?.(v) }, options: agents.map((a) => [a, a] as const) },\n          ].map((f) => (\n            <div key={f.key}>\n              <p id={`${id}-${f.key}`} className=\"text-muted-foreground mb-1.5 text-xs font-medium\">\n                {f.label}\n              </p>\n              <Select value={f.value} onValueChange={f.set}>\n                <SelectTrigger aria-labelledby={`${id}-${f.key}`} className=\"w-full\">\n                  <SelectValue />\n                </SelectTrigger>\n                <SelectContent>\n                  {f.options.map(([v, l]) => (\n                    <SelectItem key={v} value={v}>\n                      {l}\n                    </SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n            </div>\n          ))}\n          {ticket.tags?.length ? (\n            <ul aria-label=\"Tags\" className=\"flex flex-wrap gap-1.5 pt-1\">\n              {ticket.tags.map((t) => (\n                <li key={t} className=\"bg-muted rounded-full px-2.5 py-0.5 text-xs font-medium\">\n                  {t}\n                </li>\n              ))}\n            </ul>\n          ) : null}\n        </div>\n      </aside>\n    </div>\n  )\n\n  return (\n    <section data-slot=\"tickets-1\" data-state={state} aria-label={empty ? \"Ticket\" : `Ticket ${ticket.number}`} className={cn(\"mx-auto w-full max-w-6xl px-4 py-8 sm:px-6\", className)} {...frameProps(props)}>\n      <StateView state={state === \"loading\" || state === \"error\" ? state : \"ready\"} onRetry={onRetry} label=\"Loading the ticket\" loading={<Skel />} error={{ title: errorState?.title ?? \"The ticket could not be loaded\", description: errorState?.description ?? \"Nothing was changed. Check your connection and try again.\", ...errorState }}>\n        {content}\n      </StateView>\n    </section>\n  )\n}\n\nexport { Tickets1, type Tickets1Props, type Tickets1Ticket, type Tickets1Message, type Tickets1Status, type Tickets1Priority }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/tickets-1/tickets-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "tickets",
      "support",
      "helpdesk",
      "customer service",
      "conversation",
      "sla",
      "internal notes",
      "canned replies",
      "inbox"
    ],
    "version": "1.0.0",
    "updated": "2026-10-18",
    "tests": true,
    "ai": {
      "summary": "A support ticket. <Tickets1 ticket={{ id, number, subject, status: 'open' | 'pending' | 'solved', priority: 'low' | 'normal' | 'high' | 'urgent', assignee, tags, replyDueAt: ISO, requester: { name, email, plan, location, since }, messages: [{ id, from: 'customer' | 'agent' | 'note', author, time, body }] }} agents cannedReplies now='2026-10-18T10:42:00Z' me onSend={({ kind: 'reply' | 'note', body }) => Promise} onStatusChange onPriorityChange onAssign />. The reply timer shows only while the last message is from the customer; it says time left, or how late. A sent reply sets the status to Waiting on customer. Reject onSend and the message stays with Retry. Pass now yourself; the block never reads the clock.",
      "whenToUse": [
        "A helpdesk or support tool's ticket view",
        "An internal request or approval thread with private notes"
      ],
      "whenNotToUse": [
        "A live chat widget (use chat-1)",
        "A public help center (use help-1)"
      ],
      "composesWith": [
        "inbox-1",
        "app-shell-1",
        "help-1",
        "directory-1"
      ],
      "a11y": [
        {
          "keys": "Tab, Ctrl or Command + Enter",
          "action": "Reply and Internal note are tabs; the composer sends with the button or the shortcut, and the field is named for what it will do"
        },
        {
          "keys": "Notes",
          "action": "Private notes are dashed, tinted and say 'Internal note, only your team sees this' in words, so they are never mistaken for a reply"
        },
        {
          "keys": "Timer",
          "action": "'Reply due in 48 min' or 'Reply is 35 min late' is a live region with an icon, not only a color"
        }
      ],
      "customization": [
        "Messages you send appear at once with Sending, then settle; a failure shows Not sent and Retry in place",
        "Canned replies insert into the draft without replacing it",
        "Your messages sit on the reading end and mirror in right-to-left"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "tickets-1-loading",
        "trigger": "state=\"loading\"",
        "note": "A skeleton conversation and side panel, announced as loading."
      },
      {
        "id": "empty",
        "title": "Ticket not found",
        "example": "tickets-1-empty",
        "trigger": "state=\"empty\"",
        "note": "Says the ticket may have been merged or deleted; emptyState sets the copy and can add a link back."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "tickets-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was changed, with Try again (onRetry). A message that fails to send stays in the thread with Not sent and Retry."
      }
    ],
    "examples": [
      "tickets-1-demo",
      "tickets-1-late",
      "tickets-1-failed",
      "tickets-1-loading",
      "tickets-1-empty",
      "tickets-1-error"
    ],
    "url": "https://ui.ballmac.com/components/tickets-1"
  },
  "categories": [
    "blocks",
    "tickets"
  ],
  "type": "registry:block"
}