{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "message",
  "title": "Message",
  "description": "No balloons. Of the speech bubble only its tail is left, a leaning hairline. What they wrote is roman; what you wrote sits on the other side, in italic.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/avatar.json",
    "https://thedirectors.agency/ui/r/badge.json",
    "https://thedirectors.agency/ui/r/spinner.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/message.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { roll } from \"@/lib/0nlytype/roll\"\nimport { Avatar, type AvatarProps } from \"@/components/ui/avatar\"\nimport { Spinner } from \"@/components/ui/spinner\"\n\ntype MessageProps = React.ComponentProps<\"article\"> & {\n  /** Theirs sit at the start in roman; yours sit at the end, in italic. */\n  from?: \"them\" | \"you\"\n  /** Writes the body in from the left when it mounts. */\n  arriving?: boolean\n  /**\n   * script: a play text. The name stands in the margin, the words in the column, and only the type says who is speaking.\n   * quote: no face. A large quotation mark opens theirs at the start and closes yours at the end.\n   */\n  variant?: \"default\" | \"script\" | \"quote\"\n}\n\n/** A message: what they wrote is roman, what you wrote is italic. */\nfunction Message({ className, from = \"them\", arriving, variant = \"default\", ...props }: MessageProps) {\n  return (\n    <article\n      data-slot=\"message\"\n      data-from={from}\n      data-arriving={arriving || undefined}\n      data-variant={variant === \"default\" ? undefined : variant}\n      className={cn(\"ot-msg\", className)}\n      {...props}\n    />\n  )\n}\n\n/** The face beside the message. It sits in the message's grid, so it must be a direct child. */\nfunction MessageAvatar({ className, ...props }: AvatarProps) {\n  // The header already names the person, so the ring is decoration for assistive tech.\n  return <Avatar data-slot=\"message-avatar\" aria-hidden=\"true\" className={className} {...props} />\n}\n\ntype MessageHeaderProps = Omit<React.ComponentProps<\"header\">, \"children\"> & {\n  name: React.ReactNode\n  time: React.ReactNode\n  /** The machine-readable time, such as 2026-09-30T09:40. */\n  dateTime?: string\n}\n\n/** The name and the time. Yours flips them, time first, so the name stays beside your avatar. */\nfunction MessageHeader({ name, time, dateTime, className, ...props }: MessageHeaderProps) {\n  return (\n    <header data-slot=\"message-header\" className={cn(\"ot-msg-head\", className)} {...props}>\n      <b>{name}</b>\n      <time dateTime={dateTime}>{time}</time>\n    </header>\n  )\n}\n\nfunction MessageBody({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"message-body\" className={cn(\"ot-msg-body\", className)} {...props} />\n}\n\ntype MessageBubbleProps = React.ComponentProps<\"p\"> & {\n  /** Which side the tail leans to. Set \"end\" for what you wrote. */\n  align?: \"start\" | \"end\"\n  /** ink: reversed type in a block, the tail under it. mark: a highlighter behind the words. */\n  variant?: \"default\" | \"ink\" | \"mark\"\n}\n\n/** No balloon. Of the speech bubble only its tail is left, a leaning hairline. */\nfunction MessageBubble({ align = \"start\", variant = \"default\", className, children, ...props }: MessageBubbleProps) {\n  const shared = {\n    \"data-slot\": \"message-bubble\",\n    \"data-align\": align === \"end\" ? \"end\" : undefined,\n    \"data-variant\": variant === \"default\" ? undefined : variant,\n  }\n  // The highlighter needs an inline paragraph inside the block, so the marker wraps.\n  if (variant === \"mark\") {\n    return (\n      <div {...shared} className={cn(\"ot-bubble\", className)} {...(props as React.ComponentProps<\"div\">)}>\n        <p>{children}</p>\n      </div>\n    )\n  }\n  return (\n    <p {...shared} className={cn(\"ot-bubble\", className)} {...props}>\n      {children}\n    </p>\n  )\n}\n\ntype MessageTypingProps = Omit<React.ComponentProps<\"p\">, \"children\"> & {\n  /** True only while the other side is writing. Keep it mounted and drive this, so the words are heard when it starts. */\n  writing: boolean\n  /** What a screen reader hears while it shows, such as \"Ada is writing\". */\n  label?: string\n  /** Which side the tail leans to, as on a bubble. */\n  align?: \"start\" | \"end\"\n}\n\n/**\n * The other side is writing: the tail and three periods where their words will be, in pencil, breathing\n * as a busy button's do. Not writing, it stays mounted as an empty, silent status that takes no room.\n */\nfunction MessageTyping({ writing, label = \"Writing\", align = \"start\", className, ...props }: MessageTypingProps) {\n  return (\n    <p\n      data-slot=\"message-typing\"\n      data-writing={writing || undefined}\n      data-align={align === \"end\" ? \"end\" : undefined}\n      role=\"status\"\n      className={cn(\"ot-bubble ot-typing\", !writing && \"ot-sr\", className)}\n      {...props}\n    >\n      {writing ? (\n        <>\n          <Spinner />\n          <span className=\"ot-sr\">{label}</span>\n        </>\n      ) : null}\n    </p>\n  )\n}\n\nfunction MessageFooter({ className, ...props }: React.ComponentProps<\"footer\">) {\n  return <footer data-slot=\"message-footer\" className={cn(\"ot-msg-foot\", className)} {...props} />\n}\n\ntype MessageStatusProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /** Sent is a ring; read, it fills to a dot. */\n  read?: boolean\n  /** The word. Defaults to \"Sent\" or \"Read\". */\n  children?: React.ReactNode\n}\n\n/** The word and its dot, side by side in the footer. */\nfunction MessageStatus({ read, children, className, ...props }: MessageStatusProps) {\n  return (\n    <>\n      <span>{children ?? (read ? \"Read\" : \"Sent\")}</span>\n      <span data-slot=\"message-status\" data-read={read || undefined} aria-hidden=\"true\" className={cn(\"ot-msg-status\", className)} {...props} />\n    </>\n  )\n}\n\nfunction MessageReactions({ className, ...props }: React.ComponentProps<\"span\">) {\n  return <span data-slot=\"message-reactions\" role=\"group\" aria-label=\"Reactions\" className={cn(\"ot-reactions\", className)} {...props} />\n}\n\ntype MessageReactionProps = Omit<React.ComponentProps<\"button\">, \"onClick\"> & {\n  defaultPressed?: boolean\n  /** How many people reacted, including you if pressed. */\n  defaultCount?: number\n  /** Called after the count has rolled. */\n  onPressedChange?: (pressed: boolean, count: number) => void\n}\n\n/** A reaction: a tag you can press. Yours turns italic and the count rolls. */\n// ponytail: uncontrolled; lift pressed and count to props when a thread needs to own them.\nfunction MessageReaction({ defaultPressed = false, defaultCount = 0, onPressedChange, className, children, ...props }: MessageReactionProps) {\n  const [pressed, setPressed] = React.useState(defaultPressed)\n  const [count, setCount] = React.useState(defaultCount)\n  const counter = React.useRef<HTMLSpanElement>(null)\n\n  const toggle = () => {\n    const on = !pressed\n    const n = count + (on ? 1 : -1)\n    setPressed(on)\n    if (counter.current) roll(counter.current, () => setCount(n), \"0.5em\", on ? 1 : -1)\n    else setCount(n)\n    onPressedChange?.(on, n)\n  }\n\n  return (\n    <button type=\"button\" data-slot=\"message-reaction\" aria-pressed={pressed} className={cn(\"ot-tag\", className)} onClick={toggle} {...props}>\n      <span>{children}</span>\n      <span ref={counter} className=\"ot-reaction-count\">{count}</span>\n    </button>\n  )\n}\n\nexport {\n  Message,\n  MessageAvatar,\n  MessageHeader,\n  MessageBody,\n  MessageBubble,\n  MessageTyping,\n  MessageFooter,\n  MessageStatus,\n  MessageReactions,\n  MessageReaction,\n  type MessageProps,\n  type MessageTypingProps,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/message.css",
      "content": "@layer components {\n  /* ── ot-msg ── a message: what they wrote is roman, what you wrote is italic */\n  .ot-msg {\n    display: grid; grid-template-columns: auto minmax(0, 1fr); grid-template-areas: \"face head\" \"face body\" \". foot\";\n    column-gap: var(--ot-space-5); row-gap: var(--ot-space-1); max-width: 34rem;\n  }\n  .ot-msg > .ot-avatar { --s: 2.25rem; grid-area: face; align-self: start; }\n  .ot-msg-head { display: flex; align-items: baseline; gap: var(--ot-space-3); font-size: var(--ot-pp); line-height: var(--ot-pp-lh); color: var(--ot-pencil); }\n  .ot-msg-head > b { font-weight: 500; color: var(--ot-ink); }\n  .ot-msg-body { color: var(--ot-ink); }\n  .ot-msg-foot { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ot-space-2); margin-top: var(--ot-space-2); font-size: var(--ot-pp); color: var(--ot-pencil); }\n  /* Placed by name only inside a message: elsewhere (the docs' states) they are plain blocks. */\n  .ot-msg > .ot-msg-head { grid-area: head; }\n  .ot-msg > .ot-msg-body { grid-area: body; }\n  .ot-msg > .ot-msg-foot { grid-area: foot; }\n  .ot-msg[data-from=\"you\"] { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: \"head face\" \"body face\" \"foot .\"; margin-inline-start: auto; text-align: end; }\n  .ot-msg[data-from=\"you\"] :is(.ot-msg-head, .ot-msg-foot) { justify-content: flex-end; }\n  .ot-msg[data-from=\"you\"] .ot-msg-body { font-family: var(--ot-expression); font-style: italic; font-size: calc(var(--ot-expression-scale) * 1em); line-height: 1.4; }\n  .ot-msg[data-arriving] .ot-msg-body { animation: ot-message-write var(--ot-andante) var(--ot-exhale) backwards; }\n  @keyframes ot-message-write { from { clip-path: inset(0 100% 0 0); } }\n  /* The words write in from the inline start, so from the right on a right-to-left page. */\n  :is([dir=\"rtl\"], [dir=\"rtl\"] *) .ot-msg[data-arriving] .ot-msg-body { animation-name: ot-message-write-rtl; }\n  @keyframes ot-message-write-rtl { from { clip-path: inset(0 0 0 100%); } }\n  /* Yours: the time comes first, then the name, next to the avatar. */\n  .ot-msg[data-from=\"you\"] .ot-msg-head > time { order: -1; }\n  /* Sent is a ring; read, it fills to a dot, like the switch's full stop. */\n  .ot-msg-status { display: inline-block; width: 6px; height: 6px; border-radius: 50%; box-shadow: inset 0 0 0 var(--ot-hairline) currentColor; transition: box-shadow var(--ot-andante) var(--ot-exhale); }\n  .ot-msg-status[data-read] { box-shadow: inset 0 0 0 4px currentColor; }\n\n  /* ── ot-bubble ── the speech balloon is gone; only its tail is left */\n  .ot-bubble { position: relative; padding-inline-start: 1.1em; }\n  .ot-bubble::before { content: \"\"; position: absolute; inset-inline-start: 0.65em; top: 0.4em; width: var(--ot-hairline); height: 0.8em; background: var(--ot-ink); rotate: 38deg; transform-origin: 50% 0; }\n  .ot-bubble[data-align=\"end\"] { padding-inline: 0 1.1em; }\n  .ot-bubble[data-align=\"end\"]::before { inset-inline: auto 0.65em; rotate: -38deg; }\n  /* In a message the tail hangs in the gutter beside the avatar, so the words share the name's edge. */\n  .ot-msg-body > .ot-bubble:not([data-variant=\"ink\"]) { margin-inline-start: -1.1em; }\n  .ot-msg-body > .ot-bubble[data-align=\"end\"]:not([data-variant=\"ink\"]) { margin-inline: 0 -1.1em; }\n  :is([dir=\"rtl\"], [dir=\"rtl\"] *) .ot-bubble::before { rotate: -38deg; }\n  :is([dir=\"rtl\"], [dir=\"rtl\"] *) .ot-bubble[data-align=\"end\"]::before { rotate: 38deg; }\n  .ot-bubble[data-variant=\"ink\"] { padding: 0.55em 0.95em 0.6em; background: var(--ot-ink); color: var(--ot-paper); }\n  .ot-bubble[data-variant=\"ink\"]::before { inset-inline-start: 0.9em; top: 100%; height: 0.7em; }\n  .ot-bubble[data-variant=\"mark\"] > p { display: inline; padding: 0 0.12em; background: var(--ot-mark); color: var(--ot-on-mark); -webkit-box-decoration-break: clone; box-decoration-break: clone; }\n  /* Selected inside the highlighter, the words reverse, as they do in a mark: yellow on yellow would lose them. */\n  .ot-bubble[data-variant=\"mark\"] > p ::selection, .ot-bubble[data-variant=\"mark\"] > p::selection { background: var(--ot-ink); color: var(--ot-paper); }\n  /* typing: the other side is writing. Only the tail and three periods where the words will be, sketched in pencil\n     until the message inks them in. Shown only while they write (data-writing); otherwise a silent, empty status (.ot-sr). */\n  .ot-typing[data-writing] { color: var(--ot-pencil); animation: ot-arrive var(--ot-moderato) var(--ot-exhale) backwards; }\n  .ot-typing::before { background: currentColor; }\n  /* A message that holds a silent typing status folds away with it (as .ot-sr does), so it can stay mounted, face and all. */\n  .ot-msg:has(> .ot-msg-body > .ot-typing:not([data-writing])) { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n  .ot-reactions { display: flex; flex-wrap: wrap; gap: var(--ot-space-2); }\n  .ot-reactions > .ot-tag[aria-pressed=\"true\"] { color: var(--ot-ink); box-shadow: inset 0 0 0 var(--ot-hairline) var(--ot-ink); }\n  .ot-reactions > .ot-tag[aria-pressed=\"true\"] > :first-child { font-family: var(--ot-expression); font-style: italic; font-size: calc(var(--ot-expression-scale) * 1em); line-height: 1; }\n  .ot-reaction-count { display: inline-block; color: var(--ot-pencil); font-variant-numeric: tabular-nums; }\n\n  /* ── script ── a play text: the name in caps in the margin, the words in the column, one column for everyone.\n     Who is speaking shows only in the type: theirs roman, yours italic. The time is a stage direction, in parentheses. */\n  .ot-msg[data-variant=\"script\"], .ot-msg[data-variant=\"script\"][data-from=\"you\"] {\n    grid-template-columns: 8.5rem minmax(0, 1fr); grid-template-areas: \"head body\" \". foot\"; align-items: baseline;\n    max-width: 40rem; margin-inline-start: 0; text-align: start;\n  }\n  .ot-msg:is([data-variant=\"script\"], [data-variant=\"quote\"]) > .ot-avatar { display: none; }\n  .ot-msg[data-variant=\"script\"] > .ot-msg-head { flex-direction: column; align-items: flex-end; justify-content: flex-start; gap: 0; text-align: end; }\n  .ot-msg[data-variant=\"script\"] .ot-msg-head > b { font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; }\n  .ot-msg[data-variant=\"script\"] .ot-msg-head > b::after { content: \".\" / \"\"; }\n  .ot-msg[data-variant=\"script\"] .ot-msg-head > time { order: 0; font-variant-numeric: tabular-nums; }\n  .ot-msg[data-variant=\"script\"] .ot-msg-head > time::before { content: \"(\" / \"\"; }\n  .ot-msg[data-variant=\"script\"] .ot-msg-head > time::after { content: \")\" / \"\"; }\n  .ot-msg[data-variant=\"script\"] > .ot-msg-foot { justify-content: flex-start; }\n  /* Script and quote have no balloon at all: the tail goes, and the words keep the column's edge. */\n  .ot-msg:is([data-variant=\"script\"], [data-variant=\"quote\"]) .ot-bubble::before { display: none; }\n  .ot-msg:is([data-variant=\"script\"], [data-variant=\"quote\"]) .ot-msg-body > .ot-bubble { margin-inline: 0; }\n  .ot-msg:is([data-variant=\"script\"], [data-variant=\"quote\"]) .ot-msg-body > .ot-bubble:not([data-variant=\"ink\"]) { padding-inline: 0; }\n  @media (max-width: 30rem) {\n    .ot-msg[data-variant=\"script\"], .ot-msg[data-variant=\"script\"][data-from=\"you\"] { grid-template-columns: minmax(0, 1fr); grid-template-areas: \"head\" \"body\" \"foot\"; }\n    .ot-msg[data-variant=\"script\"] > .ot-msg-head { flex-direction: row; align-items: baseline; gap: var(--ot-space-3); }\n  }\n\n  /* ── quote ── the typographer's speech balloon: a large italic quotation mark where the face was.\n     Theirs opens at the top of the start; yours closes at the foot of the end, so each exchange is framed from both sides. */\n  .ot-msg[data-variant=\"quote\"]::before {\n    content: \"\\201C\" / \"\"; grid-area: face; align-self: start; margin-block-start: -0.08em;\n    font: italic 400 var(--ot-ff) / 0.8 var(--ot-expression); color: var(--ot-pencil);\n  }\n  .ot-msg[data-variant=\"quote\"][data-from=\"you\"]::before { content: \"\\201D\" / \"\"; align-self: end; margin-block: 0 -0.42em; }\n  :is([dir=\"rtl\"], [dir=\"rtl\"] *) .ot-msg[data-variant=\"quote\"]::before { scale: -1 1; }\n  .ot-msg[data-variant=\"quote\"][data-arriving]::before { animation: ot-message-quote var(--ot-moderato) var(--ot-spiccato) backwards; }\n  @keyframes ot-message-quote { from { opacity: 0; translate: 0 -0.12em; } }\n  @media (forced-colors: active) {\n    .ot-msg[data-variant=\"quote\"]::before { color: GrayText; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/message.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/message.css\"": {}
  },
  "meta": {
    "movement": "XI",
    "underneath": "native",
    "contract": "ot-msg"
  },
  "type": "registry:ui"
}
