{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "rich-text-editor",
  "title": "Rich Text Editor",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/rich-text-editor/RichTextEditor.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, watch, onBeforeUnmount, ref } from 'vue'\nimport { useEditor, EditorContent } from '@tiptap/vue-3'\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-vue-next'\nimport { Toggle } from '@/components/ui/toggle'\nimport { Separator } from '@/components/ui/separator'\nimport { cn } from '@/lib/utils'\n\nconst props = withDefaults(\n  defineProps<{\n    modelValue?: string\n    placeholder?: string\n    class?: string\n    editorClass?: string\n    minHeight?: string\n  }>(),\n  {\n    modelValue: '',\n    placeholder: 'Start writing...',\n    minHeight: '120px',\n  },\n)\n\nconst emit = defineEmits<{\n  'update:modelValue': [value: string]\n}>()\n\nconst editor = useEditor({\n  content: props.modelValue,\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: props.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    emit('update:modelValue', e.getHTML())\n  },\n})\n\nwatch(\n  () => props.modelValue,\n  (val) => {\n    if (editor.value && editor.value.getHTML() !== val) {\n      editor.value.commands.setContent(val || '', { emitUpdate: false })\n    }\n  },\n)\n\nonBeforeUnmount(() => {\n  editor.value?.destroy()\n})\n\nfunction toggleLink() {\n  if (!editor.value) return\n  if (editor.value.isActive('link')) {\n    editor.value.chain().focus().unsetLink().run()\n  } else {\n    const url = window.prompt('Enter URL')\n    if (url) {\n      editor.value.chain().focus().extendMarkRange('link').setLink({ href: url }).run()\n    }\n  }\n}\n\ninterface ToolbarItem {\n  type: 'button' | 'separator'\n  icon?: any\n  action?: () => void\n  isActive?: () => boolean\n  title?: string\n}\n\nconst showExtended = ref(false)\n\nconst essentialItems = computed<ToolbarItem[]>(() => {\n  if (!editor.value) return []\n  const e = editor.value\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})\n\nconst extendedItems = computed<ToolbarItem[]>(() => {\n  if (!editor.value) return []\n  const e = editor.value\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})\n</script>\n\n<template>\n  <div :class=\"cn('rich-text-editor rounded-lg border', props.class)\">\n    <!-- Toolbar -->\n    <div v-if=\"editor\" class=\"border-b\">\n      <!-- Essential row -->\n      <div class=\"flex items-center gap-0.5 px-2 py-1.5\">\n        <template v-for=\"(item, i) in essentialItems\" :key=\"'e' + i\">\n          <Separator v-if=\"item.type === 'separator'\" orientation=\"vertical\" class=\"mx-1 h-5\" />\n          <Toggle\n            v-else\n            size=\"sm\"\n            :pressed=\"item.isActive?.()\"\n            :title=\"item.title\"\n            class=\"focus-visible:ring-ring size-7 p-0 focus-visible:ring-2 focus-visible:outline-none\"\n            @click=\"item.action?.()\"\n          >\n            <component :is=\"item.icon\" class=\"size-3.5\" />\n          </Toggle>\n        </template>\n\n        <Separator orientation=\"vertical\" class=\"mx-1 h-5\" />\n\n        <!-- Expand toggle -->\n        <button\n          type=\"button\"\n          :title=\"showExtended ? 'Hide more options' : 'Show more options'\"\n          :class=\"[\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          @click=\"showExtended = !showExtended\"\n        >\n          <ChevronDown :class=\"['size-3.5 transition-transform duration-200', showExtended && 'rotate-180']\" />\n        </button>\n      </div>\n\n      <!-- Extended row (collapsible) -->\n      <div\n        :class=\"[\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 class=\"overflow-hidden\">\n          <div class=\"flex items-center gap-0.5 border-t px-2 py-1.5\">\n            <template v-for=\"(item, i) in extendedItems\" :key=\"'x' + i\">\n              <Separator v-if=\"item.type === 'separator'\" orientation=\"vertical\" class=\"mx-1 h-5\" />\n              <Toggle\n                v-else\n                size=\"sm\"\n                :pressed=\"item.isActive?.()\"\n                :title=\"item.title\"\n                class=\"focus-visible:ring-ring size-7 p-0 focus-visible:ring-2 focus-visible:outline-none\"\n                @click=\"item.action?.()\"\n              >\n                <component :is=\"item.icon\" class=\"size-3.5\" />\n              </Toggle>\n            </template>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- Editor -->\n    <EditorContent\n      :editor=\"editor\"\n      :class=\"cn('rich-text-content overflow-y-auto px-3 py-2', props.editorClass)\"\n      :style=\"{ minHeight: props.minHeight }\"\n    />\n  </div>\n</template>\n\n<style>\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</style>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/rich-text-editor/RichTextEditor.vue"
    },
    {
      "path": "packages/registry-vue/components/rich-text-editor/index.ts",
      "content": "export { default as RichTextEditor } from './RichTextEditor.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/rich-text-editor/index.ts"
    }
  ],
  "dependencies": [
    "@tiptap/extension-link",
    "@tiptap/extension-placeholder",
    "@tiptap/extension-task-item",
    "@tiptap/extension-task-list",
    "@tiptap/extension-text-align",
    "@tiptap/extension-underline",
    "@tiptap/starter-kit",
    "@tiptap/vue-3",
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/separator.json",
    "https://uipkge.dev/r/vue/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"
  ]
}