{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "block-discussion",
  "title": "Block Discussion",
  "description": "A popover interface for managing discussions: comments, replies, suggestions.",
  "dependencies": [
    "@platejs/comment",
    "date-fns",
    "@platejs/suggestion"
  ],
  "registryDependencies": [
    "button",
    "popover",
    "avatar",
    "dropdown-menu",
    "https://platejs.org/r/editor.json",
    "https://platejs.org/r/ai-node.json",
    "https://platejs.org/r/date-node.json",
    "https://platejs.org/r/emoji-node.json",
    "https://platejs.org/r/link-node.json",
    "https://platejs.org/r/mention-node.json",
    "https://platejs.org/r/highlight-style.json",
    "https://platejs.org/r/suggestion-kit.json",
    "https://platejs.org/r/discussion-kit.json",
    "https://platejs.org/r/basic-marks-kit.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/block-discussion.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\nimport type { PlateElementProps, RenderNodeWrapper } from 'platejs/react';\n\nimport { getDraftCommentKey } from '@platejs/comment';\nimport { CommentPlugin } from '@platejs/comment/react';\nimport { getTransientSuggestionKey } from '@platejs/suggestion';\nimport { SuggestionPlugin } from '@platejs/suggestion/react';\nimport {\n  MessageSquareTextIcon,\n  MessagesSquareIcon,\n  PencilLineIcon,\n} from 'lucide-react';\nimport { type AnyPluginConfig, type NodeEntry, PathApi } from 'platejs';\nimport { useEditorRef, usePluginOption } from 'platejs/react';\n\nimport { Button } from '@/components/ui/button';\nimport {\n  Popover,\n  PopoverAnchor,\n  PopoverContent,\n  PopoverTrigger,\n} from '@/components/ui/popover';\nimport { commentPlugin } from '@/registry/components/editor/plugins/comment-kit';\nimport type { TDiscussion } from '@/registry/components/editor/plugins/discussion-kit';\nimport { useBlockDiscussionItems } from '@/registry/lib/block-discussion-index';\nimport { suggestionPlugin } from '@/registry/components/editor/plugins/suggestion-kit';\n\nimport { BlockSuggestionCard, isResolvedSuggestion } from './block-suggestion';\nimport { Comment, CommentCreateForm } from './comment';\n\nexport const BlockDiscussion: RenderNodeWrapper<AnyPluginConfig> =\n  (_props) => (props) => <BlockCommentContent {...props} />;\n\nconst BlockCommentContent = ({ children, element }: PlateElementProps) => {\n  const editor = useEditorRef();\n  const commentsApi = editor.getApi(CommentPlugin).comment;\n  const blockPath = editor.api.findPath(element) ?? [];\n  const isTopLevelBlock = blockPath.length === 1;\n  const draftCommentNode = isTopLevelBlock\n    ? commentsApi.node({ at: blockPath, isDraft: true })\n    : undefined;\n  const commentNodes = isTopLevelBlock\n    ? [...commentsApi.nodes({ at: blockPath })]\n    : [];\n  const suggestionNodes = isTopLevelBlock\n    ? [\n        ...editor.getApi(SuggestionPlugin).suggestion.nodes({ at: blockPath }),\n      ].filter(([node]) => !node[getTransientSuggestionKey()])\n    : [];\n  const { resolvedDiscussions, resolvedSuggestions } =\n    useBlockDiscussionItems(blockPath);\n\n  const suggestionsCount = resolvedSuggestions.length;\n  const discussionsCount = resolvedDiscussions.length;\n  const totalCount = suggestionsCount + discussionsCount;\n\n  const activeSuggestionId = usePluginOption(suggestionPlugin, 'activeId');\n  const activeSuggestion =\n    activeSuggestionId &&\n    resolvedSuggestions.find((s) => s.suggestionId === activeSuggestionId);\n\n  const commentingBlock = usePluginOption(commentPlugin, 'commentingBlock');\n  const activeCommentId = usePluginOption(commentPlugin, 'activeId');\n  const isCommenting = activeCommentId === getDraftCommentKey();\n  const activeDiscussion =\n    activeCommentId &&\n    resolvedDiscussions.find((d) => d.id === activeCommentId);\n\n  const noneActive = !activeSuggestion && !activeDiscussion;\n\n  const sortedMergedData = [\n    ...resolvedDiscussions,\n    ...resolvedSuggestions,\n  ].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());\n\n  const selected =\n    resolvedDiscussions.some((d) => d.id === activeCommentId) ||\n    resolvedSuggestions.some((s) => s.suggestionId === activeSuggestionId);\n\n  const [_open, setOpen] = React.useState(selected);\n\n  // in some cases, we may comment the multiple blocks\n  const commentingCurrent =\n    !!commentingBlock && PathApi.equals(blockPath, commentingBlock);\n\n  const open =\n    _open ||\n    selected ||\n    (isCommenting && !!draftCommentNode && commentingCurrent);\n\n  const anchorElement = React.useMemo(() => {\n    let activeNode: NodeEntry | undefined;\n\n    if (activeSuggestion) {\n      activeNode = suggestionNodes.find(\n        ([node]) =>\n          editor.getApi(SuggestionPlugin).suggestion.nodeId(node) ===\n          activeSuggestion.suggestionId\n      );\n    }\n\n    if (activeCommentId) {\n      if (activeCommentId === getDraftCommentKey()) {\n        activeNode = draftCommentNode;\n      } else {\n        activeNode = commentNodes.find(\n          ([node]) =>\n            editor.getApi(commentPlugin).comment.nodeId(node) ===\n            activeCommentId\n        );\n      }\n    }\n\n    if (!activeNode) return null;\n\n    return editor.api.toDOMNode(activeNode[0])!;\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [\n    open,\n    activeSuggestion,\n    activeCommentId,\n    editor.api,\n    suggestionNodes,\n    draftCommentNode,\n    commentNodes,\n  ]);\n\n  if (!isTopLevelBlock) return <>{children}</>;\n\n  if (suggestionsCount + resolvedDiscussions.length === 0 && !draftCommentNode)\n    return <div className=\"w-full\">{children}</div>;\n\n  return (\n    <div className=\"flex w-full justify-between\">\n      <Popover\n        open={open}\n        onOpenChange={(_open_) => {\n          if (!_open_ && isCommenting && draftCommentNode) {\n            editor.tf.unsetNodes(getDraftCommentKey(), {\n              at: [],\n              mode: 'lowest',\n              match: (n) => n[getDraftCommentKey()],\n            });\n          }\n          setOpen(_open_);\n        }}\n      >\n        <div className=\"w-full\">{children}</div>\n        {anchorElement && (\n          <PopoverAnchor\n            asChild\n            className=\"w-full\"\n            virtualRef={{ current: anchorElement }}\n          />\n        )}\n\n        <PopoverContent\n          className=\"max-h-[min(50dvh,calc(-24px+var(--radix-popper-available-height)))] w-[380px] min-w-[130px] max-w-[calc(100vw-24px)] overflow-y-auto p-0 data-[state=closed]:opacity-0\"\n          onCloseAutoFocus={(e) => e.preventDefault()}\n          onOpenAutoFocus={(e) => e.preventDefault()}\n          align=\"center\"\n          side=\"bottom\"\n        >\n          {isCommenting ? (\n            <CommentCreateForm className=\"p-4\" focusOnMount />\n          ) : noneActive ? (\n            sortedMergedData.map((item, index) =>\n              isResolvedSuggestion(item) ? (\n                <BlockSuggestionCard\n                  key={item.suggestionId}\n                  idx={index}\n                  isLast={index === sortedMergedData.length - 1}\n                  suggestion={item}\n                />\n              ) : (\n                <BlockComment\n                  key={item.id}\n                  discussion={item}\n                  isLast={index === sortedMergedData.length - 1}\n                />\n              )\n            )\n          ) : (\n            <>\n              {activeSuggestion && (\n                <BlockSuggestionCard\n                  key={activeSuggestion.suggestionId}\n                  idx={0}\n                  isLast={true}\n                  suggestion={activeSuggestion}\n                />\n              )}\n\n              {activeDiscussion && (\n                <BlockComment discussion={activeDiscussion} isLast={true} />\n              )}\n            </>\n          )}\n        </PopoverContent>\n\n        {totalCount > 0 && (\n          <div className=\"relative left-0 size-0 select-none\">\n            <PopoverTrigger asChild>\n              <Button\n                variant=\"ghost\"\n                className=\"!px-1.5 mt-1 ml-1 flex h-6 gap-1 py-0 text-muted-foreground/80 hover:text-muted-foreground/80 data-[active=true]:bg-muted\"\n                data-active={open}\n                contentEditable={false}\n              >\n                {suggestionsCount > 0 && discussionsCount === 0 && (\n                  <PencilLineIcon className=\"size-4 shrink-0\" />\n                )}\n\n                {suggestionsCount === 0 && discussionsCount > 0 && (\n                  <MessageSquareTextIcon className=\"size-4 shrink-0\" />\n                )}\n\n                {suggestionsCount > 0 && discussionsCount > 0 && (\n                  <MessagesSquareIcon className=\"size-4 shrink-0\" />\n                )}\n\n                <span className=\"font-semibold text-xs\">{totalCount}</span>\n              </Button>\n            </PopoverTrigger>\n          </div>\n        )}\n      </Popover>\n    </div>\n  );\n};\n\nfunction BlockComment({\n  discussion,\n  isLast,\n}: {\n  discussion: TDiscussion;\n  isLast: boolean;\n}) {\n  const [editingId, setEditingId] = React.useState<string | null>(null);\n\n  return (\n    <React.Fragment key={discussion.id}>\n      <div className=\"p-4\">\n        {discussion.comments.map((comment, index) => (\n          <Comment\n            key={comment.id ?? index}\n            comment={comment}\n            discussionLength={discussion.comments.length}\n            documentContent={discussion?.documentContent}\n            editingId={editingId}\n            index={index}\n            setEditingId={setEditingId}\n            showDocumentContent\n          />\n        ))}\n        <CommentCreateForm discussionId={discussion.id} />\n      </div>\n\n      {!isLast && <div className=\"h-px w-full bg-muted\" />}\n    </React.Fragment>\n  );\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/registry/lib/block-discussion-index.ts",
      "content": "'use client';\n\nimport * as React from 'react';\n\nimport type { TResolvedSuggestion } from '@platejs/suggestion';\nimport type { PlateEditor } from 'platejs/react';\n\nimport { CommentPlugin } from '@platejs/comment/react';\nimport { getSuggestionKey, keyId2SuggestionId } from '@platejs/suggestion';\nimport { SuggestionPlugin } from '@platejs/suggestion/react';\nimport {\n  type NodeEntry,\n  NodeApi,\n  type Path,\n  type TCommentText,\n  type TElement,\n  type TSuggestionText,\n  ElementApi,\n  KEYS,\n  PathApi,\n  TextApi,\n} from 'platejs';\nimport { useEditorRef, useEditorVersion, usePluginOption } from 'platejs/react';\n\nimport {\n  type TDiscussion,\n  discussionPlugin,\n} from '@/registry/components/editor/plugins/discussion-kit';\n\nimport type { TComment } from '@/registry/ui/comment';\n\nexport interface ResolvedSuggestion extends TResolvedSuggestion {\n  comments: TComment[];\n}\n\nexport const BLOCK_SUGGESTION_TOKEN = '__block__';\n\ntype BlockDiscussionEntry = NodeEntry<\n  TCommentText | TElement | TSuggestionText\n>;\ntype SuggestionEntry = NodeEntry<TElement | TSuggestionText>;\n\ntype BlockDiscussionIndex = {\n  discussionsByBlock: Map<string, TDiscussion[]>;\n  suggestionsByBlock: Map<string, ResolvedSuggestion[]>;\n};\n\ntype BuildBlockDiscussionIndexOptions = {\n  entries: BlockDiscussionEntry[];\n  discussions: TDiscussion[];\n  getCommentId: (node: TCommentText) => string | undefined;\n  getSuggestionData: (node: TElement | TSuggestionText) =>\n    | {\n        createdAt: Date | number | string;\n        id: string;\n        isLineBreak?: boolean;\n        newProperties?: Record<string, unknown>;\n        properties?: Record<string, unknown>;\n        type: 'insert' | 'remove' | 'update';\n        userId: string;\n      }\n    | undefined;\n  getSuggestionDataList: (node: TSuggestionText) => Array<{\n    id: string;\n    newProperties?: Record<string, unknown>;\n    properties?: Record<string, unknown>;\n    type: 'insert' | 'remove' | 'update';\n  }>;\n  getSuggestionId: (node: TElement | TSuggestionText) => string | undefined;\n  isBlockSuggestion: (node: TElement | TSuggestionText) => boolean;\n};\n\nconst discussionIndexCache = new WeakMap<\n  PlateEditor,\n  {\n    discussions: TDiscussion[];\n    index: BlockDiscussionIndex;\n    version: number;\n  }\n>();\n\nconst TYPE_TEXT_MAP: Record<string, (node?: TElement) => string> = {\n  [KEYS.audio]: () => 'Audio',\n  [KEYS.blockquote]: () => 'Blockquote',\n  [KEYS.callout]: () => 'Callout',\n  [KEYS.codeBlock]: () => 'Code Block',\n  [KEYS.column]: () => 'Column',\n  [KEYS.equation]: () => 'Equation',\n  [KEYS.file]: () => 'File',\n  [KEYS.h1]: () => 'Heading 1',\n  [KEYS.h2]: () => 'Heading 2',\n  [KEYS.h3]: () => 'Heading 3',\n  [KEYS.h4]: () => 'Heading 4',\n  [KEYS.h5]: () => 'Heading 5',\n  [KEYS.h6]: () => 'Heading 6',\n  [KEYS.hr]: () => 'Horizontal Rule',\n  [KEYS.img]: () => 'Image',\n  [KEYS.mediaEmbed]: () => 'Media',\n  [KEYS.p]: (node) => {\n    if (node?.[KEYS.listType] === KEYS.listTodo) return 'Todo List';\n    if (node?.[KEYS.listType] === KEYS.ol) return 'Ordered List';\n    if (node?.[KEYS.listType] === KEYS.ul) return 'List';\n\n    return 'Paragraph';\n  },\n  [KEYS.table]: () => 'Table',\n  [KEYS.toc]: () => 'Table of Contents',\n  [KEYS.toggle]: () => 'Toggle',\n  [KEYS.video]: () => 'Video',\n};\n\nconst appendByKey = <T>(map: Map<string, T[]>, key: string, value: T) => {\n  const values = map.get(key);\n\n  if (values) {\n    values.push(value);\n    return;\n  }\n\n  map.set(key, [value]);\n};\n\nconst getBlockKey = (path: Path) => path.join(',');\n\nconst getTopLevelPath = (path: Path): Path | null =>\n  path.length > 0 ? path.slice(0, 1) : null;\n\nconst getSuggestionIds = (\n  node: TCommentText | TElement | TSuggestionText,\n  getSuggestionDataList: BuildBlockDiscussionIndexOptions['getSuggestionDataList'],\n  getSuggestionId: BuildBlockDiscussionIndexOptions['getSuggestionId']\n) => {\n  if (TextApi.isText(node)) {\n    const dataList = getSuggestionDataList(node as TSuggestionText);\n    const updateIds = dataList\n      .filter((data) => data.type === 'update')\n      .map((data) => data.id);\n\n    if (updateIds.length > 0) return updateIds;\n\n    const suggestionId = getSuggestionId(node as TSuggestionText);\n\n    return suggestionId ? [suggestionId] : [];\n  }\n\n  if (ElementApi.isElement(node)) {\n    const suggestionId = getSuggestionId(node);\n\n    return suggestionId ? [suggestionId] : [];\n  }\n\n  return [];\n};\n\nconst suggestionTypeText = (node: TElement) =>\n  (TYPE_TEXT_MAP[node.type] ?? (() => node.type))(node);\n\nconst formatSuggestionDateText = (date: string) => {\n  const elementDate = new Date(date);\n\n  if (Number.isNaN(elementDate.getTime())) return date;\n\n  const today = new Date();\n  const yesterday = new Date(today);\n  const tomorrow = new Date(today);\n\n  yesterday.setDate(today.getDate() - 1);\n  tomorrow.setDate(today.getDate() + 1);\n\n  const sameDay = (left: Date, right: Date) =>\n    left.getDate() === right.getDate() &&\n    left.getMonth() === right.getMonth() &&\n    left.getFullYear() === right.getFullYear();\n\n  if (sameDay(elementDate, today)) return 'Today';\n  if (sameDay(elementDate, yesterday)) return 'Yesterday';\n  if (sameDay(elementDate, tomorrow)) return 'Tomorrow';\n\n  return elementDate.toLocaleDateString(undefined, {\n    day: 'numeric',\n    month: 'long',\n    year: 'numeric',\n  });\n};\n\nconst getInlineSuggestionElementText = (node: TElement) => {\n  if (typeof node.value === 'string' && node.value.length > 0) {\n    return node.value;\n  }\n\n  if (typeof node.date === 'string' && node.date.length > 0) {\n    return formatSuggestionDateText(node.date);\n  }\n\n  if (\n    node.type === KEYS.inlineEquation &&\n    typeof (node as TElement & { texExpression?: unknown }).texExpression ===\n      'string' &&\n    (node as TElement & { texExpression: string }).texExpression.length > 0\n  ) {\n    return (node as TElement & { texExpression: string }).texExpression;\n  }\n\n  const nodeText = NodeApi.string(node);\n\n  if (nodeText.length > 0) {\n    return nodeText;\n  }\n};\n\nconst toResolvedSuggestion = ({\n  discussionsById,\n  entries,\n  getSuggestionData,\n  getSuggestionDataList,\n  id,\n  isBlockSuggestion,\n}: {\n  discussionsById: Map<string, TDiscussion>;\n  entries: SuggestionEntry[];\n  getSuggestionData: BuildBlockDiscussionIndexOptions['getSuggestionData'];\n  getSuggestionDataList: BuildBlockDiscussionIndexOptions['getSuggestionDataList'];\n  id: string;\n  isBlockSuggestion: BuildBlockDiscussionIndexOptions['isBlockSuggestion'];\n}): ResolvedSuggestion | null => {\n  const sortedEntries = [...entries].sort(([, path1], [, path2]) =>\n    PathApi.isChild(path1, path2) ? -1 : 1\n  );\n\n  if (sortedEntries.length === 0) return null;\n\n  let newText = '';\n  let text = '';\n  let properties: Record<string, unknown> = {};\n  let newProperties: Record<string, unknown> = {};\n\n  sortedEntries.forEach(([node]) => {\n    if (TextApi.isText(node)) {\n      getSuggestionDataList(node as TSuggestionText).forEach((data) => {\n        if (data.id !== id) return;\n\n        switch (data.type) {\n          case 'insert': {\n            newText += node.text;\n            break;\n          }\n          case 'remove': {\n            text += node.text;\n            break;\n          }\n          case 'update': {\n            properties = { ...properties, ...data.properties };\n            newProperties = { ...newProperties, ...data.newProperties };\n            newText += node.text;\n            break;\n          }\n        }\n      });\n\n      return;\n    }\n\n    if (!ElementApi.isElement(node)) return;\n\n    const suggestionData = getSuggestionData(node);\n\n    if (suggestionData?.id !== keyId2SuggestionId(id)) return;\n\n    const inlineSuggestionText = getInlineSuggestionElementText(node);\n\n    if (inlineSuggestionText) {\n      if (suggestionData.type === 'insert') {\n        newText += inlineSuggestionText;\n      } else if (suggestionData.type === 'remove') {\n        text += inlineSuggestionText;\n      } else if (suggestionData.type === 'update') {\n        properties = { ...properties, ...suggestionData.properties };\n        newProperties = {\n          ...newProperties,\n          ...suggestionData.newProperties,\n        };\n        newText += inlineSuggestionText;\n      }\n\n      return;\n    }\n\n    if (!isBlockSuggestion(node)) return;\n\n    const nextText = suggestionData.isLineBreak\n      ? BLOCK_SUGGESTION_TOKEN\n      : `${BLOCK_SUGGESTION_TOKEN}${suggestionTypeText(node)}`;\n\n    if (suggestionData.type === 'insert') {\n      newText += nextText;\n    } else if (suggestionData.type === 'remove') {\n      text += nextText;\n    }\n  });\n\n  const suggestionData = getSuggestionData(sortedEntries[0][0]);\n\n  if (!suggestionData) return null;\n\n  const keyId = getSuggestionKey(id);\n  const comments = discussionsById.get(id)?.comments ?? [];\n  const createdAt = new Date(suggestionData.createdAt);\n  const suggestionId = keyId2SuggestionId(id);\n\n  if (suggestionData.type === 'update') {\n    return {\n      comments,\n      createdAt,\n      keyId,\n      newProperties,\n      newText,\n      properties,\n      suggestionId,\n      type: 'update',\n      userId: suggestionData.userId,\n    };\n  }\n\n  if (newText.length > 0 && text.length > 0) {\n    return {\n      comments,\n      createdAt,\n      keyId,\n      newText,\n      suggestionId,\n      text,\n      type: 'replace',\n      userId: suggestionData.userId,\n    };\n  }\n\n  if (newText.length > 0) {\n    return {\n      comments,\n      createdAt,\n      keyId,\n      newText,\n      suggestionId,\n      type: 'insert',\n      userId: suggestionData.userId,\n    };\n  }\n\n  if (text.length > 0) {\n    return {\n      comments,\n      createdAt,\n      keyId,\n      suggestionId,\n      text,\n      type: 'remove',\n      userId: suggestionData.userId,\n    };\n  }\n\n  return null;\n};\n\nexport const buildBlockDiscussionIndex = ({\n  discussions,\n  entries,\n  getCommentId,\n  getSuggestionData,\n  getSuggestionDataList,\n  getSuggestionId,\n  isBlockSuggestion,\n}: BuildBlockDiscussionIndexOptions): BlockDiscussionIndex => {\n  const commentOwnerById = new Map<string, Path>();\n  const suggestionOwnerById = new Map<string, Path>();\n  const commentIds = new Set<string>();\n  const suggestionEntriesById = new Map<string, SuggestionEntry[]>();\n  const discussionsById = new Map(\n    discussions.map((discussion) => [discussion.id, discussion])\n  );\n\n  entries.forEach(([node, path]) => {\n    const blockPath = getTopLevelPath(path);\n\n    if (!blockPath) return;\n\n    if (TextApi.isText(node)) {\n      const commentId = getCommentId(node);\n\n      if (commentId) {\n        commentIds.add(commentId);\n\n        if (!commentOwnerById.has(commentId)) {\n          commentOwnerById.set(commentId, blockPath);\n        }\n      }\n    }\n\n    getSuggestionIds(node, getSuggestionDataList, getSuggestionId).forEach(\n      (suggestionId) => {\n        if (!suggestionOwnerById.has(suggestionId)) {\n          suggestionOwnerById.set(suggestionId, blockPath);\n        }\n\n        appendByKey(suggestionEntriesById, suggestionId, [\n          node as TElement | TSuggestionText,\n          path,\n        ]);\n      }\n    );\n  });\n\n  const discussionsByBlock = new Map<string, TDiscussion[]>();\n\n  discussions.forEach((discussion) => {\n    const ownerPath = commentOwnerById.get(discussion.id);\n\n    if (!ownerPath || !commentIds.has(discussion.id) || discussion.isResolved) {\n      return;\n    }\n\n    appendByKey(discussionsByBlock, getBlockKey(ownerPath), {\n      ...discussion,\n      createdAt: new Date(discussion.createdAt),\n    });\n  });\n\n  const suggestionsByBlock = new Map<string, ResolvedSuggestion[]>();\n\n  suggestionEntriesById.forEach((suggestionEntries, suggestionId) => {\n    const ownerPath = suggestionOwnerById.get(suggestionId);\n\n    if (!ownerPath) return;\n\n    const resolvedSuggestion = toResolvedSuggestion({\n      discussionsById,\n      entries: suggestionEntries,\n      getSuggestionData,\n      getSuggestionDataList,\n      id: suggestionId,\n      isBlockSuggestion,\n    });\n\n    if (!resolvedSuggestion) return;\n\n    appendByKey(suggestionsByBlock, getBlockKey(ownerPath), resolvedSuggestion);\n  });\n\n  return {\n    discussionsByBlock,\n    suggestionsByBlock,\n  };\n};\n\nconst getDiscussionIndex = (\n  editor: PlateEditor,\n  discussions: TDiscussion[],\n  version: number\n) => {\n  const cached = discussionIndexCache.get(editor);\n\n  if (\n    cached &&\n    cached.version === version &&\n    cached.discussions === discussions\n  ) {\n    return cached.index;\n  }\n\n  const commentApi = editor.getApi(CommentPlugin).comment;\n  const suggestionApi = editor.getApi(SuggestionPlugin).suggestion;\n\n  const index = buildBlockDiscussionIndex({\n    discussions,\n    entries: [...editor.api.nodes({ at: [], mode: 'all' })],\n    getCommentId: (node) => commentApi.nodeId(node),\n    getSuggestionData: (node) => suggestionApi.suggestionData(node),\n    getSuggestionDataList: (node) => suggestionApi.dataList(node),\n    getSuggestionId: (node) => suggestionApi.nodeId(node),\n    isBlockSuggestion: (node) =>\n      ElementApi.isElement(node) && suggestionApi.isBlockSuggestion(node),\n  });\n\n  discussionIndexCache.set(editor, { discussions, index, version });\n\n  return index;\n};\n\nexport const useBlockDiscussionItems = (blockPath: Path) => {\n  const editor = useEditorRef();\n  const discussions = usePluginOption(discussionPlugin, 'discussions');\n  const version = useEditorVersion() ?? 0;\n\n  return React.useMemo(() => {\n    const index = getDiscussionIndex(editor, discussions, version);\n    const blockKey = getBlockKey(blockPath);\n\n    return {\n      resolvedDiscussions: index.discussionsByBlock.get(blockKey) ?? [],\n      resolvedSuggestions: index.suggestionsByBlock.get(blockKey) ?? [],\n    };\n  }, [blockPath, discussions, editor, version]);\n};\n",
      "type": "registry:lib"
    },
    {
      "path": "src/registry/ui/block-suggestion.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\nimport { acceptSuggestion, rejectSuggestion } from '@platejs/suggestion';\nimport { SuggestionPlugin } from '@platejs/suggestion/react';\nimport { CheckIcon, XIcon } from 'lucide-react';\nimport { useEditorPlugin, usePluginOption } from 'platejs/react';\n\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';\nimport { Button } from '@/components/ui/button';\nimport {\n  type TDiscussion,\n  discussionPlugin,\n} from '@/registry/components/editor/plugins/discussion-kit';\n\nimport { Comment, CommentCreateForm, formatCommentDate } from './comment';\nimport {\n  BLOCK_SUGGESTION_TOKEN,\n  type ResolvedSuggestion,\n} from '@/registry/lib/block-discussion-index';\n\nexport function BlockSuggestionCard({\n  idx,\n  isLast,\n  suggestion,\n}: {\n  idx: number;\n  isLast: boolean;\n  suggestion: ResolvedSuggestion;\n}) {\n  const { api, editor } = useEditorPlugin(SuggestionPlugin);\n\n  const userInfo = usePluginOption(discussionPlugin, 'user', suggestion.userId);\n\n  const accept = (suggestion: ResolvedSuggestion) => {\n    api.suggestion.withoutSuggestions(() => {\n      acceptSuggestion(editor, suggestion);\n    });\n  };\n\n  const reject = (suggestion: ResolvedSuggestion) => {\n    api.suggestion.withoutSuggestions(() => {\n      rejectSuggestion(editor, suggestion);\n    });\n  };\n\n  const [hovering, setHovering] = React.useState(false);\n\n  const suggestionText2Array = (text: string) => {\n    if (text === BLOCK_SUGGESTION_TOKEN) return ['line breaks'];\n\n    return text.split(BLOCK_SUGGESTION_TOKEN).filter(Boolean);\n  };\n\n  const getRemoveSummaryItems = (text: string) => {\n    const items = suggestionText2Array(text).map((item) => {\n      if (item === 'column_group') return 'Column';\n      if (item === 'code_block') return 'Code Block';\n\n      return item;\n    });\n\n    if (items.includes('Table')) return ['Table'];\n    if (items.includes('Code Block')) return ['Code Block'];\n    if (items.includes('Column')) return ['Column'];\n\n    return items;\n  };\n\n  const [editingId, setEditingId] = React.useState<string | null>(null);\n\n  return (\n    <div\n      key={`${suggestion.suggestionId}-${idx}`}\n      className=\"relative\"\n      onMouseEnter={() => setHovering(true)}\n      onMouseLeave={() => setHovering(false)}\n    >\n      <div className=\"flex flex-col p-4\">\n        <div className=\"relative flex items-center\">\n          {/* Replace to your own backend or refer to potion */}\n          <Avatar className=\"size-5\">\n            <AvatarImage alt={userInfo?.name} src={userInfo?.avatarUrl} />\n            <AvatarFallback>{userInfo?.name?.[0]}</AvatarFallback>\n          </Avatar>\n          <h4 className=\"mx-2 font-semibold text-sm leading-none\">\n            {userInfo?.name}\n          </h4>\n          <div className=\"text-muted-foreground/80 text-xs leading-none\">\n            <span className=\"mr-1\">\n              {formatCommentDate(new Date(suggestion.createdAt))}\n            </span>\n          </div>\n        </div>\n\n        <div className=\"relative mt-1 mb-4 pl-[32px]\">\n          <div className=\"flex flex-col gap-2\">\n            {suggestion.type === 'remove' &&\n              getRemoveSummaryItems(suggestion.text!).map((text, index) => (\n                <div key={index} className=\"flex items-center gap-2\">\n                  <span className=\"text-muted-foreground text-sm\">Delete:</span>\n\n                  <span key={index} className=\"text-sm\">\n                    {text}\n                  </span>\n                </div>\n              ))}\n\n            {suggestion.type === 'insert' &&\n              suggestionText2Array(suggestion.newText!).map((text, index) => (\n                <div key={index} className=\"flex items-center gap-2\">\n                  <span className=\"text-muted-foreground text-sm\">Add:</span>\n\n                  <span key={index} className=\"text-sm\">\n                    {text || 'line breaks'}\n                  </span>\n                </div>\n              ))}\n\n            {suggestion.type === 'replace' && (\n              <div className=\"flex flex-col gap-2\">\n                {suggestionText2Array(suggestion.newText!).map(\n                  (text, index) => (\n                    <React.Fragment key={index}>\n                      <div\n                        key={index}\n                        className=\"flex items-start gap-2 text-brand/80\"\n                      >\n                        <span className=\"text-sm\">with:</span>\n                        <span className=\"text-sm\">{text || 'line breaks'}</span>\n                      </div>\n                    </React.Fragment>\n                  )\n                )}\n\n                {suggestionText2Array(suggestion.text!).map((text, index) => (\n                  <React.Fragment key={index}>\n                    <div key={index} className=\"flex items-start gap-2\">\n                      <span className=\"text-muted-foreground text-sm\">\n                        {index === 0 ? 'Replace:' : 'Delete:'}\n                      </span>\n                      <span className=\"text-sm\">{text || 'line breaks'}</span>\n                    </div>\n                  </React.Fragment>\n                ))}\n              </div>\n            )}\n\n            {suggestion.type === 'update' && (\n              <div className=\"flex items-center gap-2\">\n                <span className=\"text-muted-foreground text-sm\">\n                  {Object.keys(suggestion.properties).map((key) => (\n                    <span key={key}>Un{key}</span>\n                  ))}\n\n                  {Object.keys(suggestion.newProperties).map((key) => (\n                    <span key={key}>\n                      {key.charAt(0).toUpperCase() + key.slice(1)}\n                    </span>\n                  ))}\n                </span>\n                <span className=\"text-sm\">{suggestion.newText}</span>\n              </div>\n            )}\n          </div>\n        </div>\n\n        {suggestion.comments.map((comment, index) => (\n          <Comment\n            key={comment.id ?? index}\n            comment={comment}\n            discussionLength={suggestion.comments.length}\n            documentContent=\"__suggestion__\"\n            editingId={editingId}\n            index={index}\n            setEditingId={setEditingId}\n          />\n        ))}\n\n        {hovering && (\n          <div className=\"absolute top-4 right-4 flex gap-2\">\n            <Button\n              variant=\"ghost\"\n              className=\"size-6 p-1 text-muted-foreground\"\n              onClick={() => accept(suggestion)}\n            >\n              <CheckIcon className=\"size-4\" />\n            </Button>\n\n            <Button\n              variant=\"ghost\"\n              className=\"size-6 p-1 text-muted-foreground\"\n              onClick={() => reject(suggestion)}\n            >\n              <XIcon className=\"size-4\" />\n            </Button>\n          </div>\n        )}\n\n        <CommentCreateForm discussionId={suggestion.suggestionId} />\n      </div>\n\n      {!isLast && <div className=\"h-px w-full bg-muted\" />}\n    </div>\n  );\n}\n\nexport const isResolvedSuggestion = (\n  suggestion: ResolvedSuggestion | TDiscussion\n): suggestion is ResolvedSuggestion => 'suggestionId' in suggestion;\n",
      "type": "registry:ui"
    },
    {
      "path": "src/registry/ui/comment.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\nimport type { CreatePlateEditorOptions } from 'platejs/react';\n\nimport { getCommentKey, getDraftCommentKey } from '@platejs/comment';\nimport { CommentPlugin, useCommentId } from '@platejs/comment/react';\nimport {\n  differenceInDays,\n  differenceInHours,\n  differenceInMinutes,\n  format,\n} from 'date-fns';\nimport {\n  ArrowUpIcon,\n  CheckIcon,\n  MoreHorizontalIcon,\n  PencilIcon,\n  TrashIcon,\n  XIcon,\n} from 'lucide-react';\nimport {\n  type NodeEntry,\n  type TCommentText,\n  type Value,\n  KEYS,\n  nanoid,\n  NodeApi,\n} from 'platejs';\nimport {\n  Plate,\n  useEditorPlugin,\n  useEditorRef,\n  usePlateEditor,\n  usePluginOption,\n} from 'platejs/react';\n\nimport { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';\nimport { Button } from '@/components/ui/button';\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from '@/components/ui/dropdown-menu';\nimport { cn } from '@/lib/utils';\nimport { BasicMarksKit } from '@/registry/components/editor/plugins/basic-marks-kit';\nimport {\n  type TDiscussion,\n  discussionPlugin,\n} from '@/registry/components/editor/plugins/discussion-kit';\n\nimport { Editor, EditorContainer } from './editor';\n\nexport type TComment = {\n  id: string;\n  contentRich: Value;\n  createdAt: Date;\n  discussionId: string;\n  isEdited: boolean;\n  userId: string;\n};\n\nexport function Comment(props: {\n  comment: TComment;\n  discussionLength: number;\n  editingId: string | null;\n  index: number;\n  setEditingId: React.Dispatch<React.SetStateAction<string | null>>;\n  documentContent?: string;\n  showDocumentContent?: boolean;\n  onEditorClick?: () => void;\n}) {\n  const {\n    comment,\n    discussionLength,\n    documentContent,\n    editingId,\n    index,\n    setEditingId,\n    showDocumentContent = false,\n    onEditorClick,\n  } = props;\n\n  const editor = useEditorRef();\n  const userInfo = usePluginOption(discussionPlugin, 'user', comment.userId);\n  const currentUserId = usePluginOption(discussionPlugin, 'currentUserId');\n\n  const resolveDiscussion = async (id: string) => {\n    const updatedDiscussions = editor\n      .getOption(discussionPlugin, 'discussions')\n      .map((discussion) => {\n        if (discussion.id === id) {\n          return { ...discussion, isResolved: true };\n        }\n        return discussion;\n      });\n    editor.setOption(discussionPlugin, 'discussions', updatedDiscussions);\n  };\n\n  const removeDiscussion = async (id: string) => {\n    const updatedDiscussions = editor\n      .getOption(discussionPlugin, 'discussions')\n      .filter((discussion) => discussion.id !== id);\n    editor.setOption(discussionPlugin, 'discussions', updatedDiscussions);\n  };\n\n  const updateComment = async (input: {\n    id: string;\n    contentRich: Value;\n    discussionId: string;\n    isEdited: boolean;\n  }) => {\n    const updatedDiscussions = editor\n      .getOption(discussionPlugin, 'discussions')\n      .map((discussion) => {\n        if (discussion.id === input.discussionId) {\n          const updatedComments = discussion.comments.map((comment) => {\n            if (comment.id === input.id) {\n              return {\n                ...comment,\n                contentRich: input.contentRich,\n                isEdited: true,\n                updatedAt: new Date(),\n              };\n            }\n            return comment;\n          });\n          return { ...discussion, comments: updatedComments };\n        }\n        return discussion;\n      });\n    editor.setOption(discussionPlugin, 'discussions', updatedDiscussions);\n  };\n\n  const { tf } = useEditorPlugin(CommentPlugin);\n\n  // Replace to your own backend or refer to potion\n  const isMyComment = currentUserId === comment.userId;\n\n  const initialValue = comment.contentRich;\n\n  const commentEditor = useCommentEditor(\n    {\n      id: comment.id,\n      value: initialValue,\n    },\n    [initialValue]\n  );\n\n  const onCancel = () => {\n    setEditingId(null);\n    commentEditor.tf.replaceNodes(initialValue, {\n      at: [],\n      children: true,\n    });\n  };\n\n  const onSave = () => {\n    void updateComment({\n      id: comment.id,\n      contentRich: commentEditor.children,\n      discussionId: comment.discussionId,\n      isEdited: true,\n    });\n    setEditingId(null);\n  };\n\n  const onResolveComment = () => {\n    void resolveDiscussion(comment.discussionId);\n    tf.comment.unsetMark({ id: comment.discussionId });\n  };\n\n  const isFirst = index === 0;\n  const isLast = index === discussionLength - 1;\n  const isEditing = editingId && editingId === comment.id;\n\n  const [hovering, setHovering] = React.useState(false);\n  const [dropdownOpen, setDropdownOpen] = React.useState(false);\n\n  return (\n    <div\n      onMouseEnter={() => setHovering(true)}\n      onMouseLeave={() => setHovering(false)}\n    >\n      <div className=\"relative flex items-center\">\n        <Avatar className=\"size-5\">\n          <AvatarImage alt={userInfo?.name} src={userInfo?.avatarUrl} />\n          <AvatarFallback>{userInfo?.name?.[0]}</AvatarFallback>\n        </Avatar>\n        <h4 className=\"mx-2 font-semibold text-sm leading-none\">\n          {/* Replace to your own backend or refer to potion */}\n          {userInfo?.name}\n        </h4>\n\n        <div className=\"text-muted-foreground/80 text-xs leading-none\">\n          <span className=\"mr-1\">\n            {formatCommentDate(new Date(comment.createdAt))}\n          </span>\n          {comment.isEdited && <span>(edited)</span>}\n        </div>\n\n        {isMyComment && (hovering || dropdownOpen) && (\n          <div className=\"absolute top-0 right-0 flex space-x-1\">\n            {index === 0 && (\n              <Button\n                variant=\"ghost\"\n                className=\"h-6 p-1 text-muted-foreground\"\n                onClick={onResolveComment}\n                type=\"button\"\n              >\n                <CheckIcon className=\"size-4\" />\n              </Button>\n            )}\n\n            <CommentMoreDropdown\n              onCloseAutoFocus={() => {\n                setTimeout(() => {\n                  commentEditor.tf.focus({ edge: 'endEditor' });\n                }, 0);\n              }}\n              onRemoveComment={() => {\n                if (discussionLength === 1) {\n                  tf.comment.unsetMark({ id: comment.discussionId });\n                  void removeDiscussion(comment.discussionId);\n                }\n              }}\n              comment={comment}\n              dropdownOpen={dropdownOpen}\n              setDropdownOpen={setDropdownOpen}\n              setEditingId={setEditingId}\n            />\n          </div>\n        )}\n      </div>\n\n      {isFirst && showDocumentContent && (\n        <div className=\"relative mt-1 flex pl-[32px] text-sm text-subtle-foreground\">\n          {discussionLength > 1 && (\n            <div className=\"absolute top-[5px] left-3 h-full w-0.5 shrink-0 bg-muted\" />\n          )}\n          <div className=\"my-px w-0.5 shrink-0 bg-highlight\" />\n          {documentContent && <div className=\"ml-2\">{documentContent}</div>}\n        </div>\n      )}\n\n      <div className=\"relative my-1 pl-[26px]\">\n        {!isLast && (\n          <div className=\"absolute top-0 left-3 h-full w-0.5 shrink-0 bg-muted\" />\n        )}\n        <Plate readOnly={!isEditing} editor={commentEditor}>\n          <EditorContainer variant=\"comment\">\n            <Editor\n              variant=\"comment\"\n              className=\"w-auto grow\"\n              onClick={() => onEditorClick?.()}\n            />\n\n            {isEditing && (\n              <div className=\"ml-auto flex shrink-0 gap-1\">\n                <Button\n                  size=\"icon\"\n                  variant=\"ghost\"\n                  className=\"size-[28px]\"\n                  onClick={(e: React.MouseEvent<HTMLButtonElement>) => {\n                    e.stopPropagation();\n                    void onCancel();\n                  }}\n                >\n                  <div className=\"flex size-5 shrink-0 items-center justify-center rounded-[50%] bg-primary/40\">\n                    <XIcon className=\"size-3 stroke-[3px] text-background\" />\n                  </div>\n                </Button>\n\n                <Button\n                  size=\"icon\"\n                  variant=\"ghost\"\n                  onClick={(e: React.MouseEvent<HTMLButtonElement>) => {\n                    e.stopPropagation();\n                    void onSave();\n                  }}\n                >\n                  <div className=\"flex size-5 shrink-0 items-center justify-center rounded-[50%] bg-brand\">\n                    <CheckIcon className=\"size-3 stroke-[3px] text-background\" />\n                  </div>\n                </Button>\n              </div>\n            )}\n          </EditorContainer>\n        </Plate>\n      </div>\n    </div>\n  );\n}\n\nfunction CommentMoreDropdown(props: {\n  comment: TComment;\n  dropdownOpen: boolean;\n  setDropdownOpen: React.Dispatch<React.SetStateAction<boolean>>;\n  setEditingId: React.Dispatch<React.SetStateAction<string | null>>;\n  onCloseAutoFocus?: () => void;\n  onRemoveComment?: () => void;\n}) {\n  const {\n    comment,\n    dropdownOpen,\n    setDropdownOpen,\n    setEditingId,\n    onCloseAutoFocus,\n    onRemoveComment,\n  } = props;\n\n  const editor = useEditorRef();\n\n  const selectedEditCommentRef = React.useRef<boolean>(false);\n\n  const onDeleteComment = React.useCallback(() => {\n    if (!comment.id)\n      return alert('You are operating too quickly, please try again later.');\n\n    // Find and update the discussion\n    const updatedDiscussions = editor\n      .getOption(discussionPlugin, 'discussions')\n      .map((discussion) => {\n        if (discussion.id !== comment.discussionId) {\n          return discussion;\n        }\n\n        const commentIndex = discussion.comments.findIndex(\n          (c) => c.id === comment.id\n        );\n        if (commentIndex === -1) {\n          return discussion;\n        }\n\n        return {\n          ...discussion,\n          comments: [\n            ...discussion.comments.slice(0, commentIndex),\n            ...discussion.comments.slice(commentIndex + 1),\n          ],\n        };\n      });\n\n    // Save back to session storage\n    editor.setOption(discussionPlugin, 'discussions', updatedDiscussions);\n    onRemoveComment?.();\n  }, [comment.discussionId, comment.id, editor, onRemoveComment]);\n\n  const onEditComment = React.useCallback(() => {\n    selectedEditCommentRef.current = true;\n\n    if (!comment.id)\n      return alert('You are operating too quickly, please try again later.');\n\n    setEditingId(comment.id);\n  }, [comment.id, setEditingId]);\n\n  return (\n    <DropdownMenu\n      open={dropdownOpen}\n      onOpenChange={setDropdownOpen}\n      modal={false}\n    >\n      <DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>\n        <Button variant=\"ghost\" className={cn('h-6 p-1 text-muted-foreground')}>\n          <MoreHorizontalIcon className=\"size-4\" />\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent\n        className=\"w-48\"\n        onCloseAutoFocus={(e) => {\n          if (selectedEditCommentRef.current) {\n            onCloseAutoFocus?.();\n            selectedEditCommentRef.current = false;\n          }\n\n          return e.preventDefault();\n        }}\n      >\n        <DropdownMenuGroup>\n          <DropdownMenuItem onClick={onEditComment}>\n            <PencilIcon className=\"size-4\" />\n            Edit comment\n          </DropdownMenuItem>\n          <DropdownMenuItem onClick={onDeleteComment}>\n            <TrashIcon className=\"size-4\" />\n            Delete comment\n          </DropdownMenuItem>\n        </DropdownMenuGroup>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nconst useCommentEditor = (\n  options: Omit<CreatePlateEditorOptions, 'plugins'> = {},\n  deps: any[] = []\n) => {\n  const commentEditor = usePlateEditor(\n    {\n      id: 'comment',\n      plugins: BasicMarksKit,\n      value: [],\n      ...options,\n    },\n    deps\n  );\n\n  return commentEditor;\n};\n\nexport function CommentCreateForm({\n  autoFocus = false,\n  className,\n  discussionId: discussionIdProp,\n  focusOnMount = false,\n}: {\n  autoFocus?: boolean;\n  className?: string;\n  discussionId?: string;\n  focusOnMount?: boolean;\n}) {\n  const discussions = usePluginOption(discussionPlugin, 'discussions');\n\n  const editor = useEditorRef();\n  const commentId = useCommentId();\n  const discussionId = discussionIdProp ?? commentId;\n\n  const userInfo = usePluginOption(discussionPlugin, 'currentUser');\n  const [commentValue, setCommentValue] = React.useState<Value | undefined>();\n  const commentContent = React.useMemo(\n    () =>\n      commentValue\n        ? NodeApi.string({ children: commentValue, type: KEYS.p })\n        : '',\n    [commentValue]\n  );\n  const commentEditor = useCommentEditor();\n\n  React.useEffect(() => {\n    if (commentEditor && focusOnMount) {\n      commentEditor.tf.focus();\n    }\n  }, [commentEditor, focusOnMount]);\n\n  const onAddComment = React.useCallback(async () => {\n    if (!commentValue) return;\n\n    commentEditor.tf.reset();\n\n    if (discussionId) {\n      // Get existing discussion\n      const discussion = discussions.find((d) => d.id === discussionId);\n      if (!discussion) {\n        // Mock creating suggestion\n        const newDiscussion: TDiscussion = {\n          id: discussionId,\n          comments: [\n            {\n              id: nanoid(),\n              contentRich: commentValue,\n              createdAt: new Date(),\n              discussionId,\n              isEdited: false,\n              userId: editor.getOption(discussionPlugin, 'currentUserId'),\n            },\n          ],\n          createdAt: new Date(),\n          isResolved: false,\n          userId: editor.getOption(discussionPlugin, 'currentUserId'),\n        };\n\n        editor.setOption(discussionPlugin, 'discussions', [\n          ...discussions,\n          newDiscussion,\n        ]);\n        return;\n      }\n\n      // Create reply comment\n      const comment: TComment = {\n        id: nanoid(),\n        contentRich: commentValue,\n        createdAt: new Date(),\n        discussionId,\n        isEdited: false,\n        userId: editor.getOption(discussionPlugin, 'currentUserId'),\n      };\n\n      // Add reply to discussion comments\n      const updatedDiscussion = {\n        ...discussion,\n        comments: [...discussion.comments, comment],\n      };\n\n      // Filter out old discussion and add updated one\n      const updatedDiscussions = discussions\n        .filter((d) => d.id !== discussionId)\n        .concat(updatedDiscussion);\n\n      editor.setOption(discussionPlugin, 'discussions', updatedDiscussions);\n\n      return;\n    }\n\n    const commentsNodeEntry = editor\n      .getApi(CommentPlugin)\n      .comment.nodes({ at: [], isDraft: true });\n\n    if (commentsNodeEntry.length === 0) return;\n\n    const documentContent = commentsNodeEntry\n      .map(([node, _path]: NodeEntry<TCommentText>) => node.text)\n      .join('');\n\n    const _discussionId = nanoid();\n    // Mock creating new discussion\n    const newDiscussion: TDiscussion = {\n      id: _discussionId,\n      comments: [\n        {\n          id: nanoid(),\n          contentRich: commentValue,\n          createdAt: new Date(),\n          discussionId: _discussionId,\n          isEdited: false,\n          userId: editor.getOption(discussionPlugin, 'currentUserId'),\n        },\n      ],\n      createdAt: new Date(),\n      documentContent,\n      isResolved: false,\n      userId: editor.getOption(discussionPlugin, 'currentUserId'),\n    };\n\n    editor.setOption(discussionPlugin, 'discussions', [\n      ...discussions,\n      newDiscussion,\n    ]);\n\n    const id = newDiscussion.id;\n\n    commentsNodeEntry.forEach(([, path]: NodeEntry<TCommentText>) => {\n      editor.tf.setNodes(\n        {\n          [getCommentKey(id)]: true,\n        },\n        { at: path, split: true }\n      );\n      editor.tf.unsetNodes([getDraftCommentKey()], { at: path });\n    });\n  }, [commentValue, commentEditor.tf, discussionId, editor, discussions]);\n\n  return (\n    <div className={cn('flex w-full', className)}>\n      <div className=\"mt-2 mr-1 shrink-0\">\n        {/* Replace to your own backend or refer to potion */}\n        <Avatar className=\"size-5\">\n          <AvatarImage alt={userInfo?.name} src={userInfo?.avatarUrl} />\n          <AvatarFallback>{userInfo?.name?.[0]}</AvatarFallback>\n        </Avatar>\n      </div>\n\n      <div className=\"relative flex grow gap-2\">\n        <Plate\n          onChange={({ value }) => {\n            setCommentValue(value);\n          }}\n          editor={commentEditor}\n        >\n          <EditorContainer variant=\"comment\">\n            <Editor\n              variant=\"comment\"\n              className=\"min-h-[25px] grow pt-0.5 pr-8\"\n              onKeyDown={(e) => {\n                if (e.key === 'Enter' && !e.shiftKey) {\n                  e.preventDefault();\n                  onAddComment();\n                }\n              }}\n              placeholder=\"Reply...\"\n              autoComplete=\"off\"\n              autoFocus={autoFocus}\n            />\n\n            <Button\n              size=\"icon\"\n              variant=\"ghost\"\n              className=\"absolute right-0.5 bottom-0.5 ml-auto size-6 shrink-0\"\n              disabled={commentContent.trim().length === 0}\n              onClick={(e) => {\n                e.stopPropagation();\n                onAddComment();\n              }}\n            >\n              <div className=\"flex size-6 items-center justify-center rounded-full\">\n                <ArrowUpIcon />\n              </div>\n            </Button>\n          </EditorContainer>\n        </Plate>\n      </div>\n    </div>\n  );\n}\n\nexport const formatCommentDate = (date: Date) => {\n  const now = new Date();\n  const diffMinutes = differenceInMinutes(now, date);\n  const diffHours = differenceInHours(now, date);\n  const diffDays = differenceInDays(now, date);\n\n  if (diffMinutes < 60) {\n    return `${diffMinutes}m`;\n  }\n  if (diffHours < 24) {\n    return `${diffHours}h`;\n  }\n  if (diffDays < 2) {\n    return `${diffDays}d`;\n  }\n\n  return format(date, 'MM/dd/yyyy');\n};\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "docs": [
      {
        "route": "/docs/comment"
      },
      {
        "route": "https://pro.platejs.org/docs/components/block-discussion"
      }
    ],
    "examples": [
      "discussion-demo",
      "discussion-pro"
    ]
  },
  "type": "registry:ui"
}