{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "media-placeholder-node",
  "title": "Media Placeholder Element",
  "description": "A placeholder for media upload progress indication.",
  "dependencies": [
    "@platejs/media",
    "use-file-picker@2.1.2"
  ],
  "registryDependencies": [
    "https://platejs.org/r/uploadthing.json"
  ],
  "files": [
    {
      "path": "src/registry/ui/media-placeholder-node.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\n\nimport type { TPlaceholderElement } from 'platejs';\nimport type { PlateElementProps } from 'platejs/react';\n\nimport {\n  PlaceholderPlugin,\n  PlaceholderProvider,\n  updateUploadHistory,\n} from '@platejs/media/react';\nimport { AudioLines, FileUp, Film, ImageIcon, Loader2Icon } from 'lucide-react';\nimport { KEYS } from 'platejs';\nimport { PlateElement, useEditorPlugin, withHOC } from 'platejs/react';\nimport { useFilePicker } from 'use-file-picker';\n\nimport { cn } from '@/lib/utils';\nimport { useUploadFile } from '@/registry/hooks/use-upload-file';\n\nconst CONTENT: Record<\n  string,\n  {\n    accept: string[];\n    content: React.ReactNode;\n    icon: React.ReactNode;\n  }\n> = {\n  [KEYS.audio]: {\n    accept: ['audio/*'],\n    content: 'Add an audio file',\n    icon: <AudioLines />,\n  },\n  [KEYS.file]: {\n    accept: ['*'],\n    content: 'Add a file',\n    icon: <FileUp />,\n  },\n  [KEYS.img]: {\n    accept: ['image/*'],\n    content: 'Add an image',\n    icon: <ImageIcon />,\n  },\n  [KEYS.video]: {\n    accept: ['video/*'],\n    content: 'Add a video',\n    icon: <Film />,\n  },\n};\n\nexport const PlaceholderElement = withHOC(\n  PlaceholderProvider,\n  function PlaceholderElement(props: PlateElementProps<TPlaceholderElement>) {\n    const { editor, element } = props;\n\n    const { api } = useEditorPlugin(PlaceholderPlugin);\n\n    const { isUploading, progress, uploadedFile, uploadFile, uploadingFile } =\n      useUploadFile();\n\n    const loading = isUploading && uploadingFile;\n\n    const currentContent = CONTENT[element.mediaType];\n\n    const isImage = element.mediaType === KEYS.img;\n\n    const imageRef = React.useRef<HTMLImageElement>(null);\n\n    const { openFilePicker } = useFilePicker({\n      accept: currentContent.accept,\n      multiple: true,\n      onFilesSelected: ({ plainFiles: updatedFiles }) => {\n        const firstFile = updatedFiles[0];\n        const restFiles = updatedFiles.slice(1);\n\n        replaceCurrentPlaceholder(firstFile);\n\n        if (restFiles.length > 0) {\n          editor.getTransforms(PlaceholderPlugin).insert.media(restFiles);\n        }\n      },\n    });\n\n    const replaceCurrentPlaceholder = React.useCallback(\n      (file: File) => {\n        void uploadFile(file);\n        api.placeholder.addUploadingFile(element.id as string, file);\n      },\n      [api.placeholder, element.id, uploadFile]\n    );\n\n    React.useEffect(() => {\n      if (!uploadedFile) return;\n\n      const path = editor.api.findPath(element);\n\n      editor.tf.withoutSaving(() => {\n        editor.tf.removeNodes({ at: path });\n\n        const node = {\n          children: [{ text: '' }],\n          initialHeight: imageRef.current?.height,\n          initialWidth: imageRef.current?.width,\n          isUpload: true,\n          name: element.mediaType === KEYS.file ? uploadedFile.name : '',\n          placeholderId: element.id as string,\n          type: element.mediaType!,\n          url: uploadedFile.url,\n        };\n\n        editor.tf.insertNodes(node, { at: path });\n\n        updateUploadHistory(editor, node);\n      });\n\n      api.placeholder.removeUploadingFile(element.id as string);\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [uploadedFile, element.id]);\n\n    // React dev mode will call React.useEffect twice\n    const isReplaced = React.useRef(false);\n\n    /** Paste and drop */\n    React.useEffect(() => {\n      if (isReplaced.current) return;\n\n      isReplaced.current = true;\n      const currentFiles = api.placeholder.getUploadingFile(\n        element.id as string\n      );\n\n      if (!currentFiles) return;\n\n      replaceCurrentPlaceholder(currentFiles);\n\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [isReplaced]);\n\n    return (\n      <PlateElement className=\"my-1\" {...props}>\n        {(!loading || !isImage) && (\n          <div\n            className={cn(\n              'flex cursor-pointer select-none items-center rounded-sm bg-muted p-3 pr-9 hover:bg-primary/10'\n            )}\n            onClick={() => !loading && openFilePicker()}\n            contentEditable={false}\n          >\n            <div className=\"relative mr-3 flex text-muted-foreground/80 [&_svg]:size-6\">\n              {currentContent.icon}\n            </div>\n            <div className=\"whitespace-nowrap text-muted-foreground text-sm\">\n              <div>\n                {loading ? uploadingFile?.name : currentContent.content}\n              </div>\n\n              {loading && !isImage && (\n                <div className=\"mt-1 flex items-center gap-1.5\">\n                  <div>{formatBytes(uploadingFile?.size ?? 0)}</div>\n                  <div>–</div>\n                  <div className=\"flex items-center\">\n                    <Loader2Icon className=\"mr-1 size-3.5 animate-spin text-muted-foreground\" />\n                    {progress ?? 0}%\n                  </div>\n                </div>\n              )}\n            </div>\n          </div>\n        )}\n\n        {isImage && loading && (\n          <ImageProgress\n            file={uploadingFile}\n            imageRef={imageRef}\n            progress={progress}\n          />\n        )}\n\n        {props.children}\n      </PlateElement>\n    );\n  }\n);\n\nexport function ImageProgress({\n  className,\n  file,\n  imageRef,\n  progress = 0,\n}: {\n  file: File;\n  className?: string;\n  imageRef?: React.RefObject<HTMLImageElement | null>;\n  progress?: number;\n}) {\n  const [objectUrl, setObjectUrl] = React.useState<string | null>(null);\n\n  React.useEffect(() => {\n    const url = URL.createObjectURL(file);\n    // eslint-disable-next-line react-hooks/set-state-in-effect -- Store the object URL tied to this File and revoke it on cleanup.\n    setObjectUrl(url);\n\n    return () => {\n      URL.revokeObjectURL(url);\n    };\n  }, [file]);\n\n  if (!objectUrl) {\n    return null;\n  }\n\n  return (\n    <div className={cn('relative', className)} contentEditable={false}>\n      <img\n        ref={imageRef}\n        className=\"h-auto w-full rounded-sm object-cover\"\n        alt={file.name}\n        src={objectUrl}\n      />\n      {progress < 100 && (\n        <div className=\"absolute right-1 bottom-1 flex items-center space-x-2 rounded-full bg-black/50 px-1 py-0.5\">\n          <Loader2Icon className=\"size-3.5 animate-spin text-muted-foreground\" />\n          <span className=\"font-medium text-white text-xs\">\n            {Math.round(progress)}%\n          </span>\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction formatBytes(\n  bytes: number,\n  opts: {\n    decimals?: number;\n    sizeType?: 'accurate' | 'normal';\n  } = {}\n) {\n  const { decimals = 0, sizeType = 'normal' } = opts;\n\n  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];\n  const accurateSizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'];\n\n  if (bytes === 0) return '0 Byte';\n\n  const i = Math.floor(Math.log(bytes) / Math.log(1024));\n\n  return `${(bytes / 1024 ** i).toFixed(decimals)} ${\n    sizeType === 'accurate'\n      ? (accurateSizes[i] ?? 'Bytest')\n      : (sizes[i] ?? 'Bytes')\n  }`;\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "docs": [
      {
        "route": "/docs/media"
      },
      {
        "route": "https://pro.platejs.org/docs/components/media-placeholder-node"
      }
    ],
    "examples": [
      "media-demo",
      "media-pro"
    ]
  },
  "type": "registry:ui"
}