{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "message",
  "title": "Message",
  "description": "Message from the Get IT Done design system.",
  "dependencies": [
    "class-variance-authority@^0.7.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/file-icon.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/recipes.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/message.tsx",
      "content": "'use client';\n\nimport { cva } from 'class-variance-authority';\nimport * as React from 'react';\n\nimport { focusRing } from '@/lib/recipes';\nimport { cn } from '@/lib/utils';\nimport { FileIcon } from '@/components/ui/file-icon';\nimport { useLabels, usePreset } from '@/components/ui/labels-context';\n\n// Figma: Message 1242:996 (chat message: incoming/outgoing text, attachment, reactions, typing). Avatar is an exposed slot.\nconst bubbleVariants = cva('w-fit max-w-full', {\n  variants: {\n    density: {\n      default: 'px-4 py-2',\n      // 13px UI role for dense chat panels.\n      compact: 'typography-ui px-3 py-1',\n    },\n    own: {\n      true: '',\n      false: 'bg-muted text-text-default',\n    },\n    // Text size of the default density: default is the 16px paragraph role, sm the 14px small role.\n    size: {\n      default: '',\n      sm: '',\n    },\n    // Corner radius from the token radius tiers.\n    radius: {\n      item: 'rounded-item',\n      control: 'rounded-control',\n      surface: 'rounded-surface',\n      dialog: 'rounded-dialog',\n      full: 'rounded-full',\n    },\n    ownTone: {\n      brand: '',\n      neutral: '',\n      // Static inverse surface for own messages: dark in light and dark mode.\n      inverse: '',\n    },\n    // Bare drops the bubble: no fill, no padding, text on the page surface.\n    bare: {\n      true: '',\n      false: '',\n    },\n  },\n  compoundVariants: [\n    { own: true, ownTone: 'brand', className: 'bg-background-brand-bold-default text-text-on-brand' },\n    { own: true, ownTone: 'neutral', className: 'bg-background-neutral-active text-text-default' },\n    { own: true, ownTone: 'inverse', className: 'bg-surface-inverse-static text-text-on-inverse-static' },\n    { density: 'default', size: 'default', className: 'typography-p-ui' },\n    // 14px UI role with its 20px line height (the `small` role is 16px, too tight for a bubble).\n    { density: 'default', size: 'sm', className: 'typography-text-small' },\n    { bare: true, className: 'rounded-none bg-transparent p-0 text-text-default' },\n  ],\n  defaultVariants: { own: false, density: 'default', size: 'default', radius: 'control', ownTone: 'brand', bare: false },\n});\n\nexport interface MessageProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {\n  // Name shown above the bubble (\"You\" for own messages). Optional with `quiet`.\n  author?: React.ReactNode;\n  // Quiet variant: no visible sender. The author stays available to assistive tech; time and meta still show.\n  quiet?: boolean;\n  // Formatted time such as \"Friday 2:20pm\".\n  time?: React.ReactNode;\n  // Outgoing message: right aligned, brand bubble, no avatar.\n  own?: boolean;\n  // Colour of the own bubble. `neutral` keeps own messages without the brand colour, `inverse` uses the static inverse surface (dark in both modes). The default `brand` is the primary fill of the active theme (the Button primary role), blue in the zero theme.\n  ownTone?: 'brand' | 'neutral' | 'inverse';\n  // Text size of the bubble in default density: `default` is the 16px paragraph role (13px UI role in the zero preset), `sm` the 14px small role. Compact density keeps its own 13px role.\n  size?: 'default' | 'sm';\n  // Corner radius of the bubble from the token radius tiers. Default `control`.\n  radius?: 'item' | 'control' | 'surface' | 'dialog' | 'full';\n  // Reply variant without a bubble: the text sits on the page surface. Applies to incoming and own messages.\n  bare?: boolean;\n  // Avatar slot for incoming messages (use Avatar with AvatarOnlineIndicator).\n  avatar?: React.ReactNode;\n  // Text of the message. Use attachment for files.\n  text?: React.ReactNode;\n  attachment?: React.ReactNode;\n  reactions?: React.ReactNode;\n  // Shows an animated typing indicator instead of content.\n  typing?: boolean;\n  // `compact` sets the bubble text to the 13px UI role. `default` is unchanged.\n  density?: 'default' | 'compact';\n  // Where the time sits in the author row. `meta` (default, `author` in the zero preset) puts it at the end with `meta`. `author` puts it right next to the author name; `meta` stays at the end. Ignored with `quiet`, which has no visible name.\n  timePlacement?: 'meta' | 'author';\n  // Extra content in the author row after the time, e.g. a role chip or \"edited\".\n  meta?: React.ReactNode;\n  // Hover and focus-within action slot (react, reply, more). Use MessageActions with MessageAction buttons.\n  actions?: React.ReactNode;\n}\n\nconst Message = React.forwardRef<HTMLDivElement, MessageProps>(\n  ({ className, author, quiet = false, time, own = false, ownTone = 'brand', size = 'default', radius = 'control', bare = false, avatar, text, attachment, reactions, typing = false, density = 'default', timePlacement: timePlacementProp, meta, actions, ...props }, ref) => {\n  const l = useLabels();\n  const zero = usePreset() === 'zero';\n  const timePlacement = timePlacementProp ?? (zero ? 'author' : 'meta');\n  // Zero: bordered bubble with the corner on the author side square and the own header aligned to the end. Compact adds the 8px gap to the avatar.\n  const zeroCompact = zero && density === 'compact';\n  // Zero also caps the bubble width (36rem) so long text wraps inside the bubble and consumers need no wrapper.\n  // The default 16px paragraph role becomes the 13px UI role in zero (Figma text-small); an explicit `size=\"sm\"` or compact density keeps its own role.\n  const bubbleExtra = zero ? cn('max-w-xl', density === 'default' && size === 'default' && 'typography-ui', !bare && ['border border-solid px-3 py-2', own ? 'rounded-tr-none border-transparent' : 'rounded-tl-none border-border-default']) : undefined;\n  const authorNode = quiet\n    ? author\n      ? <span className=\"sr-only\">{author}</span>\n      : null\n    : <span className=\"typography-ui-medium text-text-default\">{author}</span>;\n  const timeBesideAuthor = timePlacement === 'author' && !quiet && time != null && time !== false;\n  return (\n    <div ref={ref} className={cn('group flex w-full', zeroCompact ? 'gap-2' : 'gap-3', own && 'justify-end', className)} {...props}>\n      {!own && avatar ? <div className=\"shrink-0\">{avatar}</div> : null}\n      <div className={cn('relative flex min-w-0 max-w-full flex-col gap-1', own && 'items-end', !own && 'flex-1')}>\n        {quiet && !time && !meta ? (\n          authorNode\n        ) : (\n        <div className={cn('flex w-full items-baseline gap-2', quiet || (zero && own) ? 'justify-end' : 'justify-between')}>\n          {timeBesideAuthor ? (\n            <span data-time-placement=\"author\" className=\"flex min-w-0 items-baseline gap-2\">\n              {authorNode}\n              <span className={cn('text-text-subtle', zero ? 'typography-micro' : 'typography-ui')}>{time}</span>\n            </span>\n          ) : (\n            authorNode\n          )}\n          {(time && !timeBesideAuthor) || meta ? (\n            <span className=\"flex items-baseline gap-2 typography-ui text-text-subtle\">\n              {time && !timeBesideAuthor ? <span>{time}</span> : null}\n              {meta}\n            </span>\n          ) : null}\n        </div>\n        )}\n        {actions && !typing ? (\n          <div\n            data-message-actions=\"\"\n            className={cn(\n              'absolute top-5 z-raised opacity-0 transition-opacity motion-reduce:transition-none',\n              'group-hover:opacity-100 group-focus-within:opacity-100',\n              own ? 'left-0' : 'right-0',\n            )}\n          >\n            {actions}\n          </div>\n        ) : null}\n        {typing ? (\n          <div role=\"status\" aria-label={l.typing} className={cn(bubbleVariants({ own, ownTone, density, size, radius }), 'flex items-center gap-1')}>\n            {[0, 1, 2].map((i) => (\n              <span key={i} className=\"h-1 w-1 animate-pulse rounded-full bg-icon-subtle motion-reduce:animate-none\" aria-hidden=\"true\" />\n            ))}\n          </div>\n        ) : (\n          <>\n            {text ? <div className={cn(bubbleVariants({ own, ownTone, bare, density, size, radius }), bubbleExtra)}>{text}</div> : null}\n            {attachment}\n            {reactions}\n          </>\n        )}\n      </div>\n    </div>\n  );\n  },\n);\nMessage.displayName = 'Message';\n\nexport interface MessageAttachmentProps extends React.HTMLAttributes<HTMLDivElement> {\n  name: React.ReactNode;\n  size?: React.ReactNode;\n  // Glyph in a round tile. Without it a FileIcon shows the type of a string `name`.\n  icon?: React.ReactNode;\n}\n\nconst MessageAttachment = React.forwardRef<HTMLDivElement, MessageAttachmentProps>(({ className, name, size, icon, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn('flex w-full items-center gap-3 rounded-control border border-solid border-border bg-card p-3', className)}\n    {...props}\n  >\n    {icon ? (\n      <span className=\"flex h-avatar-md w-avatar-md shrink-0 items-center justify-center rounded-full bg-muted text-icon-default\">{icon}</span>\n    ) : (\n      <FileIcon name={typeof name === 'string' ? name : undefined} size=\"xs\" />\n    )}\n    <span className=\"flex min-w-0 flex-col\">\n      <span className=\"typography-p-ui truncate text-text-default\">{name}</span>\n      {size ? <span className=\"typography-ui text-text-subtle\">{size}</span> : null}\n    </span>\n  </div>\n));\nMessageAttachment.displayName = 'MessageAttachment';\n\nexport interface MessageReactionProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n  emoji?: React.ReactNode;\n  // Icon reaction (Figma _Message reaction Type=Icon), for example a thumbs-up. Used instead of `emoji`; name the reaction with `aria-label`.\n  icon?: React.ReactNode;\n  count?: number;\n  // You reacted with this emoji.\n  active?: boolean;\n}\n\nconst MessageReaction = React.forwardRef<HTMLButtonElement, MessageReactionProps>(\n  ({ className, emoji, icon, count, active = false, type = 'button', ...props }, ref) => (\n    <button\n      ref={ref}\n      type={type}\n      aria-pressed={active}\n      className={cn(\n        'inline-flex h-6 items-center gap-1 rounded-full px-2 typography-ui text-text-default transition-colors',\n        active ? 'bg-background-brand-subtle' : 'bg-muted',\n        'hover:bg-muted-strong',\n        ...focusRing,\n        className,\n      )}\n      {...props}\n    >\n      {icon != null ? (\n        <span aria-hidden=\"true\" data-reaction-icon=\"\" className=\"flex shrink-0 text-icon-subtle [&>svg]:h-icon-xs [&>svg]:w-icon-xs\">\n          {icon}\n        </span>\n      ) : (\n        <span>{emoji}</span>\n      )}\n      {count !== undefined ? <span>{count}</span> : null}\n    </button>\n  ),\n);\nMessageReaction.displayName = 'MessageReaction';\n\nconst MessageReactions = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => {\n  const l = useLabels();\n  return <div ref={ref} role=\"group\" aria-label={l.reactions} className={cn('flex flex-wrap items-center gap-1', className)} {...props} />;\n});\nMessageReactions.displayName = 'MessageReactions';\n\nexport type MessageActionsProps = React.HTMLAttributes<HTMLDivElement>;\n\n// Compact toolbar surface for the Message actions slot. Children are MessageAction buttons.\nconst MessageActions = React.forwardRef<HTMLDivElement, MessageActionsProps>(({ className, ...props }, ref) => {\n  const l = useLabels();\n  return (\n    <div\n      ref={ref}\n      role=\"group\"\n      aria-label={l.messageActions}\n      className={cn('flex items-center gap-1 rounded-control border border-solid border-border bg-card p-1 shadow-popover', className)}\n      {...props}\n    />\n  );\n});\nMessageActions.displayName = 'MessageActions';\n\nexport interface MessageActionProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'aria-label'> {\n  // Accessible name, required because the button is usually icon only.\n  'aria-label': string;\n}\n\nconst MessageAction = React.forwardRef<HTMLButtonElement, MessageActionProps>(({ className, type = 'button', ...props }, ref) => (\n  <button\n    ref={ref}\n    type={type}\n    className={cn(\n      'inline-flex h-6 w-6 items-center justify-center rounded-item text-icon-default transition-colors',\n      'hover:bg-muted active:bg-muted-strong disabled:cursor-not-allowed disabled:opacity-disabled [&_svg]:h-icon-sm [&_svg]:w-icon-sm',\n      ...focusRing,\n      className,\n    )}\n    {...props}\n  />\n));\nMessageAction.displayName = 'MessageAction';\n\nexport { Message, MessageAction, MessageActions, MessageAttachment, MessageReaction, MessageReactions };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}