{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "comment",
  "title": "Comment",
  "description": "Comments with nested replies, reactions, inline editing, moderation states, and timestamps.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "avatar",
    "button",
    "collapsible",
    "textarea",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/comment.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { EyeOff, Flag, Pencil } from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { Button } from \"@/components/ui/button\";\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"@/components/ui/collapsible\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const COMMENT_VARIANTS = [\"thread\", \"card\", \"compact\"] as const;\nexport type CommentVariant = (typeof COMMENT_VARIANTS)[number];\nexport type CommentModeration = \"visible\" | \"hidden\" | \"flagged\" | \"removed\";\nexport type CommentProps = ComponentProps<\"article\"> & {\n  variant?: CommentVariant;\n  moderation?: CommentModeration;\n  editing?: boolean;\n  defaultEditing?: boolean;\n  onEditingChange?: (editing: boolean) => void;\n};\ntype CommentContext = {\n  id: string;\n  variant: CommentVariant;\n  moderation: CommentModeration;\n  editing: boolean;\n  setEditing: (editing: boolean) => void;\n  revealed: boolean;\n  setRevealed: (revealed: boolean) => void;\n  editTriggerRef: React.RefObject<HTMLButtonElement | null>;\n};\nconst Context = createContext<CommentContext | null>(null);\nfunction useComment(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within Comment`);\n  return context;\n}\ntype ControlTone = \"ghost\" | \"secondary\" | \"primary\" | \"link\";\nconst controlVariants = cva(\n  \"inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-full border-0 py-0 font-medium whitespace-nowrap [transition:background-color_120ms_ease-out,color_120ms_ease-out,filter_120ms_ease-out,scale_140ms_var(--ease-out-quint)] focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid enabled:active:scale-[0.97] motion-reduce:transition-none motion-reduce:enabled:active:scale-100 [&_svg:not([class*='size-'])]:size-auto\",\n  {\n    variants: {\n      compact: {\n        true: \"h-6 px-2 text-[12px] has-[>svg]:px-2\",\n        false: \"h-7 px-2.5 text-[12.5px] has-[>svg]:px-2.5\",\n      },\n      tone: {\n        ghost:\n          \"bg-transparent text-muted-foreground hover:bg-accent hover:text-foreground dark:hover:bg-accent\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklab,var(--secondary)_85%,var(--foreground))]\",\n        primary:\n          \"bg-primary text-primary-foreground hover:bg-primary enabled:hover:brightness-[1.08] disabled:pointer-events-auto disabled:cursor-not-allowed disabled:opacity-45\",\n        link: \"h-auto bg-transparent p-0 text-foreground underline decoration-border-strong underline-offset-2 hover:decoration-current has-[>svg]:p-0\",\n      },\n    },\n    defaultVariants: { compact: false, tone: \"ghost\" },\n  },\n);\nconst controlClasses = (compact: boolean, tone: ControlTone = \"ghost\") =>\n  cn(controlVariants({ compact, tone }));\nconst buttonVariantFor = {\n  ghost: \"ghost\",\n  secondary: \"secondary\",\n  primary: \"default\",\n  link: \"link\",\n} as const;\n\nconst commentVariants = cva(\"grid min-w-0 rounded-[14px] text-[13px]/[18px] text-foreground\", {\n  variants: {\n    variant: { thread: \"gap-1.5\", card: \"gap-1.5\", compact: \"gap-1\" },\n    card: {\n      true: \"border bg-card px-4 py-3.5 text-card-foreground\",\n      false: \"border-0 bg-transparent p-0\",\n    },\n  },\n});\n\nconst noticeClasses =\n  \"m-0 flex flex-wrap items-center gap-1.5 text-[12.5px]/[18px] text-muted-foreground\";\n\nfunction initials(name: string) {\n  return name\n    .trim()\n    .split(/\\s+/)\n    .slice(0, 2)\n    .map((word) => word.charAt(0))\n    .join(\"\")\n    .toUpperCase();\n}\n\nexport function Comment({\n  variant = \"thread\",\n  moderation = \"visible\",\n  editing,\n  defaultEditing = false,\n  onEditingChange,\n  className,\n  children,\n  ...props\n}: CommentProps) {\n  const id = useId();\n  const nested = useContext(Context) !== null;\n  const card = variant === \"card\" && !nested;\n  const editTriggerRef = useRef<HTMLButtonElement>(null);\n  const [internal, setInternal] = useState(defaultEditing);\n  const [revealed, setRevealed] = useState(false);\n  const current = (editing ?? internal) && moderation !== \"removed\";\n  return (\n    <Context.Provider\n      value={{\n        id,\n        variant,\n        moderation,\n        editing: current,\n        setEditing: (next) => {\n          if (editing === undefined) setInternal(next);\n          onEditingChange?.(next);\n        },\n        revealed,\n        setRevealed,\n        editTriggerRef,\n      }}\n    >\n      <article\n        aria-labelledby={`${id}-author`}\n        data-slot=\"comment\"\n        className={cn(commentVariants({ variant, card }), className)}\n        {...props}\n        data-variant={variant}\n        data-moderation={moderation}\n      >\n        {children}\n      </article>\n    </Context.Provider>\n  );\n}\n\nexport function CommentHeader({ className, ...props }: ComponentProps<\"header\">) {\n  useComment(\"CommentHeader\");\n  return (\n    <header\n      data-slot=\"comment-header\"\n      className={cn(\"flex min-w-0 flex-wrap items-center gap-x-2 gap-y-0.5\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function CommentAvatar({\n  name,\n  src,\n  className,\n  ...props\n}: Omit<ComponentProps<\"span\">, \"children\"> & { name: string; src?: string }) {\n  const { variant } = useComment(\"CommentAvatar\");\n  // Radix shows the image only once it loads and falls back to initials otherwise.\n  return (\n    <Avatar\n      aria-hidden=\"true\"\n      data-slot=\"comment-avatar\"\n      className={cn(\n        \"grid shrink-0 place-items-center overflow-hidden rounded-full bg-muted font-medium text-muted-foreground shadow-[0_0_0_1px_oklch(1_0_0/0.08)]\",\n        variant === \"compact\" ? \"size-5 text-[9.5px]\" : \"size-6 text-[10.5px]\",\n        className,\n      )}\n      {...props}\n    >\n      {src ? <AvatarImage src={src} alt=\"\" className=\"object-cover\" /> : null}\n      <AvatarFallback className=\"text-[length:inherit]\">{initials(name)}</AvatarFallback>\n    </Avatar>\n  );\n}\n\nexport function CommentAuthor({ className, ...props }: ComponentProps<\"span\">) {\n  const { id } = useComment(\"CommentAuthor\");\n  return (\n    <span\n      data-slot=\"comment-author\"\n      className={cn(\"font-medium\", className)}\n      {...props}\n      id={`${id}-author`}\n    />\n  );\n}\n\nexport function CommentTime({\n  className,\n  ...props\n}: ComponentProps<\"time\"> & { dateTime: string }) {\n  useComment(\"CommentTime\");\n  return (\n    <time\n      data-slot=\"comment-time\"\n      className={cn(\"text-[12px] text-subtle-foreground tabular-nums\", className)}\n      {...props}\n    />\n  );\n}\n\n/** Shows \"Edited\" once a comment has been changed. */\nexport function CommentEdited({\n  children = \"Edited\",\n  className,\n  ...props\n}: ComponentProps<\"span\">) {\n  useComment(\"CommentEdited\");\n  return (\n    <span\n      data-slot=\"comment-edited\"\n      className={cn(\"text-[12px] text-subtle-foreground\", className)}\n      {...props}\n    >\n      · {children}\n    </span>\n  );\n}\n\nexport function CommentBody({ className, children, ...props }: ComponentProps<\"div\">) {\n  const context = useComment(\"CommentBody\");\n  const bodyId = `${context.id}-body`;\n  if (context.editing) return null;\n  if (context.moderation === \"removed\")\n    return (\n      <p\n        data-slot=\"comment-body\"\n        className={cn(noticeClasses, \"text-subtle-foreground italic\", className)}\n        {...props}\n      >\n        This comment was removed by a moderator.\n      </p>\n    );\n  const hidden = context.moderation === \"hidden\";\n  return (\n    <Collapsible\n      asChild\n      open={!hidden || context.revealed}\n      onOpenChange={(open) => context.setRevealed(open)}\n    >\n      <div data-slot=\"comment-body\" className={cn(\"grid min-w-0 gap-1.5\", className)} {...props}>\n        {context.moderation === \"flagged\" && (\n          <p className={noticeClasses}>\n            <span className=\"inline-flex items-center gap-1.25 rounded-full bg-warning/14 px-2 py-0.5 text-[11.5px]/4 font-medium text-warning-ink\">\n              <Flag size={12} className=\"size-3\" strokeWidth={2} aria-hidden=\"true\" />\n              Flagged for review\n            </span>\n          </p>\n        )}\n        {hidden && (\n          <p className={noticeClasses}>\n            <EyeOff size={13} className=\"size-[13px]\" strokeWidth={1.75} aria-hidden=\"true\" />\n            This comment is hidden.\n            <CollapsibleTrigger asChild>\n              <Button\n                type=\"button\"\n                variant=\"link\"\n                size=\"sm\"\n                aria-controls={context.revealed ? bodyId : undefined}\n                className={controlClasses(true, \"link\")}\n              >\n                {context.revealed ? \"Hide\" : \"Show\"}\n              </Button>\n            </CollapsibleTrigger>\n          </p>\n        )}\n        <CollapsibleContent asChild>\n          <div\n            id={bodyId}\n            className={cn(\n              \"min-w-0 text-pretty whitespace-pre-line text-foreground wrap-anywhere\",\n              hidden &&\n                \"animate-in duration-200 ease-out-quint fade-in-0 motion-reduce:animate-none\",\n            )}\n          >\n            {children}\n          </div>\n        </CollapsibleContent>\n      </div>\n    </Collapsible>\n  );\n}\n\nexport type CommentEditorProps = Omit<ComponentProps<\"form\">, \"onSubmit\" | \"defaultValue\"> & {\n  defaultValue: string;\n  onSave: (value: string) => void;\n  label?: string;\n};\nexport function CommentEditor({\n  defaultValue,\n  onSave,\n  label = \"Edit comment\",\n  className,\n  ...props\n}: CommentEditorProps) {\n  const context = useComment(\"CommentEditor\");\n  const [value, setValue] = useState(defaultValue);\n  const textareaRef = useRef<HTMLTextAreaElement>(null);\n  const compact = context.variant === \"compact\";\n  const { editing } = context;\n  useEffect(() => {\n    if (!editing) return;\n    setValue(defaultValue);\n    const textarea = textareaRef.current;\n    textarea?.focus();\n    textarea?.setSelectionRange(textarea.value.length, textarea.value.length);\n  }, [editing, defaultValue]);\n  if (!editing) return null;\n  const finish = () => {\n    context.setEditing(false);\n    requestAnimationFrame(() => context.editTriggerRef.current?.focus());\n  };\n  const save = () => {\n    if (!value.trim()) return;\n    onSave(value.trim());\n    finish();\n  };\n  const buttonPadding = compact ? \"px-2.5\" : \"px-3\";\n  return (\n    <form\n      data-slot=\"comment-editor\"\n      className={cn(\n        \"grid min-w-0 animate-in gap-2 duration-200 ease-out-quint fade-in-0 motion-reduce:animate-none\",\n        className,\n      )}\n      {...props}\n      onSubmit={(event) => {\n        event.preventDefault();\n        save();\n      }}\n    >\n      <Textarea\n        ref={textareaRef}\n        aria-label={label}\n        value={value}\n        rows={3}\n        onChange={(event) => setValue(event.target.value)}\n        onKeyDown={(event) => {\n          if (event.key === \"Escape\") {\n            event.preventDefault();\n            finish();\n          } else if (event.key === \"Enter\" && (event.metaKey || event.ctrlKey)) {\n            event.preventDefault();\n            save();\n          }\n        }}\n        className={cn(\n          \"box-border block field-sizing-fixed min-h-0 w-full min-w-0 resize-y border-0 px-2.5 py-2 text-[13px]/[18px] text-inherit shadow-[0_0_0_1px_var(--border)] transition-shadow duration-120 ease-[ease-out] placeholder:text-subtle-foreground focus-visible:shadow-[0_0_0_1px_var(--border-strong),0_0_0_4px_color-mix(in_oklab,var(--primary)_14%,transparent)] focus-visible:ring-0 focus-visible:outline-none md:text-[13px]/[18px] motion-reduce:transition-none\",\n          compact ? \"rounded-lg\" : \"rounded-[10px]\",\n          context.variant === \"card\" ? \"bg-background dark:bg-background\" : \"bg-card dark:bg-card\",\n        )}\n      />\n      <div className=\"flex justify-end gap-1.5\">\n        <Button\n          type=\"button\"\n          variant={buttonVariantFor.secondary}\n          size=\"sm\"\n          onClick={finish}\n          className={cn(controlClasses(compact, \"secondary\"), buttonPadding)}\n        >\n          Cancel\n        </Button>\n        <Button\n          type=\"submit\"\n          variant={buttonVariantFor.primary}\n          size=\"sm\"\n          disabled={!value.trim()}\n          className={cn(controlClasses(compact, \"primary\"), buttonPadding)}\n        >\n          Save\n        </Button>\n      </div>\n    </form>\n  );\n}\n\nexport function CommentActions({\n  \"aria-label\": label = \"Comment actions\",\n  className,\n  ...props\n}: ComponentProps<\"div\">) {\n  const context = useComment(\"CommentActions\");\n  if (context.editing || context.moderation === \"removed\") return null;\n  return (\n    // biome-ignore lint/a11y/useSemanticElements: a labelled group of action buttons, not a fieldset.\n    <div\n      role=\"group\"\n      aria-label={label}\n      data-slot=\"comment-actions\"\n      className={cn(\n        \"-ms-2 flex flex-wrap items-center gap-1 has-[>:first-child:not([data-slot=comment-action],[data-slot=comment-edit-trigger])]:ms-0\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function CommentAction({ className, ...props }: ComponentProps<\"button\">) {\n  const context = useComment(\"CommentAction\");\n  return (\n    <Button\n      type=\"button\"\n      variant={buttonVariantFor.ghost}\n      size=\"sm\"\n      data-slot=\"comment-action\"\n      className={cn(controlClasses(context.variant === \"compact\"), className)}\n      {...props}\n    />\n  );\n}\n\nexport function CommentEditTrigger({\n  children = \"Edit\",\n  onClick,\n  className,\n  ...props\n}: ComponentProps<\"button\">) {\n  const context = useComment(\"CommentEditTrigger\");\n  return (\n    <Button\n      ref={context.editTriggerRef}\n      variant={buttonVariantFor.ghost}\n      size=\"sm\"\n      data-slot=\"comment-edit-trigger\"\n      className={cn(controlClasses(context.variant === \"compact\"), className)}\n      {...props}\n      type=\"button\"\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) context.setEditing(true);\n      }}\n    >\n      <Pencil size={12} strokeWidth={1.75} aria-hidden=\"true\" className=\"size-3\" />\n      {children}\n    </Button>\n  );\n}\n\nexport function CommentReplies({\n  \"aria-label\": label = \"Replies\",\n  className,\n  ...props\n}: ComponentProps<\"section\">) {\n  const context = useComment(\"CommentReplies\");\n  const compact = context.variant === \"compact\";\n  return (\n    <section\n      aria-label={label}\n      data-slot=\"comment-replies\"\n      className={cn(\n        \"grid border-s *:animate-in *:fill-mode-both *:fade-in-0 *:slide-in-from-bottom-1 *:duration-240 *:ease-out-quint motion-reduce:*:animate-none [&>:nth-child(2)]:[animation-delay:40ms] [&>:nth-child(3)]:[animation-delay:80ms] [&>:nth-child(4)]:[animation-delay:120ms] [&>:nth-child(n+5)]:[animation-delay:160ms]\",\n        compact ? \"ms-[9.5px] mt-1 gap-3 ps-3.5\" : \"ms-[11.5px] mt-1.5 gap-4 ps-4.5\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/comment.tsx"
    }
  ],
  "categories": [
    "content",
    "community"
  ],
  "type": "registry:ui"
}