{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "content-block-editor",
  "title": "Content Block Editor",
  "author": "Ben Hofstetter <ben.hofstetter@gmail.com>",
  "description": "A drag-and-drop editor for managing a sequence of content blocks — markdown, media, embeds, and interactive components.",
  "type": "registry:component",
  "categories": [
    "snax"
  ],
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "https://benhof.ai/r/content-blocks.json",
    "https://benhof.ai/r/link-preview.json"
  ],
  "files": [
    {
      "path": "src/registry/components/content-block-editor/content-block-editor.tsx",
      "content": "\"use client\";\n\nimport { useMemo, useState, type ComponentProps, type ReactNode } from \"react\";\nimport { DragDropContext, Draggable, Droppable } from \"@hello-pangea/dnd\";\nimport {\n  ArrowDown,\n  ArrowUp,\n  AudioLines,\n  Code2,\n  Copy,\n  GripVertical,\n  Image,\n  Images,\n  HelpCircle,\n  Link2,\n  ListOrdered,\n  MessageSquareText,\n  Plus,\n  RefreshCw,\n  Trash2,\n  LayoutGrid,\n  Type,\n  Video,\n  Vote,\n} from \"lucide-react\";\n\nimport {\n  CONTENT_BLOCK_LABELS,\n  CONTENT_BLOCK_TYPES,\n  MarkdownBlock,\n  createContentBlock,\n  reorderContentBlocks,\n  resequenceContentBlocks,\n  validateContentBlocks,\n  type ContentBlockRecord,\n  type ContentBlockType,\n} from \"@/components/content-blocks\";\nimport type { LinkMetadata } from \"@/components/link-preview\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ContentBlockImageFieldProps = {\n  label: string;\n  value: string;\n  onChange: (value: string) => void;\n};\n\nexport type ContentBlockGalleryFieldProps = {\n  label: string;\n  images: string[];\n  onChange: (images: string[]) => void;\n};\n\nexport type ContentBlockInteractiveFieldProps = {\n  type: \"poll\" | \"quiz\" | \"list\";\n  label: string;\n  value: string;\n  onChange: (value: string, title?: string) => void;\n};\n\nexport type ContentBlockLinkMetadataFetcher = (\n  url: string,\n) => Promise<Partial<LinkMetadata>>;\n\nexport type ContentBlockEditorProps = Omit<\n  ComponentProps<\"div\">,\n  \"onChange\"\n> & {\n  blocks: ContentBlockRecord[];\n  onChange: (blocks: ContentBlockRecord[]) => void;\n  renderImageField?: (props: ContentBlockImageFieldProps) => ReactNode;\n  renderGalleryField?: (props: ContentBlockGalleryFieldProps) => ReactNode;\n  renderInteractiveField?: (\n    props: ContentBlockInteractiveFieldProps,\n  ) => ReactNode;\n  fetchLinkMetadata?: ContentBlockLinkMetadataFetcher;\n};\n\nconst BLOCK_ICONS = {\n  markdown: Type,\n  image: Image,\n  gallery: Images,\n  video: Video,\n  audio: AudioLines,\n  embed: Code2,\n  link: Link2,\n  callout: MessageSquareText,\n  poll: Vote,\n  quiz: HelpCircle,\n  list: ListOrdered,\n  component: LayoutGrid,\n} satisfies Record<ContentBlockType, typeof Type>;\n\nconst REGISTRY_COMPONENT_OPTIONS = [\n  { value: \"tournament-bracket\", label: \"Tournament Bracket\" },\n  { value: \"bracket-pickem\", label: \"Bracket Pick'em\" },\n  { value: \"pool-standings\", label: \"Pool Standings\" },\n  { value: \"event-countdown\", label: \"Event Countdown\" },\n  { value: \"field-schedule\", label: \"Field Schedule\" },\n  { value: \"roster-check-in\", label: \"Roster Check-In\" },\n  { value: \"run-of-show\", label: \"Run of Show\" },\n  { value: \"game-setup-panel\", label: \"Game Setup Panel\" },\n  { value: \"active-game-card\", label: \"Active Game Card\" },\n  { value: \"score-panel\", label: \"Score Panel\" },\n  { value: \"count-display\", label: \"Count Display\" },\n  { value: \"base-diamond\", label: \"Base Diamond\" },\n  { value: \"play-by-play\", label: \"Play-by-Play\" },\n  { value: \"scoreboard\", label: \"Scoreboard\" },\n  { value: \"scorekeeper-layout\", label: \"Scorekeeper Layout\" },\n  { value: \"final-score-summary\", label: \"Final Score Summary\" },\n  { value: \"blueprint-frame\", label: \"Blueprint Frame\" },\n  { value: \"now-section\", label: \"Now Section\" },\n  { value: \"wiffle-at-bat\", label: \"Wiffle At-Bat\" },\n];\n\nfunction stringValue(value: unknown) {\n  return typeof value === \"string\" ? value : \"\";\n}\n\nfunction stringArray(value: unknown) {\n  return Array.isArray(value)\n    ? value.filter((item): item is string => typeof item === \"string\")\n    : [];\n}\n\nfunction TextField({\n  label,\n  value,\n  onChange,\n  placeholder,\n  type = \"text\",\n}: {\n  label: string;\n  value?: string;\n  onChange: (value: string) => void;\n  placeholder?: string;\n  type?: \"text\" | \"url\";\n}) {\n  return (\n    <label className=\"block space-y-1.5\">\n      <span className=\"text-sm font-medium\">{label}</span>\n      <input\n        type={type}\n        value={value || \"\"}\n        onChange={(event) => onChange(event.target.value)}\n        placeholder={placeholder}\n        className=\"w-full rounded-md border bg-transparent px-3 py-2 text-sm outline-none transition-all focus:border-foreground\"\n      />\n    </label>\n  );\n}\n\nfunction TextAreaField({\n  label,\n  value,\n  onChange,\n  placeholder,\n  rows = 4,\n}: {\n  label: string;\n  value?: string;\n  onChange: (value: string) => void;\n  placeholder?: string;\n  rows?: number;\n}) {\n  return (\n    <label className=\"block space-y-1.5\">\n      <span className=\"text-sm font-medium\">{label}</span>\n      <textarea\n        value={value || \"\"}\n        onChange={(event) => onChange(event.target.value)}\n        placeholder={placeholder}\n        rows={rows}\n        className=\"w-full resize-y rounded-md border bg-transparent px-3 py-2 text-sm leading-relaxed outline-none transition-all focus:border-foreground\"\n      />\n    </label>\n  );\n}\n\nfunction DefaultImageField({\n  label,\n  value,\n  onChange,\n}: ContentBlockImageFieldProps) {\n  return (\n    <TextField\n      label={label}\n      value={value}\n      onChange={onChange}\n      placeholder=\"https://…\"\n      type=\"url\"\n    />\n  );\n}\n\nfunction DefaultGalleryField({\n  label,\n  images,\n  onChange,\n}: ContentBlockGalleryFieldProps) {\n  return (\n    <div className=\"space-y-3\">\n      <p className=\"text-sm font-medium\">{label}</p>\n      {images.map((url, index) => (\n        <div key={`${url}-${index}`} className=\"flex items-center gap-2\">\n          <input\n            type=\"url\"\n            value={url}\n            onChange={(event) => {\n              const next = [...images];\n              next[index] = event.target.value;\n              onChange(next);\n            }}\n            placeholder=\"https://…\"\n            className=\"min-w-0 flex-1 rounded-md border bg-transparent px-3 py-2 text-sm outline-none transition-all focus:border-foreground\"\n            aria-label={`Gallery image ${index + 1} URL`}\n          />\n          <button\n            type=\"button\"\n            onClick={() =>\n              onChange(images.filter((_, itemIndex) => itemIndex !== index))\n            }\n            className=\"flex size-9 shrink-0 items-center justify-center rounded-md border text-muted-foreground transition-all hover:text-foreground\"\n            aria-label={`Remove gallery image ${index + 1}`}\n          >\n            <Trash2 className=\"size-3.5\" />\n          </button>\n        </div>\n      ))}\n      <button\n        type=\"button\"\n        onClick={() => onChange([...images, \"\"])}\n        className=\"inline-flex items-center gap-2 rounded-md border px-3 py-2 text-xs font-medium transition-all hover:bg-muted\"\n      >\n        <Plus className=\"size-3.5\" /> Add image URL\n      </button>\n    </div>\n  );\n}\n\nfunction LinkBlockFields({\n  data,\n  onDataChange,\n  renderImageField,\n  fetchLinkMetadata,\n}: {\n  data: Record<string, unknown>;\n  onDataChange: (data: Record<string, unknown>) => void;\n  renderImageField?: ContentBlockEditorProps[\"renderImageField\"];\n  fetchLinkMetadata?: ContentBlockEditorProps[\"fetchLinkMetadata\"];\n}) {\n  const [loading, setLoading] = useState(false);\n  const [message, setMessage] = useState(\"\");\n  const ImageField = renderImageField || DefaultImageField;\n  const url = stringValue(data.url);\n  const update = (field: string, value: unknown) =>\n    onDataChange({ ...data, [field]: value });\n\n  const refreshMetadata = async () => {\n    if (!fetchLinkMetadata || !url.trim() || loading) return;\n    setLoading(true);\n    setMessage(\"\");\n    try {\n      const metadata = await fetchLinkMetadata(url.trim());\n      onDataChange({\n        ...data,\n        canonical_url: stringValue(metadata.canonical_url),\n        title: stringValue(metadata.title),\n        description: stringValue(metadata.description),\n        image: stringValue(metadata.image),\n        site_name: stringValue(metadata.site_name),\n        favicon: stringValue(metadata.favicon),\n      });\n      setMessage(\"Preview metadata refreshed. You can edit every field below.\");\n    } catch (error) {\n      setMessage(\n        error instanceof Error\n          ? error.message\n          : \"Link metadata could not be loaded.\",\n      );\n    } finally {\n      setLoading(false);\n    }\n  };\n\n  return (\n    <div className=\"space-y-4\">\n      <div className=\"grid gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-end\">\n        <TextField\n          label=\"Destination URL\"\n          value={url}\n          onChange={(value) => update(\"url\", value)}\n          placeholder=\"https://…\"\n          type=\"url\"\n        />\n        {fetchLinkMetadata ? (\n          <button\n            type=\"button\"\n            onClick={refreshMetadata}\n            disabled={!url.trim() || loading}\n            className=\"inline-flex h-10 items-center justify-center gap-2 rounded-md border px-3 text-xs font-medium transition-all hover:bg-muted disabled:opacity-40\"\n          >\n            <RefreshCw className={cn(\"size-3.5\", loading && \"animate-spin\")} />\n            {loading ? \"Fetching…\" : \"Fetch metadata\"}\n          </button>\n        ) : null}\n      </div>\n      {message ? (\n        <p className=\"font-mono text-[0.6875rem] leading-relaxed text-muted-foreground\">\n          {message}\n        </p>\n      ) : null}\n      <div className=\"grid gap-4 sm:grid-cols-2\">\n        <TextField\n          label=\"Title\"\n          value={stringValue(data.title)}\n          onChange={(value) => update(\"title\", value)}\n          placeholder=\"Link title\"\n        />\n        <TextField\n          label=\"Site name\"\n          value={stringValue(data.site_name)}\n          onChange={(value) => update(\"site_name\", value)}\n          placeholder=\"Publisher or website\"\n        />\n      </div>\n      <TextAreaField\n        label=\"Description\"\n        value={stringValue(data.description)}\n        onChange={(value) => update(\"description\", value)}\n        placeholder=\"Short link summary\"\n        rows={3}\n      />\n      <ImageField\n        value={stringValue(data.image)}\n        onChange={(value) => update(\"image\", value)}\n        label=\"Preview image (optional)\"\n      />\n      <div className=\"grid gap-4 sm:grid-cols-2\">\n        <TextField\n          label=\"Canonical URL\"\n          value={stringValue(data.canonical_url)}\n          onChange={(value) => update(\"canonical_url\", value)}\n          placeholder=\"Optional canonical URL\"\n          type=\"url\"\n        />\n        <TextField\n          label=\"Favicon URL\"\n          value={stringValue(data.favicon)}\n          onChange={(value) => update(\"favicon\", value)}\n          placeholder=\"Optional site icon\"\n          type=\"url\"\n        />\n      </div>\n    </div>\n  );\n}\n\nfunction BlockFields({\n  block,\n  onDataChange,\n  renderImageField,\n  renderGalleryField,\n  renderInteractiveField,\n  fetchLinkMetadata,\n}: {\n  block: ContentBlockRecord;\n  onDataChange: (data: Record<string, unknown>) => void;\n  renderImageField?: ContentBlockEditorProps[\"renderImageField\"];\n  renderGalleryField?: ContentBlockEditorProps[\"renderGalleryField\"];\n  renderInteractiveField?: ContentBlockEditorProps[\"renderInteractiveField\"];\n  fetchLinkMetadata?: ContentBlockEditorProps[\"fetchLinkMetadata\"];\n}) {\n  const data = block.data || {};\n  const update = (field: string, value: unknown) =>\n    onDataChange({ ...data, [field]: value });\n  const ImageField = renderImageField || DefaultImageField;\n  const GalleryField = renderGalleryField || DefaultGalleryField;\n\n  switch (block.block_type) {\n    case \"markdown\": {\n      const content = stringValue(data.content);\n      return (\n        <div className=\"grid gap-4 lg:grid-cols-2\">\n          <TextAreaField\n            label=\"Markdown\"\n            value={content}\n            onChange={(value) => update(\"content\", value)}\n            placeholder=\"Write in Markdown…\"\n            rows={12}\n          />\n          <div className=\"min-h-48 rounded-md border bg-muted/10 p-4\">\n            <p className=\"mb-2 font-mono text-[0.625rem] tracking-[0.18em] text-muted-foreground uppercase\">\n              Preview\n            </p>\n            <MarkdownBlock content={content} />\n          </div>\n        </div>\n      );\n    }\n    case \"image\":\n      return (\n        <div className=\"space-y-4\">\n          <ImageField\n            value={stringValue(data.url)}\n            onChange={(value) => update(\"url\", value)}\n            label=\"Block image\"\n          />\n          <div className=\"grid gap-4 sm:grid-cols-2\">\n            <TextField\n              label=\"Alt text\"\n              value={stringValue(data.alt)}\n              onChange={(value) => update(\"alt\", value)}\n              placeholder=\"Describe the image\"\n            />\n            <TextField\n              label=\"Caption\"\n              value={stringValue(data.caption)}\n              onChange={(value) => update(\"caption\", value)}\n              placeholder=\"Optional visible caption\"\n            />\n          </div>\n        </div>\n      );\n    case \"gallery\": {\n      const images = stringArray(data.images);\n      const alts = stringArray(data.alts);\n      return (\n        <div className=\"space-y-4\">\n          <GalleryField\n            images={images}\n            onChange={(value) => {\n              const altByUrl = new Map(\n                images.map((url, index) => [url, alts[index] || \"\"]),\n              );\n              onDataChange({\n                ...data,\n                images: value,\n                alts: value.map((url) => altByUrl.get(url) || \"\"),\n              });\n            }}\n            label=\"Block gallery\"\n          />\n          {images.map((url, index) => (\n            <TextField\n              key={`${url}-${index}`}\n              label={`Image ${index + 1} alt text`}\n              value={alts[index]}\n              onChange={(value) => {\n                const nextAlts = [...alts];\n                nextAlts[index] = value;\n                update(\"alts\", nextAlts);\n              }}\n              placeholder=\"Describe this gallery image\"\n            />\n          ))}\n          <TextField\n            label=\"Caption\"\n            value={stringValue(data.caption)}\n            onChange={(value) => update(\"caption\", value)}\n            placeholder=\"Optional gallery caption\"\n          />\n        </div>\n      );\n    }\n    case \"video\":\n      return (\n        <div className=\"space-y-4\">\n          <TextField\n            label=\"Video URL\"\n            value={stringValue(data.url)}\n            onChange={(value) => update(\"url\", value)}\n            placeholder=\"YouTube, Vimeo, or a direct video URL\"\n            type=\"url\"\n          />\n          <TextField\n            label=\"Caption\"\n            value={stringValue(data.caption)}\n            onChange={(value) => update(\"caption\", value)}\n            placeholder=\"Optional visible caption\"\n          />\n        </div>\n      );\n    case \"audio\":\n      return (\n        <div className=\"grid gap-4 sm:grid-cols-2\">\n          <TextField\n            label=\"Audio URL\"\n            value={stringValue(data.url)}\n            onChange={(value) => update(\"url\", value)}\n            placeholder=\"Direct MP3, WAV, or audio URL\"\n            type=\"url\"\n          />\n          <TextField\n            label=\"Title\"\n            value={stringValue(data.title)}\n            onChange={(value) => update(\"title\", value)}\n            placeholder=\"Audio title\"\n          />\n          <div className=\"sm:col-span-2\">\n            <TextField\n              label=\"Caption\"\n              value={stringValue(data.caption)}\n              onChange={(value) => update(\"caption\", value)}\n              placeholder=\"Optional visible caption\"\n            />\n          </div>\n        </div>\n      );\n    case \"embed\":\n      return (\n        <div className=\"space-y-4\">\n          <TextField\n            label=\"Embed URL\"\n            value={stringValue(data.url)}\n            onChange={(value) => update(\"url\", value)}\n            placeholder=\"YouTube, Vimeo, Spotify, X, or GitHub Gist URL\"\n            type=\"url\"\n          />\n          <TextField\n            label=\"Accessible title\"\n            value={stringValue(data.title)}\n            onChange={(value) => update(\"title\", value)}\n            placeholder=\"Describe the embedded content\"\n          />\n          <p className=\"font-mono text-[0.6875rem] leading-relaxed text-muted-foreground\">\n            Rich iframes are limited to YouTube, Vimeo, Spotify, and X posts.\n            GitHub Gists and other web URLs render as safe link cards.\n          </p>\n        </div>\n      );\n    case \"link\":\n      return (\n        <LinkBlockFields\n          data={data}\n          onDataChange={onDataChange}\n          renderImageField={renderImageField}\n          fetchLinkMetadata={fetchLinkMetadata}\n        />\n      );\n    case \"callout\":\n      return (\n        <div className=\"space-y-4\">\n          <div className=\"grid gap-4 sm:grid-cols-2\">\n            <TextField\n              label=\"Eyebrow\"\n              value={stringValue(data.eyebrow)}\n              onChange={(value) => update(\"eyebrow\", value)}\n              placeholder=\"Note\"\n            />\n            <TextField\n              label=\"Title\"\n              value={stringValue(data.title)}\n              onChange={(value) => update(\"title\", value)}\n              placeholder=\"Optional callout title\"\n            />\n          </div>\n          <TextAreaField\n            label=\"Content\"\n            value={stringValue(data.content)}\n            onChange={(value) => update(\"content\", value)}\n            placeholder=\"Callout content (Markdown supported)\"\n            rows={5}\n          />\n        </div>\n      );\n    case \"component\": {\n      return (\n        <div className=\"space-y-4\">\n          <label className=\"block space-y-1.5\">\n            <span className=\"text-sm font-medium\">Registry component</span>\n            <select\n              value={stringValue(data.component_name)}\n              onChange={(event) => update(\"component_name\", event.target.value)}\n              className=\"w-full rounded-md border bg-transparent px-3 py-2 text-sm outline-none transition-all focus:border-foreground\"\n            >\n              <option value=\"\">Choose a component…</option>\n              {REGISTRY_COMPONENT_OPTIONS.map((opt) => (\n                <option key={opt.value} value={opt.value}>{opt.label}</option>\n              ))}\n            </select>\n          </label>\n          <TextField\n            label=\"Caption (optional)\"\n            value={stringValue(data.title)}\n            onChange={(value) => update(\"title\", value)}\n            placeholder=\"Optional label above the component\"\n          />\n        </div>\n      );\n    }\n    case \"poll\":\n    case \"quiz\":\n    case \"list\": {\n      const interactiveField = renderInteractiveField ? (\n        renderInteractiveField({\n          type: block.block_type,\n          label: `${CONTENT_BLOCK_LABELS[block.block_type]} record`,\n          value: stringValue(data.entity_id),\n          onChange: (value, title) =>\n            onDataChange({\n              ...data,\n              entity_id: value,\n              title: title ?? stringValue(data.title),\n            }),\n        })\n      ) : (\n        <TextField\n          label={`${CONTENT_BLOCK_LABELS[block.block_type]} record ID`}\n          value={stringValue(data.entity_id)}\n          onChange={(value) => update(\"entity_id\", value)}\n          placeholder=\"Connected record ID\"\n        />\n      );\n      return (\n        <div className=\"grid gap-4 sm:grid-cols-2\">\n          {interactiveField}\n          <TextField\n            label=\"Editor label\"\n            value={stringValue(data.title)}\n            onChange={(value) => update(\"title\", value)}\n            placeholder={`Optional ${block.block_type} title`}\n          />\n        </div>\n      );\n    }\n  }\n}\n\nexport function ContentBlockEditor({\n  blocks,\n  onChange,\n  renderImageField,\n  renderGalleryField,\n  renderInteractiveField,\n  fetchLinkMetadata,\n  className,\n  ...props\n}: ContentBlockEditorProps) {\n  const errors = useMemo(() => validateContentBlocks(blocks), [blocks]);\n  const emit = (next: ContentBlockRecord[]) =>\n    onChange(resequenceContentBlocks(next));\n\n  const addBlock = (type: ContentBlockType) => {\n    emit([...blocks, createContentBlock(type, { sort_order: blocks.length })]);\n  };\n\n  const updateBlock = (\n    blockKey: string,\n    update: Partial<ContentBlockRecord>,\n  ) => {\n    emit(\n      blocks.map((block) =>\n        block.block_key === blockKey ? { ...block, ...update } : block,\n      ),\n    );\n  };\n\n  const duplicateBlock = (block: ContentBlockRecord) => {\n    const index = blocks.findIndex(\n      (candidate) => candidate.block_key === block.block_key,\n    );\n    const next = [...blocks];\n    next.splice(\n      index + 1,\n      0,\n      createContentBlock(block.block_type, {\n        data: JSON.parse(JSON.stringify(block.data || {})) as Record<\n          string,\n          unknown\n        >,\n      }),\n    );\n    emit(next);\n  };\n\n  const moveBlock = (index: number, offset: number) => {\n    const destination = index + offset;\n    if (destination < 0 || destination >= blocks.length) return;\n    onChange(reorderContentBlocks(blocks, index, destination));\n  };\n\n  return (\n    <div className={cn(\"space-y-5\", className)} {...props}>\n      <div className=\"grid grid-cols-2 gap-px border-y border-dashed bg-border p-px sm:grid-cols-4\">\n        {CONTENT_BLOCK_TYPES.map((type) => {\n          const Icon = BLOCK_ICONS[type];\n          return (\n            <button\n              key={type}\n              type=\"button\"\n              onClick={() => addBlock(type)}\n              className=\"flex items-center justify-center gap-2 bg-background px-3 py-3 text-xs font-medium transition-all hover:bg-muted/40\"\n            >\n              <Icon className=\"size-3.5\" />\n              <span>{CONTENT_BLOCK_LABELS[type]}</span>\n              <Plus className=\"size-3 text-muted-foreground\" />\n            </button>\n          );\n        })}\n      </div>\n\n      {blocks.length === 0 ? (\n        <div className=\"rounded-md border border-dashed px-5 py-12 text-center\">\n          <p className=\"text-sm font-medium\">No content blocks yet.</p>\n          <p className=\"mt-1 text-xs text-muted-foreground\">\n            Choose a block type above to begin.\n          </p>\n        </div>\n      ) : (\n        <DragDropContext\n          onDragEnd={(result) => {\n            if (!result.destination) return;\n            onChange(\n              reorderContentBlocks(\n                blocks,\n                result.source.index,\n                result.destination.index,\n              ),\n            );\n          }}\n        >\n          <Droppable droppableId=\"content-block-editor\">\n            {(provided) => (\n              <div\n                ref={provided.innerRef}\n                {...provided.droppableProps}\n                className=\"space-y-4\"\n              >\n                {blocks.map((block, index) => {\n                  const Icon = BLOCK_ICONS[block.block_type];\n                  const blockErrors = errors[block.block_key] || [];\n                  return (\n                    <Draggable\n                      key={block.block_key}\n                      draggableId={block.block_key}\n                      index={index}\n                    >\n                      {(dragProvided, snapshot) => (\n                        <section\n                          ref={dragProvided.innerRef}\n                          {...dragProvided.draggableProps}\n                          className={cn(\n                            \"rounded-md border bg-background transition-all\",\n                            snapshot.isDragging &&\n                              \"border-foreground shadow-lg\",\n                          )}\n                        >\n                          <div className=\"flex items-center gap-2 border-b px-2 py-2\">\n                            <button\n                              type=\"button\"\n                              {...dragProvided.dragHandleProps}\n                              className=\"flex size-8 cursor-grab items-center justify-center rounded text-muted-foreground transition-all hover:bg-muted hover:text-foreground active:cursor-grabbing\"\n                              aria-label={`Reorder ${CONTENT_BLOCK_LABELS[block.block_type]} block`}\n                            >\n                              <GripVertical className=\"size-4\" />\n                            </button>\n                            <Icon className=\"size-4 text-muted-foreground\" />\n                            <span className=\"font-mono text-[0.6875rem] font-medium tracking-[0.16em] uppercase\">\n                              {index + 1}.{\" \"}\n                              {CONTENT_BLOCK_LABELS[block.block_type]}\n                            </span>\n                            <div className=\"ml-auto flex items-center gap-1\">\n                              <button\n                                type=\"button\"\n                                onClick={() => moveBlock(index, -1)}\n                                disabled={index === 0}\n                                className=\"flex size-8 items-center justify-center rounded text-muted-foreground transition-all hover:bg-muted hover:text-foreground disabled:opacity-30\"\n                                aria-label=\"Move block up\"\n                              >\n                                <ArrowUp className=\"size-3.5\" />\n                              </button>\n                              <button\n                                type=\"button\"\n                                onClick={() => moveBlock(index, 1)}\n                                disabled={index === blocks.length - 1}\n                                className=\"flex size-8 items-center justify-center rounded text-muted-foreground transition-all hover:bg-muted hover:text-foreground disabled:opacity-30\"\n                                aria-label=\"Move block down\"\n                              >\n                                <ArrowDown className=\"size-3.5\" />\n                              </button>\n                              <button\n                                type=\"button\"\n                                onClick={() => duplicateBlock(block)}\n                                className=\"flex size-8 items-center justify-center rounded text-muted-foreground transition-all hover:bg-muted hover:text-foreground\"\n                                aria-label=\"Duplicate block\"\n                              >\n                                <Copy className=\"size-3.5\" />\n                              </button>\n                              <button\n                                type=\"button\"\n                                onClick={() =>\n                                  emit(\n                                    blocks.filter(\n                                      (candidate) =>\n                                        candidate.block_key !== block.block_key,\n                                    ),\n                                  )\n                                }\n                                className=\"flex size-8 items-center justify-center rounded text-muted-foreground transition-all hover:bg-muted hover:text-foreground\"\n                                aria-label=\"Delete block\"\n                              >\n                                <Trash2 className=\"size-3.5\" />\n                              </button>\n                            </div>\n                          </div>\n                          <div className=\"p-4 sm:p-5\">\n                        <BlockFields\n                              block={block}\n                              onDataChange={(data) =>\n                                updateBlock(block.block_key, { data })\n                              }\n                              renderImageField={renderImageField}\n                          renderGalleryField={renderGalleryField}\n                          renderInteractiveField={renderInteractiveField}\n                          fetchLinkMetadata={fetchLinkMetadata}\n                        />\n                            {blockErrors.length > 0 ? (\n                              <ul className=\"mt-4 border-t border-dashed pt-3 font-mono text-[0.6875rem] text-muted-foreground\">\n                                {blockErrors.map((error) => (\n                                  <li key={error}>— {error}</li>\n                                ))}\n                              </ul>\n                            ) : null}\n                          </div>\n                        </section>\n                      )}\n                    </Draggable>\n                  );\n                })}\n                {provided.placeholder}\n              </div>\n            )}\n          </Droppable>\n        </DragDropContext>\n      )}\n    </div>\n  );\n}",
      "type": "registry:component",
      "target": "@components/content-block-editor.tsx"
    }
  ]
}