{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "rich-text-editor",
  "title": "Rich Text Editor",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/rich-text-editor/rich-text-editor.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { useEditor, EditorContent } from '@tiptap/react'\nimport StarterKit from '@tiptap/starter-kit'\nimport Placeholder from '@tiptap/extension-placeholder'\nimport Underline from '@tiptap/extension-underline'\nimport Link from '@tiptap/extension-link'\nimport TextAlign from '@tiptap/extension-text-align'\nimport TaskList from '@tiptap/extension-task-list'\nimport TaskItem from '@tiptap/extension-task-item'\nimport {\n  Bold,\n  Italic,\n  Underline as UnderlineIcon,\n  Strikethrough,\n  List,\n  ListOrdered,\n  ListChecks,\n  AlignLeft,\n  AlignCenter,\n  AlignRight,\n  Link as LinkIcon,\n  Heading1,\n  Heading2,\n  Quote,\n  Minus,\n  Undo2,\n  Redo2,\n  Code,\n  RemoveFormatting,\n  ChevronDown,\n} from 'lucide-react'\nimport { Toggle } from '@/components/ui/toggle'\nimport { Separator } from '@/components/ui/separator'\nimport { cn } from '@/lib/utils'\n\n// Ported from RichTextEditor.vue's <style> block. Injected once so the\n// component ships self-contained (placeholder, prose overrides, task-list,\n// blockquote/code/hr/link styling). Selectors match the Vue source 1:1.\nconst richTextEditorCss = `\n/* Editor placeholder */\n.rich-text-editor .tiptap p.is-editor-empty:first-child::before {\n  content: attr(data-placeholder);\n  float: left;\n  color: var(--muted-foreground);\n  opacity: 0.5;\n  pointer-events: none;\n  height: 0;\n}\n\n/* Prose overrides for compact styling */\n.rich-text-content .tiptap {\n  min-height: inherit;\n}\n\n.rich-text-content .tiptap > *:first-child {\n  margin-top: 0;\n}\n\n.rich-text-content .tiptap > *:last-child {\n  margin-bottom: 0;\n}\n\n.rich-text-content .tiptap h1 {\n  font-size: 1.25rem;\n  font-weight: 700;\n  line-height: 1.3;\n  margin-top: 1rem;\n  margin-bottom: 0.5rem;\n}\n\n.rich-text-content .tiptap h2 {\n  font-size: 1.1rem;\n  font-weight: 600;\n  line-height: 1.3;\n  margin-top: 0.75rem;\n  margin-bottom: 0.375rem;\n}\n\n.rich-text-content .tiptap p {\n  font-size: 0.875rem;\n  line-height: 1.6;\n  margin-top: 0.25rem;\n  margin-bottom: 0.25rem;\n}\n\n.rich-text-content .tiptap ul,\n.rich-text-content .tiptap ol {\n  padding-left: 1.25rem;\n  margin-top: 0.25rem;\n  margin-bottom: 0.25rem;\n}\n\n.rich-text-content .tiptap li {\n  font-size: 0.875rem;\n  margin-top: 0.125rem;\n  margin-bottom: 0.125rem;\n}\n\n.rich-text-content .tiptap blockquote {\n  border-left: 3px solid var(--border);\n  padding-left: 0.75rem;\n  margin-top: 0.5rem;\n  margin-bottom: 0.5rem;\n  color: var(--muted-foreground);\n  font-style: italic;\n}\n\n.rich-text-content .tiptap code {\n  background: var(--muted);\n  border-radius: 0.25rem;\n  padding: 0.125rem 0.25rem;\n  font-size: 0.8rem;\n  font-family: ui-monospace, monospace;\n}\n\n.rich-text-content .tiptap pre {\n  background: var(--muted);\n  border-radius: 0.5rem;\n  padding: 0.75rem 1rem;\n  margin-top: 0.5rem;\n  margin-bottom: 0.5rem;\n}\n\n.rich-text-content .tiptap pre code {\n  background: none;\n  padding: 0;\n  font-size: 0.8rem;\n}\n\n.rich-text-content .tiptap hr {\n  border-color: var(--border);\n  margin-top: 0.75rem;\n  margin-bottom: 0.75rem;\n}\n\n/* Task list styling */\n.rich-text-content .tiptap ul[data-type='taskList'] {\n  list-style: none;\n  padding-left: 0;\n}\n\n.rich-text-content .tiptap ul[data-type='taskList'] li {\n  display: flex;\n  align-items: flex-start;\n  gap: 0.5rem;\n}\n\n.rich-text-content .tiptap ul[data-type='taskList'] li > label {\n  flex-shrink: 0;\n  margin-top: 0.125rem;\n}\n\n.rich-text-content .tiptap ul[data-type='taskList'] li > label input[type='checkbox'] {\n  accent-color: var(--primary);\n  width: 0.875rem;\n  height: 0.875rem;\n  cursor: pointer;\n}\n\n.rich-text-content .tiptap ul[data-type='taskList'] li > div {\n  flex: 1;\n}\n\n/* Link styling */\n.rich-text-content .tiptap a {\n  color: var(--primary);\n  text-decoration: underline;\n  cursor: pointer;\n}\n`\n\nexport interface RichTextEditorProps {\n  value?: string\n  onValueChange?: (value: string) => void\n  placeholder?: string\n  className?: string\n  editorClassName?: string\n  minHeight?: string\n}\n\ninterface ToolbarItem {\n  type: 'button' | 'separator'\n  icon?: React.ComponentType<{ className?: string }>\n  action?: () => void\n  isActive?: () => boolean\n  title?: string\n}\n\nconst RichTextEditor = React.forwardRef<HTMLDivElement, RichTextEditorProps>(\n  (\n    {\n      value = '',\n      onValueChange,\n      placeholder = 'Start writing...',\n      className,\n      editorClassName,\n      minHeight = '120px',\n    },\n    ref,\n  ) => {\n    const [showExtended, setShowExtended] = React.useState(false)\n\n    const editor = useEditor({\n      content: value,\n      immediatelyRender: false,\n      extensions: [\n        StarterKit.configure({\n          heading: { levels: [1, 2, 3] },\n          // StarterKit ships its own link + underline since v3 — disable them so\n          // we can register the standalone packages with our own configuration\n          // without TipTap warning about duplicate extension names.\n          link: false,\n          underline: false,\n        }),\n        Placeholder.configure({ placeholder }),\n        Underline,\n        Link.configure({\n          openOnClick: false,\n          HTMLAttributes: { class: 'text-primary underline cursor-pointer' },\n        }),\n        TextAlign.configure({ types: ['heading', 'paragraph'] }),\n        TaskList,\n        TaskItem.configure({ nested: true }),\n      ],\n      editorProps: {\n        attributes: {\n          class: 'prose prose-sm dark:prose-invert max-w-none focus:outline-none',\n        },\n      },\n      onUpdate: ({ editor: e }) => {\n        onValueChange?.(e.getHTML())\n      },\n    })\n\n    React.useEffect(() => {\n      if (editor && editor.getHTML() !== value) {\n        editor.commands.setContent(value || '', { emitUpdate: false })\n      }\n    }, [editor, value])\n\n    const toggleLink = React.useCallback(() => {\n      if (!editor) return\n      if (editor.isActive('link')) {\n        editor.chain().focus().unsetLink().run()\n      } else {\n        const url = window.prompt('Enter URL')\n        if (url) {\n          editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run()\n        }\n      }\n    }, [editor])\n\n    const essentialItems = React.useMemo<ToolbarItem[]>(() => {\n      if (!editor) return []\n      const e = editor\n      return [\n        {\n          type: 'button',\n          icon: Bold,\n          action: () => e.chain().focus().toggleBold().run(),\n          isActive: () => e.isActive('bold'),\n          title: 'Bold',\n        },\n        {\n          type: 'button',\n          icon: Italic,\n          action: () => e.chain().focus().toggleItalic().run(),\n          isActive: () => e.isActive('italic'),\n          title: 'Italic',\n        },\n        {\n          type: 'button',\n          icon: UnderlineIcon,\n          action: () => e.chain().focus().toggleUnderline().run(),\n          isActive: () => e.isActive('underline'),\n          title: 'Underline',\n        },\n        {\n          type: 'button',\n          icon: Strikethrough,\n          action: () => e.chain().focus().toggleStrike().run(),\n          isActive: () => e.isActive('strike'),\n          title: 'Strikethrough',\n        },\n        { type: 'separator' },\n        {\n          type: 'button',\n          icon: List,\n          action: () => e.chain().focus().toggleBulletList().run(),\n          isActive: () => e.isActive('bulletList'),\n          title: 'Bullet list',\n        },\n        {\n          type: 'button',\n          icon: ListOrdered,\n          action: () => e.chain().focus().toggleOrderedList().run(),\n          isActive: () => e.isActive('orderedList'),\n          title: 'Numbered list',\n        },\n        { type: 'separator' },\n        { type: 'button', icon: LinkIcon, action: toggleLink, isActive: () => e.isActive('link'), title: 'Link' },\n        { type: 'separator' },\n        { type: 'button', icon: Undo2, action: () => e.chain().focus().undo().run(), isActive: () => false, title: 'Undo' },\n        { type: 'button', icon: Redo2, action: () => e.chain().focus().redo().run(), isActive: () => false, title: 'Redo' },\n      ]\n    }, [editor, toggleLink])\n\n    const extendedItems = React.useMemo<ToolbarItem[]>(() => {\n      if (!editor) return []\n      const e = editor\n      return [\n        {\n          type: 'button',\n          icon: Heading1,\n          action: () => e.chain().focus().toggleHeading({ level: 1 }).run(),\n          isActive: () => e.isActive('heading', { level: 1 }),\n          title: 'Heading 1',\n        },\n        {\n          type: 'button',\n          icon: Heading2,\n          action: () => e.chain().focus().toggleHeading({ level: 2 }).run(),\n          isActive: () => e.isActive('heading', { level: 2 }),\n          title: 'Heading 2',\n        },\n        { type: 'separator' },\n        {\n          type: 'button',\n          icon: Code,\n          action: () => e.chain().focus().toggleCode().run(),\n          isActive: () => e.isActive('code'),\n          title: 'Inline code',\n        },\n        {\n          type: 'button',\n          icon: Quote,\n          action: () => e.chain().focus().toggleBlockquote().run(),\n          isActive: () => e.isActive('blockquote'),\n          title: 'Blockquote',\n        },\n        {\n          type: 'button',\n          icon: Minus,\n          action: () => e.chain().focus().setHorizontalRule().run(),\n          isActive: () => false,\n          title: 'Divider',\n        },\n        {\n          type: 'button',\n          icon: ListChecks,\n          action: () => e.chain().focus().toggleTaskList().run(),\n          isActive: () => e.isActive('taskList'),\n          title: 'Task list',\n        },\n        { type: 'separator' },\n        {\n          type: 'button',\n          icon: AlignLeft,\n          action: () => e.chain().focus().setTextAlign('left').run(),\n          isActive: () => e.isActive({ textAlign: 'left' }),\n          title: 'Align left',\n        },\n        {\n          type: 'button',\n          icon: AlignCenter,\n          action: () => e.chain().focus().setTextAlign('center').run(),\n          isActive: () => e.isActive({ textAlign: 'center' }),\n          title: 'Align center',\n        },\n        {\n          type: 'button',\n          icon: AlignRight,\n          action: () => e.chain().focus().setTextAlign('right').run(),\n          isActive: () => e.isActive({ textAlign: 'right' }),\n          title: 'Align right',\n        },\n        { type: 'separator' },\n        {\n          type: 'button',\n          icon: RemoveFormatting,\n          action: () => e.chain().focus().clearNodes().unsetAllMarks().run(),\n          isActive: () => false,\n          title: 'Clear formatting',\n        },\n      ]\n    }, [editor])\n\n    React.useEffect(() => {\n      return () => {\n        editor?.destroy()\n      }\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [])\n\n    return (\n      <div ref={ref} data-uipkge=\"\" data-slot=\"rich-text-editor\" className={cn('rich-text-editor rounded-lg border', className)}>\n        <style dangerouslySetInnerHTML={{ __html: richTextEditorCss }} />\n        {/* Toolbar */}\n        {editor && (\n          <div className=\"border-b\">\n            {/* Essential row */}\n            <div className=\"flex items-center gap-0.5 px-2 py-1.5\">\n              {essentialItems.map((item, i) =>\n                item.type === 'separator' ? (\n                  <Separator key={'e' + i} orientation=\"vertical\" className=\"mx-1 h-5\" />\n                ) : (\n                  <Toggle\n                    key={'e' + i}\n                    size=\"sm\"\n                    pressed={item.isActive?.()}\n                    title={item.title}\n                    className=\"focus-visible:ring-ring size-7 p-0 focus-visible:ring-2 focus-visible:outline-none\"\n                    onClick={() => item.action?.()}\n                  >\n                    {item.icon && <item.icon className=\"size-3.5\" />}\n                  </Toggle>\n                ),\n              )}\n\n              <Separator orientation=\"vertical\" className=\"mx-1 h-5\" />\n\n              {/* Expand toggle */}\n              <button\n                type=\"button\"\n                title={showExtended ? 'Hide more options' : 'Show more options'}\n                className={cn(\n                  'text-muted-foreground hover:text-foreground hover:bg-muted focus-visible:ring-ring inline-flex size-7 items-center justify-center rounded-md transition-colors focus-visible:ring-2 focus-visible:outline-none',\n                  showExtended && 'bg-muted text-foreground',\n                )}\n                onClick={() => setShowExtended((v) => !v)}\n              >\n                <ChevronDown className={cn('size-3.5 transition-transform duration-200', showExtended && 'rotate-180')} />\n              </button>\n            </div>\n\n            {/* Extended row (collapsible) */}\n            <div\n              className={cn(\n                'grid transition-colors duration-200 ease-in-out',\n                showExtended ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0',\n              )}\n            >\n              <div className=\"overflow-hidden\">\n                <div className=\"flex items-center gap-0.5 border-t px-2 py-1.5\">\n                  {extendedItems.map((item, i) =>\n                    item.type === 'separator' ? (\n                      <Separator key={'x' + i} orientation=\"vertical\" className=\"mx-1 h-5\" />\n                    ) : (\n                      <Toggle\n                        key={'x' + i}\n                        size=\"sm\"\n                        pressed={item.isActive?.()}\n                        title={item.title}\n                        className=\"focus-visible:ring-ring size-7 p-0 focus-visible:ring-2 focus-visible:outline-none\"\n                        onClick={() => item.action?.()}\n                      >\n                        {item.icon && <item.icon className=\"size-3.5\" />}\n                      </Toggle>\n                    ),\n                  )}\n                </div>\n              </div>\n            </div>\n          </div>\n        )}\n\n        {/* Editor */}\n        <EditorContent\n          editor={editor}\n          className={cn('rich-text-content overflow-y-auto px-3 py-2', editorClassName)}\n          style={{ minHeight }}\n        />\n      </div>\n    )\n  },\n)\nRichTextEditor.displayName = 'RichTextEditor'\n\nexport { RichTextEditor }\n",
      "type": "registry:ui",
      "target": "~/components/ui/rich-text-editor/rich-text-editor.tsx"
    },
    {
      "path": "packages/registry-react/components/rich-text-editor/index.ts",
      "content": "export { RichTextEditor, type RichTextEditorProps } from './rich-text-editor'\n",
      "type": "registry:ui",
      "target": "~/components/ui/rich-text-editor/index.ts"
    }
  ],
  "dependencies": [
    "@tiptap/react",
    "@tiptap/starter-kit",
    "@tiptap/pm",
    "@tiptap/extension-link",
    "@tiptap/extension-placeholder",
    "@tiptap/extension-task-item",
    "@tiptap/extension-task-list",
    "@tiptap/extension-text-align",
    "@tiptap/extension-underline",
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/separator.json",
    "https://uipkge.dev/r/react/toggle.json"
  ],
  "description": "WYSIWYG editor wrapped around TipTap — bold/italic/links/lists/headings/blockquote/code, plus a configurable toolbar. Drop into forms where Markdown is too low-level.",
  "categories": [
    "form"
  ]
}