{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "media-library",
  "title": "Media Library",
  "author": "ilinxa",
  "description": "Drive-style media library — folders and files, lazy loading, drag-drop upload, drag-to-move, context menus, and multi-type preview.",
  "dependencies": [
    "@dnd-kit/core",
    "date-fns",
    "lucide-react"
  ],
  "registryDependencies": [
    "@ilinxa/file-tree",
    "@ilinxa/pdf-viewer",
    "@ilinxa/code-block",
    "@ilinxa/markdown-editor",
    "@ilinxa/video-player",
    "button",
    "context-menu",
    "dialog",
    "input",
    "progress",
    "skeleton"
  ],
  "files": [
    {
      "path": "src/registry/components/media/media-library/media-library.tsx",
      "content": "\"use client\";\n\nimport { forwardRef } from \"react\";\nimport type { MediaLibraryHandle, MediaLibraryProps } from \"./types\";\nimport { MediaLibraryRoot } from \"./parts/media-library-root\";\nimport { MediaLibraryToolbar } from \"./parts/library-toolbar\";\nimport { MediaLibraryQuotaBar } from \"./parts/quota-bar\";\nimport { MediaLibraryTypeFilters } from \"./parts/type-filters\";\nimport { MediaLibraryBreadcrumbs } from \"./parts/breadcrumbs\";\nimport { MediaLibrarySidebar } from \"./parts/sidebar\";\nimport { MediaLibraryFolderRow } from \"./parts/folder-row\";\nimport { MediaLibraryFileGrid } from \"./parts/file-grid\";\nimport { MediaLibraryDetailsPane } from \"./parts/details-pane\";\nimport { MediaLibraryLightbox } from \"./parts/preview-lightbox\";\n\n/**\n * Tier A — the batteries-included Google-Drive-style media library. A thin\n * assembly of `<MediaLibraryRoot>` + the canonical parts, gated by the\n * `show*` / `preview` toggles. For a lighter, hand-composed version, drop down\n * to `<MediaLibraryRoot>` + the individual parts (all exported).\n */\nexport const MediaLibrary = forwardRef<MediaLibraryHandle, MediaLibraryProps>(\n  function MediaLibrary(props, ref) {\n    const {\n      showQuota = true,\n      showTypeFilters = true,\n      showSidebar = true,\n      preview = \"both\",\n      className,\n      ...rootProps\n    } = props;\n\n    const showPane = preview === \"pane\" || preview === \"both\";\n    const showLightbox = preview === \"lightbox\" || preview === \"both\";\n\n    return (\n      <MediaLibraryRoot\n        ref={ref}\n        preview={preview}\n        className={className}\n        {...rootProps}\n      >\n        {showQuota ? <MediaLibraryQuotaBar /> : null}\n        <MediaLibraryToolbar />\n        {showTypeFilters ? <MediaLibraryTypeFilters /> : null}\n        <MediaLibraryBreadcrumbs />\n        <div className=\"flex min-h-0 gap-4\">\n          {showSidebar ? <MediaLibrarySidebar /> : null}\n          <div className=\"flex min-w-0 flex-1 flex-col gap-5\">\n            <MediaLibraryFolderRow />\n            <MediaLibraryFileGrid />\n          </div>\n          {showPane ? <MediaLibraryDetailsPane /> : null}\n        </div>\n        {showLightbox ? <MediaLibraryLightbox /> : null}\n      </MediaLibraryRoot>\n    );\n  },\n);\n\n// --- Tail re-export band (F-01): a future cross-procomp consumer imports public\n// types from this `.tsx` entry, never a `/types` subpath. ---\nexport type {\n  FsNode,\n  FsNodeType,\n  MediaNode,\n  MediaPreviewKind,\n  MediaFilterCategory,\n  MediaLibraryStorage,\n  MediaUploadItem,\n  MediaUploadStatus,\n  MediaUploadProgressFn,\n  MediaLibraryLabels,\n  MediaLibraryPreviewMode,\n  MediaLibraryRootProps,\n  MediaLibraryProps,\n  MediaLibraryHandle,\n} from \"./types\";\n",
      "type": "registry:component",
      "target": "components/media-library/media-library.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/index.ts",
      "content": "// Tier A — batteries-included assembly\nexport { MediaLibrary } from \"./media-library\";\n\n// Tier B — headless provider + context-connected parts (compose your own)\nexport { MediaLibraryRoot } from \"./parts/media-library-root\";\nexport { MediaLibraryToolbar } from \"./parts/library-toolbar\";\nexport { MediaLibraryQuotaBar } from \"./parts/quota-bar\";\nexport { MediaLibraryTypeFilters } from \"./parts/type-filters\";\nexport { MediaLibraryBreadcrumbs } from \"./parts/breadcrumbs\";\nexport { MediaLibrarySidebar } from \"./parts/sidebar\";\nexport { MediaLibraryFolderRow } from \"./parts/folder-row\";\nexport { MediaLibraryFileGrid } from \"./parts/file-grid\";\nexport { MediaLibraryDetailsPane } from \"./parts/details-pane\";\nexport { MediaLibraryLightbox } from \"./parts/preview-lightbox\";\nexport { MediaLibraryUploadOverlay } from \"./parts/upload-overlay\";\n\n// Tier C — standalone primitives (no context)\nexport { FilePreview } from \"./parts/file-preview\";\nexport { FilePreviewLightbox } from \"./parts/preview-lightbox\";\nexport { FileCard } from \"./parts/file-card\";\nexport { FolderCard } from \"./parts/folder-card\";\nexport { QuotaBar } from \"./parts/quota-bar\";\nexport { UploadOverlay } from \"./parts/upload-overlay\";\nexport { FileKindBadge, PreviewKindIcon } from \"./parts/file-visuals\";\n\n// Hook + helpers\nexport { useMediaLibrary } from \"./hooks/use-media-library\";\nexport type { MediaLibraryContextValue } from \"./hooks/use-media-library\";\nexport { resolvePreviewKind, isTextKind, extOf } from \"./lib/preview-kind\";\nexport {\n  filterCategoryForNode,\n  filterCategoryForKind,\n  computeFilterCounts,\n} from \"./lib/filter-category\";\nexport { formatBytes, formatRelativeTime } from \"./lib/format\";\n\n// Defaults\nexport { DEFAULT_MEDIA_LIBRARY_LABELS } from \"./types\";\n\n// Part prop types\nexport type { QuotaBarProps } from \"./parts/quota-bar\";\nexport type { FileCardProps } from \"./parts/file-card\";\nexport type { FolderCardProps } from \"./parts/folder-card\";\nexport type { FilePreviewProps } from \"./parts/file-preview\";\nexport type { FilePreviewLightboxProps } from \"./parts/preview-lightbox\";\n\n// Public types\nexport type {\n  FsNode,\n  FsNodeType,\n  MediaNode,\n  MediaPreviewKind,\n  MediaFilterCategory,\n  MediaLibraryStorage,\n  MediaUploadItem,\n  MediaUploadStatus,\n  MediaUploadProgressFn,\n  MediaLibraryLabels,\n  MediaLibraryPreviewMode,\n  MediaLibraryRootProps,\n  MediaLibraryProps,\n  MediaLibraryHandle,\n} from \"./types\";\n",
      "type": "registry:component",
      "target": "components/media-library/index.ts"
    },
    {
      "path": "src/registry/components/media/media-library/types.ts",
      "content": "import type { ReactNode, Ref } from \"react\";\n\n/**\n * FsNode is DECLARED LOCALLY (not imported from file-tree).\n *\n * VERIFIED 2026-06-09: `file-tree.tsx` only *imports* `FsNode` (no tail\n * re-export band), so a `.tsx`-entry type import would fail; a `/types`\n * subpath import trips the F-S1 slug-rewriter; and adding a tail band to\n * `file-tree.tsx` would modify a shipped component (forbidden). Re-declaring\n * the (tiny, stable) interface keeps file-tree UNTOUCHED and zeroes the\n * cross-procomp *type* surface — structural typing bridges at the composed\n * `<FileTree nodes={...}>` boundary (the carousel-composer\n * \"define your own type locally\" precedent).\n */\nexport type FsNodeType = \"file\" | \"folder\";\n\nexport interface FsNode {\n  id: string;\n  name: string;\n  type: FsNodeType;\n  parentId?: string | null;\n  /** `undefined` ⇒ lazy-load trigger; `[]` ⇒ known-empty; `FsNode[]` ⇒ loaded. */\n  children?: FsNode[];\n  ext?: string;\n  /** Bytes. */\n  size?: number;\n  /** ISO 8601. */\n  modifiedAt?: string;\n  icon?: ReactNode;\n  meta?: Record<string, unknown>;\n}\n\n/** Which viewer a file maps to in the preview dispatcher. */\nexport type MediaPreviewKind =\n  | \"image\"\n  | \"video\"\n  | \"pdf\"\n  | \"code\"\n  | \"json\"\n  | \"text\"\n  | \"markdown\"\n  | \"unknown\";\n\n/** Type-filter chip identity. */\nexport type MediaFilterCategory = \"all\" | \"images\" | \"video\" | \"pdfs\" | \"docs\";\n\n/**\n * A media-library node. Strict superset of {@link FsNode} — every added field\n * is optional, so any `FsNode[]` is assignable to `MediaNode[]` (interop with\n * file-tree / file-manager preserved).\n */\nexport interface MediaNode extends FsNode {\n  /** Canonical asset URL — the preview source. */\n  url?: string;\n  /** Poster / thumbnail for the grid (images fall back to `url`). */\n  thumbnailUrl?: string;\n  /** Authoritative MIME type; `ext` is the fallback for kind resolution. */\n  mimeType?: string;\n  width?: number;\n  height?: number;\n  owner?: string;\n  children?: MediaNode[];\n}\n\nexport interface MediaLibraryStorage {\n  /** Bytes used. */\n  used: number;\n  /** Bytes total. */\n  total: number;\n}\n\n/**\n * Fires repeatedly during a single file's upload (0–100); consumer-driven.\n * `onUpload` is invoked once per file, so the progress fn is pre-bound to that\n * file's optimistic item — the consumer just reports its percent.\n */\nexport type MediaUploadProgressFn = (pct: number) => void;\n\n/** Internal status of an optimistic upload placeholder. */\nexport type MediaUploadStatus = \"uploading\" | \"error\";\n\n/** An in-flight optimistic upload item shown in the grid before it resolves. */\nexport interface MediaUploadItem {\n  tempId: string;\n  name: string;\n  kind: MediaPreviewKind;\n  pct: number;\n  status: MediaUploadStatus;\n  /** Object URL for a local preview while uploading (owned + revoked by the lib). */\n  previewUrl?: string;\n  error?: string;\n  /** The folder the upload targets. */\n  targetFolderId: string | null;\n}\n\nexport interface MediaLibraryLabels {\n  title?: string;\n  searchPlaceholder?: string;\n  uploadButton?: string;\n  newFolderButton?: string;\n  foldersHeading?: string;\n  filesHeading?: string;\n  libraryHeading?: string;\n  /** Template with `{used}` and `{total}`, e.g. \"{used} of {total} used\". */\n  quotaUsed?: string;\n  filterAll?: string;\n  filterImages?: string;\n  filterVideo?: string;\n  filterPdfs?: string;\n  filterDocs?: string;\n  emptyFolder?: string;\n  emptyLibrary?: string;\n  dropToUpload?: string;\n  previewLoading?: string;\n  previewError?: string;\n  noPreview?: string;\n  download?: string;\n  retry?: string;\n  // context-menu verbs\n  open?: string;\n  preview?: string;\n  rename?: string;\n  move?: string;\n  cut?: string;\n  copy?: string;\n  paste?: string;\n  delete?: string;\n  // a11y\n  /** Template with `{name}` and `{kind}`. */\n  itemAria?: string;\n  /** Template with `{name}`. */\n  uploadingAria?: string;\n  /** Template with `{count}`. */\n  selectedAria?: string;\n  prev?: string;\n  next?: string;\n  close?: string;\n}\n\nexport type MediaLibraryPreviewMode = \"pane\" | \"lightbox\" | \"both\" | false;\n\n/**\n * Shared data + handler surface. {@link MediaLibraryProps} adds the chrome\n * toggles on top of this; {@link MediaLibraryRootProps} is this verbatim\n * (composition replaces the toggles).\n */\nexport interface MediaLibraryRootProps {\n  nodes: MediaNode[];\n  currentFolderId?: string | null;\n  defaultCurrentFolderId?: string | null;\n  onCurrentFolderChange?: (folderId: string | null) => void;\n  onLoadChildren?: (folderId: string) => Promise<MediaNode[]>;\n\n  storage?: MediaLibraryStorage;\n\n  selectedIds?: Set<string>;\n  defaultSelectedIds?: Set<string>;\n  onSelectedChange?: (ids: Set<string>) => void;\n\n  // mutation operations (omit to hide the affordance).\n  // `onUpload` is called once per file; `progress` reports that file's percent.\n  onUpload?: (\n    files: File[],\n    targetFolderId: string | null,\n    progress: MediaUploadProgressFn,\n  ) => Promise<MediaNode[]>;\n  onMove?: (ids: string[], targetFolderId: string | null) => void;\n  onRename?: (id: string, name: string) => void;\n  onDelete?: (ids: string[]) => void;\n  onCreateFolder?: (parentId: string | null, name: string) => void;\n  onDownload?: (ids: string[]) => void;\n  validateRename?: (id: string, name: string) => string | null;\n\n  // preview content sourcing (URL-based; text kinds fetched by the lib)\n  resolveTextContent?: (node: MediaNode) => Promise<string>;\n  /**\n   * Override the pdf.js worker URL used by the composed pdf-viewer. The viewer\n   * defaults to a unpkg CDN worker; pass a self-hosted path\n   * (e.g. \"/pdf.worker.min.mjs\") for offline / locked-down environments.\n   */\n  pdfWorkerSrc?: string;\n  onPreviewOpen?: (id: string) => void;\n  onPreviewClose?: () => void;\n\n  // behaviour\n  enableUploadDrop?: boolean;\n  enableInternalDrag?: boolean;\n  enableContextMenu?: boolean;\n  /** Whether opening a file shows a preview at all (default \"both\"). */\n  preview?: MediaLibraryPreviewMode;\n  virtualizeThreshold?: number;\n\n  labels?: Partial<MediaLibraryLabels>;\n  children?: ReactNode;\n  className?: string;\n  ref?: Ref<MediaLibraryHandle>;\n}\n\n/** The batteries-included assembly: root surface + chrome toggles. */\nexport interface MediaLibraryProps\n  extends Omit<MediaLibraryRootProps, \"children\"> {\n  showQuota?: boolean;\n  showTypeFilters?: boolean;\n  showSidebar?: boolean;\n}\n\nexport interface MediaLibraryHandle {\n  navigateTo: (folderId: string | null) => void;\n  refresh: (folderId?: string | null) => void;\n  openPreview: (id: string) => void;\n  closePreview: () => void;\n  /** Opens the OS file picker for the current folder. */\n  triggerUpload: () => void;\n  getSelectedIds: () => string[];\n  clearSelection: () => void;\n}\n\nexport const DEFAULT_MEDIA_LIBRARY_LABELS: Required<MediaLibraryLabels> = {\n  title: \"Media library\",\n  searchPlaceholder: \"Search this folder\",\n  uploadButton: \"Upload\",\n  newFolderButton: \"New folder\",\n  foldersHeading: \"Folders\",\n  filesHeading: \"Files\",\n  libraryHeading: \"Library\",\n  quotaUsed: \"{used} of {total} used\",\n  filterAll: \"All\",\n  filterImages: \"Images\",\n  filterVideo: \"Video\",\n  filterPdfs: \"PDFs\",\n  filterDocs: \"Docs\",\n  emptyFolder: \"This folder is empty\",\n  emptyLibrary: \"No media yet — upload to get started\",\n  dropToUpload: \"Drop files to upload\",\n  previewLoading: \"Loading preview…\",\n  previewError: \"Couldn't load this file\",\n  noPreview: \"No preview available\",\n  download: \"Download\",\n  retry: \"Retry\",\n  open: \"Open\",\n  preview: \"Preview\",\n  rename: \"Rename\",\n  move: \"Move\",\n  cut: \"Cut\",\n  copy: \"Copy\",\n  paste: \"Paste\",\n  delete: \"Delete\",\n  itemAria: \"{name}, {kind}\",\n  uploadingAria: \"Uploading {name}\",\n  selectedAria: \"{count} selected\",\n  prev: \"Previous\",\n  next: \"Next\",\n  close: \"Close\",\n};\n",
      "type": "registry:component",
      "target": "components/media-library/types.ts"
    },
    {
      "path": "src/registry/components/media/media-library/hooks/use-controllable-state.ts",
      "content": "import { useCallback, useEffect, useRef, useState } from \"react\";\n\ninterface UseControllableStateOpts<T> {\n  /** Controlled value. When provided, state is fully controlled by parent. */\n  value?: T;\n  /** Initial value for uncontrolled mode. */\n  defaultValue: T;\n  /** Fires on every state change (both modes). */\n  onChange?: (next: T) => void;\n  /** Component name used in dev warnings. */\n  componentName: string;\n  /** Prop name used in dev warnings (e.g., `\"selectedIds\"`). */\n  valuePropName: string;\n}\n\n/**\n * Controlled+uncontrolled state helper. Sealed copy of the proven generic used\n * across the library (account-switcher, code-block, carousel-composer)\n * — registry portability bans a shared cross-component module, so each procomp\n * vendors its own. Locks the mode at first render and dev-warns on mode-flip or\n * controlled-without-onChange.\n */\nexport function useControllableState<T>({\n  value,\n  defaultValue,\n  onChange,\n  componentName,\n  valuePropName,\n}: UseControllableStateOpts<T>): readonly [T, (next: T) => void] {\n  const [internal, setInternal] = useState<T>(defaultValue);\n  const isControlled = value !== undefined;\n  const onChangeRef = useRef(onChange);\n  useEffect(() => {\n    onChangeRef.current = onChange;\n  });\n\n  const wasControlledRef = useRef(isControlled);\n  useEffect(() => {\n    if (process.env.NODE_ENV === \"production\") return;\n    if (wasControlledRef.current !== isControlled) {\n      console.warn(\n        `[${componentName}] \\`${valuePropName}\\` switched from ${\n          wasControlledRef.current ? \"controlled\" : \"uncontrolled\"\n        } to ${\n          isControlled ? \"controlled\" : \"uncontrolled\"\n        } mode. Components should not switch modes mid-life; pick one at mount.`,\n      );\n      wasControlledRef.current = isControlled;\n    }\n  }, [isControlled, componentName, valuePropName]);\n\n  const current = isControlled ? (value as T) : internal;\n\n  const set = useCallback(\n    (next: T) => {\n      if (!isControlled) setInternal(next);\n      onChangeRef.current?.(next);\n    },\n    [isControlled],\n  );\n\n  return [current, set] as const;\n}\n",
      "type": "registry:component",
      "target": "components/media-library/hooks/use-controllable-state.ts"
    },
    {
      "path": "src/registry/components/media/media-library/hooks/use-lazy-children.ts",
      "content": "import { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { MediaNode } from \"../types\";\n\n/** Recursively replace a folder's `children` with cache-loaded children. */\nfunction mergeCache(\n  nodes: MediaNode[],\n  cache: Record<string, MediaNode[]>,\n): MediaNode[] {\n  return nodes.map((node) => {\n    if (node.type !== \"folder\") return node;\n    const loaded = cache[node.id];\n    const children = loaded ?? node.children;\n    return {\n      ...node,\n      children: children ? mergeCache(children, cache) : children,\n    };\n  });\n}\n\nexport interface LazyChildrenApi {\n  /** Root nodes with lazily-loaded children merged in. */\n  nodes: MediaNode[];\n  /** Trigger a load for a folder if not cached / in flight. */\n  ensureLoaded: (folderId: string) => void;\n  /** Drop a folder's cache (or all) so the next visit re-loads. */\n  invalidate: (folderId?: string | null) => void;\n  loadingIds: ReadonlySet<string>;\n  errorIds: Record<string, string>;\n}\n\n/**\n * Lazy-children manager: caches `onLoadChildren` results, merges them into the\n * tree, tracks per-folder loading + error. No-op when `onLoadChildren` is\n * absent (the consumer supplies the full tree inline).\n */\nexport function useLazyChildren(\n  rootNodes: MediaNode[],\n  onLoadChildren?: (folderId: string) => Promise<MediaNode[]>,\n): LazyChildrenApi {\n  const [cache, setCache] = useState<Record<string, MediaNode[]>>({});\n  const [loadingIds, setLoadingIds] = useState<Set<string>>(() => new Set());\n  const [errorIds, setErrorIds] = useState<Record<string, string>>({});\n  const inflight = useRef<Set<string>>(new Set());\n\n  const ensureLoaded = useCallback(\n    (folderId: string) => {\n      if (!onLoadChildren) return;\n      if (cache[folderId] || inflight.current.has(folderId)) return;\n      inflight.current.add(folderId);\n      setLoadingIds((prev) => new Set(prev).add(folderId));\n      setErrorIds((prev) => {\n        if (!(folderId in prev)) return prev;\n        const next = { ...prev };\n        delete next[folderId];\n        return next;\n      });\n      onLoadChildren(folderId)\n        .then((children) => {\n          setCache((prev) => ({ ...prev, [folderId]: children }));\n        })\n        .catch((err: unknown) => {\n          setErrorIds((prev) => ({\n            ...prev,\n            [folderId]: (err as Error)?.message || \"Failed to load\",\n          }));\n        })\n        .finally(() => {\n          inflight.current.delete(folderId);\n          setLoadingIds((prev) => {\n            const next = new Set(prev);\n            next.delete(folderId);\n            return next;\n          });\n        });\n    },\n    [onLoadChildren, cache],\n  );\n\n  const invalidate = useCallback((folderId?: string | null) => {\n    if (folderId == null) {\n      setCache({});\n      setErrorIds({});\n      return;\n    }\n    setCache((prev) => {\n      const next = { ...prev };\n      delete next[folderId];\n      return next;\n    });\n    setErrorIds((prev) => {\n      const next = { ...prev };\n      delete next[folderId];\n      return next;\n    });\n  }, []);\n\n  const nodes = useMemo(() => mergeCache(rootNodes, cache), [rootNodes, cache]);\n\n  return { nodes, ensureLoaded, invalidate, loadingIds, errorIds };\n}\n",
      "type": "registry:component",
      "target": "components/media-library/hooks/use-lazy-children.ts"
    },
    {
      "path": "src/registry/components/media/media-library/hooks/use-media-library.ts",
      "content": "\"use client\";\n\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type RefObject,\n} from \"react\";\nimport {\n  DEFAULT_MEDIA_LIBRARY_LABELS,\n  type MediaFilterCategory,\n  type MediaLibraryLabels,\n  type MediaLibraryStorage,\n  type MediaLibraryPreviewMode,\n  type MediaLibraryRootProps,\n  type MediaNode,\n  type MediaUploadItem,\n} from \"../types\";\nimport {\n  computeFilterCounts,\n  nodeMatchesFilter,\n  type MediaFilterCounts,\n} from \"../lib/filter-category\";\nimport { buildParentMap, canDropInto, findNode } from \"../lib/drag\";\nimport { useControllableState } from \"./use-controllable-state\";\nimport { useLazyChildren } from \"./use-lazy-children\";\nimport { useSelection, type SelectionApi } from \"./use-selection\";\nimport { useUpload } from \"./use-upload\";\n\nexport interface MediaClipboard {\n  kind: \"cut\" | null;\n  ids: string[];\n}\n\nexport interface MediaLibraryContextValue {\n  labels: Required<MediaLibraryLabels>;\n\n  // current-folder view\n  nodes: MediaNode[];\n  currentFolderId: string | null;\n  currentFolder: MediaNode | null;\n  path: MediaNode[];\n  folders: MediaNode[];\n  files: MediaNode[];\n  filterCounts: MediaFilterCounts;\n  uploads: MediaUploadItem[];\n  loadingChildren: boolean;\n  loadError: string | null;\n\n  // ui state\n  filter: MediaFilterCategory;\n  setFilter: (f: MediaFilterCategory) => void;\n  searchQuery: string;\n  setSearchQuery: (q: string) => void;\n\n  storage?: MediaLibraryStorage;\n\n  // selection\n  selectedIds: Set<string>;\n  selection: SelectionApi;\n  visibleIds: string[];\n\n  // clipboard (cut → move on paste; copy/duplicate is not implemented)\n  clipboard: MediaClipboard;\n  cut: (ids: string[]) => void;\n  paste: () => void;\n\n  // navigation\n  navigateTo: (folderId: string | null) => void;\n  navigateUp: () => void;\n  refresh: (folderId?: string | null) => void;\n\n  // preview\n  previewId: string | null;\n  previewNode: MediaNode | null;\n  openPreview: (id: string) => void;\n  closePreview: () => void;\n  previewNext: () => void;\n  previewPrev: () => void;\n  previewHasNext: boolean;\n  previewHasPrev: boolean;\n  previewMode: MediaLibraryPreviewMode;\n  canPreview: boolean;\n  resolveTextContent?: (node: MediaNode) => Promise<string>;\n  pdfWorkerSrc?: string;\n\n  // mutations\n  triggerUpload: () => void;\n  uploadFiles: (files: File[], targetFolderId?: string | null) => void;\n  retryUpload: (tempId: string) => void;\n  dismissUpload: (tempId: string) => void;\n  moveTo: (ids: string[], targetFolderId: string | null) => void;\n  download: (ids: string[]) => void;\n  requestDelete: (ids: string[]) => void;\n  canDrop: (draggedIds: string[], targetFolderId: string | null) => boolean;\n\n  // inline rename\n  renamingId: string | null;\n  renameError: string | null;\n  startRename: (id: string) => void;\n  submitRename: (id: string, name: string) => void;\n  cancelRename: () => void;\n\n  // inline create-folder\n  creatingFolder: boolean;\n  startCreateFolder: () => void;\n  submitCreateFolder: (name: string) => void;\n  cancelCreateFolder: () => void;\n\n  // capability flags\n  can: {\n    upload: boolean;\n    move: boolean;\n    rename: boolean;\n    delete: boolean;\n    createFolder: boolean;\n    download: boolean;\n  };\n\n  // behaviour\n  enableInternalDrag: boolean;\n  enableUploadDrop: boolean;\n  enableContextMenu: boolean;\n  virtualizeThreshold: number;\n\n  // dnd transient\n  activeDragIds: string[] | null;\n  setActiveDragIds: (ids: string[] | null) => void;\n  /** True while OS files are dragged over the surface (drives the upload overlay). */\n  isDraggingFiles: boolean;\n  setDraggingFiles: (v: boolean) => void;\n\n  fileInputRef: RefObject<HTMLInputElement | null>;\n}\n\nconst MediaLibraryContext = createContext<MediaLibraryContextValue | null>(null);\n\nexport function useMediaLibrary(): MediaLibraryContextValue {\n  const ctx = useContext(MediaLibraryContext);\n  if (!ctx) {\n    throw new Error(\n      \"useMediaLibrary must be used within <MediaLibraryRoot> (or <MediaLibrary>).\",\n    );\n  }\n  return ctx;\n}\n\nexport { MediaLibraryContext };\n\nfunction sortByName(a: MediaNode, b: MediaNode): number {\n  return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: \"base\" });\n}\n\n/** Builds the full context value from the root props. */\nexport function useMediaLibraryStore(\n  props: MediaLibraryRootProps,\n): MediaLibraryContextValue {\n  const {\n    nodes: rootNodes,\n    onLoadChildren,\n    storage,\n    onUpload,\n    onMove,\n    onRename,\n    onDelete,\n    onCreateFolder,\n    onDownload,\n    validateRename,\n    resolveTextContent,\n    pdfWorkerSrc,\n    onPreviewOpen,\n    onPreviewClose,\n    enableUploadDrop = true,\n    enableInternalDrag = true,\n    enableContextMenu = true,\n    preview: previewMode = \"both\",\n    virtualizeThreshold = 120,\n    labels: labelOverrides,\n  } = props;\n  const canPreview = previewMode !== false;\n\n  const labels = useMemo(\n    () => ({ ...DEFAULT_MEDIA_LIBRARY_LABELS, ...labelOverrides }),\n    [labelOverrides],\n  );\n\n  // lazy-merged tree\n  const { nodes, ensureLoaded, invalidate, loadingIds, errorIds } = useLazyChildren(\n    rootNodes,\n    onLoadChildren,\n  );\n\n  // current folder (controllable)\n  const [currentFolderId, setCurrentFolderId] = useControllableState<string | null>({\n    value: props.currentFolderId,\n    defaultValue: props.defaultCurrentFolderId ?? null,\n    onChange: props.onCurrentFolderChange,\n    componentName: \"MediaLibrary\",\n    valuePropName: \"currentFolderId\",\n  });\n\n  // selection (controllable)\n  const [selectedIds, setSelectedIds] = useControllableState<Set<string>>({\n    value: props.selectedIds,\n    defaultValue: props.defaultSelectedIds ?? new Set<string>(),\n    onChange: props.onSelectedChange,\n    componentName: \"MediaLibrary\",\n    valuePropName: \"selectedIds\",\n  });\n  const selection = useSelection(selectedIds, setSelectedIds);\n\n  // ui state\n  const [filter, setFilter] = useState<MediaFilterCategory>(\"all\");\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const [previewId, setPreviewId] = useState<string | null>(null);\n  const [clipboard, setClipboard] = useState<MediaClipboard>({ kind: null, ids: [] });\n  const [renamingId, setRenamingId] = useState<string | null>(null);\n  const [renameError, setRenameError] = useState<string | null>(null);\n  const [creatingFolder, setCreatingFolder] = useState(false);\n  const [activeDragIds, setActiveDragIds] = useState<string[] | null>(null);\n  const [isDraggingFiles, setDraggingFiles] = useState(false);\n\n  const fileInputRef = useRef<HTMLInputElement | null>(null);\n  const parentMap = useMemo(() => buildParentMap(nodes), [nodes]);\n\n  // derive current folder + path\n  const currentFolder = useMemo(\n    () => (currentFolderId == null ? null : findNode(nodes, currentFolderId) ?? null),\n    [nodes, currentFolderId],\n  );\n\n  const path = useMemo(() => {\n    if (currentFolderId == null) return [] as MediaNode[];\n    const chain: MediaNode[] = [];\n    let cur: string | null | undefined = currentFolderId;\n    const seen = new Set<string>();\n    while (cur != null && !seen.has(cur)) {\n      seen.add(cur);\n      const node = findNode(nodes, cur);\n      if (!node) break;\n      chain.unshift(node);\n      cur = node.parentId ?? parentMap.get(cur) ?? null;\n    }\n    return chain;\n  }, [nodes, currentFolderId, parentMap]);\n\n  // children of the current folder\n  const currentChildren = useMemo<MediaNode[]>(() => {\n    if (currentFolderId == null) return nodes;\n    return currentFolder?.children ?? [];\n  }, [nodes, currentFolderId, currentFolder]);\n\n  const allFolders = useMemo(\n    () => currentChildren.filter((n) => n.type === \"folder\").sort(sortByName),\n    [currentChildren],\n  );\n  const allFiles = useMemo(\n    () => currentChildren.filter((n) => n.type === \"file\"),\n    [currentChildren],\n  );\n\n  const filterCounts = useMemo(() => computeFilterCounts(allFiles), [allFiles]);\n\n  const files = useMemo(() => {\n    const q = searchQuery.trim().toLowerCase();\n    return allFiles\n      .filter((n) => nodeMatchesFilter(n, filter))\n      .filter((n) => (q ? n.name.toLowerCase().includes(q) : true))\n      .sort(sortByName);\n  }, [allFiles, filter, searchQuery]);\n\n  const folders = useMemo(() => {\n    const q = searchQuery.trim().toLowerCase();\n    return q ? allFolders.filter((n) => n.name.toLowerCase().includes(q)) : allFolders;\n  }, [allFolders, searchQuery]);\n\n  const visibleIds = useMemo(\n    () => [...folders.map((n) => n.id), ...files.map((n) => n.id)],\n    [folders, files],\n  );\n\n  const loadingChildren =\n    currentFolderId != null && loadingIds.has(currentFolderId);\n  const loadError =\n    currentFolderId != null ? (errorIds[currentFolderId] ?? null) : null;\n\n  // ensure current folder children are loaded\n  useEffect(() => {\n    if (currentFolderId != null) ensureLoaded(currentFolderId);\n  }, [currentFolderId, ensureLoaded]);\n\n  // upload\n  const onUploadResolved = useCallback(\n    (_nodes: MediaNode[], targetFolderId: string | null) => {\n      if (targetFolderId != null) invalidate(targetFolderId);\n    },\n    [invalidate],\n  );\n  const upload = useUpload(onUpload, onUploadResolved);\n\n  const uploads = useMemo(\n    () => upload.uploads.filter((u) => u.targetFolderId === currentFolderId),\n    [upload.uploads, currentFolderId],\n  );\n\n  // navigation\n  const navigateTo = useCallback(\n    (folderId: string | null) => {\n      setCurrentFolderId(folderId);\n      setSearchQuery(\"\");\n      setFilter(\"all\");\n      selection.clear();\n      if (folderId != null) ensureLoaded(folderId);\n    },\n    [setCurrentFolderId, ensureLoaded, selection],\n  );\n\n  const navigateUp = useCallback(() => {\n    if (currentFolderId == null) return;\n    const parent = currentFolder?.parentId ?? parentMap.get(currentFolderId) ?? null;\n    navigateTo(parent);\n  }, [currentFolderId, currentFolder, parentMap, navigateTo]);\n\n  const refresh = useCallback(\n    (folderId?: string | null) => {\n      const target = folderId === undefined ? currentFolderId : folderId;\n      invalidate(target);\n      if (target != null) ensureLoaded(target);\n    },\n    [currentFolderId, invalidate, ensureLoaded],\n  );\n\n  // preview\n  const previewNode = useMemo(\n    () => (previewId == null ? null : findNode(nodes, previewId) ?? null),\n    [nodes, previewId],\n  );\n  const previewIndex = previewId == null ? -1 : files.findIndex((f) => f.id === previewId);\n  const previewHasNext = previewIndex !== -1 && previewIndex < files.length - 1;\n  const previewHasPrev = previewIndex > 0;\n\n  const openPreview = useCallback(\n    (id: string) => {\n      setPreviewId(id);\n      onPreviewOpen?.(id);\n    },\n    [onPreviewOpen],\n  );\n  const closePreview = useCallback(() => {\n    setPreviewId(null);\n    onPreviewClose?.();\n  }, [onPreviewClose]);\n  const previewNext = useCallback(() => {\n    if (previewHasNext) openPreview(files[previewIndex + 1].id);\n  }, [previewHasNext, files, previewIndex, openPreview]);\n  const previewPrev = useCallback(() => {\n    if (previewHasPrev) openPreview(files[previewIndex - 1].id);\n  }, [previewHasPrev, files, previewIndex, openPreview]);\n\n  // mutations\n  const triggerUpload = useCallback(() => fileInputRef.current?.click(), []);\n  const uploadFiles = useCallback(\n    (files_: File[], targetFolderId?: string | null) =>\n      upload.start(files_, targetFolderId === undefined ? currentFolderId : targetFolderId),\n    [upload, currentFolderId],\n  );\n\n  const moveTo = useCallback(\n    (ids: string[], targetFolderId: string | null) => {\n      if (!onMove || ids.length === 0) return;\n      if (!canDropInto(ids, targetFolderId, nodes, parentMap)) return;\n      onMove(ids, targetFolderId);\n      selection.clear();\n      if (targetFolderId != null) invalidate(targetFolderId);\n      if (currentFolderId != null) invalidate(currentFolderId);\n    },\n    [onMove, nodes, parentMap, selection, invalidate, currentFolderId],\n  );\n\n  const download = useCallback(\n    (ids: string[]) => onDownload?.(ids),\n    [onDownload],\n  );\n\n  const requestDelete = useCallback(\n    (ids: string[]) => {\n      if (!onDelete || ids.length === 0) return;\n      onDelete(ids);\n      selection.clear();\n      if (currentFolderId != null) invalidate(currentFolderId);\n    },\n    [onDelete, selection, currentFolderId, invalidate],\n  );\n\n  const canDrop = useCallback(\n    (draggedIds: string[], targetFolderId: string | null) =>\n      canDropInto(draggedIds, targetFolderId, nodes, parentMap),\n    [nodes, parentMap],\n  );\n\n  // clipboard\n  const cut = useCallback((ids: string[]) => {\n    if (ids.length > 0) setClipboard({ kind: \"cut\", ids });\n  }, []);\n  const paste = useCallback(() => {\n    if (clipboard.kind === \"cut\" && clipboard.ids.length > 0) {\n      moveTo(clipboard.ids, currentFolderId);\n      setClipboard({ kind: null, ids: [] });\n    }\n  }, [clipboard, moveTo, currentFolderId]);\n\n  // inline rename\n  const startRename = useCallback((id: string) => {\n    setRenameError(null);\n    setRenamingId(id);\n  }, []);\n  const cancelRename = useCallback(() => {\n    setRenamingId(null);\n    setRenameError(null);\n  }, []);\n  const submitRename = useCallback(\n    (id: string, name: string) => {\n      const trimmed = name.trim();\n      if (!trimmed) {\n        cancelRename();\n        return;\n      }\n      const err = validateRename?.(id, trimmed) ?? null;\n      if (err) {\n        setRenameError(err);\n        return;\n      }\n      onRename?.(id, trimmed);\n      setRenamingId(null);\n      setRenameError(null);\n      if (currentFolderId != null) invalidate(currentFolderId);\n    },\n    [validateRename, onRename, cancelRename, currentFolderId, invalidate],\n  );\n\n  // inline create-folder\n  const startCreateFolder = useCallback(() => setCreatingFolder(true), []);\n  const cancelCreateFolder = useCallback(() => setCreatingFolder(false), []);\n  const submitCreateFolder = useCallback(\n    (name: string) => {\n      const trimmed = name.trim();\n      if (trimmed) onCreateFolder?.(currentFolderId, trimmed);\n      setCreatingFolder(false);\n      if (currentFolderId != null) invalidate(currentFolderId);\n    },\n    [onCreateFolder, currentFolderId, invalidate],\n  );\n\n  const can = useMemo(\n    () => ({\n      upload: Boolean(onUpload),\n      move: Boolean(onMove),\n      rename: Boolean(onRename),\n      delete: Boolean(onDelete),\n      createFolder: Boolean(onCreateFolder),\n      download: Boolean(onDownload),\n    }),\n    [onUpload, onMove, onRename, onDelete, onCreateFolder, onDownload],\n  );\n\n  return useMemo<MediaLibraryContextValue>(\n    () => ({\n      labels,\n      nodes,\n      currentFolderId,\n      currentFolder,\n      path,\n      folders,\n      files,\n      filterCounts,\n      uploads,\n      loadingChildren,\n      loadError,\n      filter,\n      setFilter,\n      searchQuery,\n      setSearchQuery,\n      storage,\n      selectedIds,\n      selection,\n      visibleIds,\n      clipboard,\n      cut,\n      paste,\n      navigateTo,\n      navigateUp,\n      refresh,\n      previewId,\n      previewNode,\n      openPreview,\n      closePreview,\n      previewNext,\n      previewPrev,\n      previewHasNext,\n      previewHasPrev,\n      previewMode,\n      canPreview,\n      resolveTextContent,\n      pdfWorkerSrc,\n      triggerUpload,\n      uploadFiles,\n      retryUpload: upload.retry,\n      dismissUpload: upload.dismiss,\n      moveTo,\n      download,\n      requestDelete,\n      canDrop,\n      renamingId,\n      renameError,\n      startRename,\n      submitRename,\n      cancelRename,\n      creatingFolder,\n      startCreateFolder,\n      submitCreateFolder,\n      cancelCreateFolder,\n      can,\n      enableInternalDrag,\n      enableUploadDrop,\n      enableContextMenu,\n      virtualizeThreshold,\n      activeDragIds,\n      setActiveDragIds,\n      isDraggingFiles,\n      setDraggingFiles,\n      fileInputRef,\n    }),\n    [\n      labels, nodes, currentFolderId, currentFolder, path, folders, files,\n      filterCounts, uploads, loadingChildren, loadError, filter, searchQuery,\n      storage, selectedIds, selection, visibleIds, clipboard, cut, paste,\n      navigateTo, navigateUp, refresh, previewId, previewNode, openPreview, closePreview,\n      previewNext, previewPrev, previewHasNext, previewHasPrev, previewMode, canPreview, resolveTextContent, pdfWorkerSrc,\n      triggerUpload, uploadFiles, upload.retry, upload.dismiss, moveTo, download,\n      requestDelete, canDrop, renamingId, renameError, startRename, submitRename,\n      cancelRename, creatingFolder, startCreateFolder, submitCreateFolder,\n      cancelCreateFolder, can, enableInternalDrag, enableUploadDrop,\n      enableContextMenu, virtualizeThreshold, activeDragIds, isDraggingFiles,\n    ],\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/hooks/use-media-library.ts"
    },
    {
      "path": "src/registry/components/media/media-library/hooks/use-selection.ts",
      "content": "import { useCallback, useRef } from \"react\";\n\nexport interface ClickModifiers {\n  metaKey?: boolean;\n  ctrlKey?: boolean;\n  shiftKey?: boolean;\n}\n\nexport interface SelectionApi {\n  isSelected: (id: string) => boolean;\n  /** Modifier-aware click: plain = replace, ⌘/Ctrl = toggle, Shift = range. */\n  handleItemClick: (id: string, mods: ClickModifiers, orderedIds: string[]) => void;\n  selectOne: (id: string) => void;\n  toggle: (id: string) => void;\n  selectAll: (ids: string[]) => void;\n  clear: () => void;\n}\n\n/**\n * Selection logic over a controllable `Set<string>`. Owns the shift-range\n * anchor. (file-manager's `use-selection` is private — this is a focused,\n * self-contained re-implementation per the GATE-2 Q5 decision.)\n */\nexport function useSelection(\n  selected: Set<string>,\n  setSelected: (ids: Set<string>) => void,\n): SelectionApi {\n  const anchorRef = useRef<string | null>(null);\n\n  const isSelected = useCallback((id: string) => selected.has(id), [selected]);\n\n  const selectOne = useCallback(\n    (id: string) => {\n      anchorRef.current = id;\n      setSelected(new Set([id]));\n    },\n    [setSelected],\n  );\n\n  const toggle = useCallback(\n    (id: string) => {\n      const next = new Set(selected);\n      if (next.has(id)) next.delete(id);\n      else next.add(id);\n      anchorRef.current = id;\n      setSelected(next);\n    },\n    [selected, setSelected],\n  );\n\n  const handleItemClick = useCallback(\n    (id: string, mods: ClickModifiers, orderedIds: string[]) => {\n      if (mods.shiftKey && anchorRef.current) {\n        const a = orderedIds.indexOf(anchorRef.current);\n        const b = orderedIds.indexOf(id);\n        if (a !== -1 && b !== -1) {\n          const [lo, hi] = a < b ? [a, b] : [b, a];\n          setSelected(new Set(orderedIds.slice(lo, hi + 1)));\n          return;\n        }\n      }\n      if (mods.metaKey || mods.ctrlKey) {\n        toggle(id);\n        return;\n      }\n      selectOne(id);\n    },\n    [selectOne, toggle, setSelected],\n  );\n\n  const selectAll = useCallback(\n    (ids: string[]) => setSelected(new Set(ids)),\n    [setSelected],\n  );\n\n  const clear = useCallback(() => {\n    anchorRef.current = null;\n    setSelected(new Set());\n  }, [setSelected]);\n\n  return { isSelected, handleItemClick, selectOne, toggle, selectAll, clear };\n}\n",
      "type": "registry:component",
      "target": "components/media-library/hooks/use-selection.ts"
    },
    {
      "path": "src/registry/components/media/media-library/hooks/use-text-content.ts",
      "content": "import { useEffect, useState } from \"react\";\nimport type { MediaNode } from \"../types\";\n\nexport interface TextContentState {\n  text: string | null;\n  loading: boolean;\n  error: string | null;\n}\n\n/**\n * Fetches text for a text-based file (code/json/txt/markdown). Default path is\n * `fetch(node.url).text()`; a `resolve` override handles auth / signed URLs /\n * transforms. Aborts on node change or unmount.\n */\nexport function useTextContent(\n  node: MediaNode | null,\n  enabled: boolean,\n  resolve?: (node: MediaNode) => Promise<string>,\n): TextContentState {\n  const [state, setState] = useState<TextContentState>({\n    text: null,\n    loading: false,\n    error: null,\n  });\n\n  // Identity-stable cache keys so an unchanged node doesn't refetch on re-render.\n  const nodeId = node?.id ?? null;\n  const url = node?.url ?? null;\n\n  useEffect(() => {\n    let cancelled = false;\n    const controller = new AbortController();\n\n    // All setState calls happen after this microtask yield, so none run\n    // synchronously within the effect (avoids cascading-render lint + flashes).\n    const run = async () => {\n      await Promise.resolve();\n      if (cancelled) return;\n      if (!enabled || !node) {\n        setState({ text: null, loading: false, error: null });\n        return;\n      }\n      setState({ text: null, loading: true, error: null });\n      try {\n        let text: string;\n        if (resolve) {\n          text = await resolve(node);\n        } else {\n          if (!node.url) throw new Error(\"No URL to fetch\");\n          const res = await fetch(node.url, { signal: controller.signal });\n          if (!res.ok) throw new Error(`HTTP ${res.status}`);\n          text = await res.text();\n        }\n        if (!cancelled) setState({ text, loading: false, error: null });\n      } catch (err) {\n        if (cancelled) return;\n        if ((err as Error).name === \"AbortError\") return;\n        setState({\n          text: null,\n          loading: false,\n          error: (err as Error).message || \"Failed to load\",\n        });\n      }\n    };\n\n    void run();\n    return () => {\n      cancelled = true;\n      controller.abort();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [nodeId, url, enabled, resolve]);\n\n  return state;\n}\n",
      "type": "registry:component",
      "target": "components/media-library/hooks/use-text-content.ts"
    },
    {
      "path": "src/registry/components/media/media-library/hooks/use-upload.ts",
      "content": "import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { resolvePreviewKind } from \"../lib/preview-kind\";\nimport type { MediaNode, MediaUploadItem, MediaLibraryRootProps } from \"../types\";\n\nlet uploadSeq = 0;\nfunction nextTempId(): string {\n  uploadSeq += 1;\n  const rand =\n    typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n      ? crypto.randomUUID()\n      : String(uploadSeq);\n  return `upl-${uploadSeq}-${rand}`;\n}\n\nexport interface UploadApi {\n  uploads: MediaUploadItem[];\n  start: (files: File[], targetFolderId: string | null) => void;\n  retry: (tempId: string) => void;\n  dismiss: (tempId: string) => void;\n}\n\n/**\n * Optimistic upload orchestration. Invokes `onUpload` once per file, shows a\n * placeholder item with a live progress ring, and on resolve hands the real\n * `MediaNode[]` to the store (which is the source of truth) while removing the\n * placeholder. Owns + revokes the object URLs it mints for local previews.\n */\nexport function useUpload(\n  onUpload: MediaLibraryRootProps[\"onUpload\"],\n  onResolved: (nodes: MediaNode[], targetFolderId: string | null) => void,\n): UploadApi {\n  const [uploads, setUploads] = useState<MediaUploadItem[]>([]);\n  const ownedUrls = useRef<Set<string>>(new Set());\n  const fileMap = useRef<Map<string, { file: File; targetFolderId: string | null }>>(\n    new Map(),\n  );\n  const onResolvedRef = useRef(onResolved);\n  useEffect(() => {\n    onResolvedRef.current = onResolved;\n  });\n\n  const revoke = useCallback((url?: string) => {\n    if (url && ownedUrls.current.has(url)) {\n      URL.revokeObjectURL(url);\n      ownedUrls.current.delete(url);\n    }\n  }, []);\n\n  const runOne = useCallback(\n    (tempId: string, file: File, targetFolderId: string | null) => {\n      if (!onUpload) return;\n      const progress = (pct: number) =>\n        setUploads((prev) =>\n          prev.map((u) =>\n            u.tempId === tempId\n              ? { ...u, pct: Math.max(0, Math.min(100, Math.round(pct))) }\n              : u,\n          ),\n        );\n      onUpload([file], targetFolderId, progress)\n        .then((nodes) => {\n          setUploads((prev) => {\n            const item = prev.find((u) => u.tempId === tempId);\n            revoke(item?.previewUrl);\n            return prev.filter((u) => u.tempId !== tempId);\n          });\n          fileMap.current.delete(tempId);\n          onResolvedRef.current(nodes, targetFolderId);\n        })\n        .catch((err: unknown) => {\n          setUploads((prev) =>\n            prev.map((u) =>\n              u.tempId === tempId\n                ? {\n                    ...u,\n                    status: \"error\",\n                    error: (err as Error)?.message || \"Upload failed\",\n                  }\n                : u,\n            ),\n          );\n        });\n    },\n    [onUpload, revoke],\n  );\n\n  const start = useCallback(\n    (files: File[], targetFolderId: string | null) => {\n      if (!onUpload || files.length === 0) return;\n      const items: MediaUploadItem[] = files.map((file) => {\n        const tempId = nextTempId();\n        const kind = resolvePreviewKind({\n          id: tempId,\n          name: file.name,\n          type: \"file\",\n          mimeType: file.type,\n        } as MediaNode);\n        let previewUrl: string | undefined;\n        if (file.type.startsWith(\"image/\")) {\n          previewUrl = URL.createObjectURL(file);\n          ownedUrls.current.add(previewUrl);\n        }\n        fileMap.current.set(tempId, { file, targetFolderId });\n        return {\n          tempId,\n          name: file.name,\n          kind,\n          pct: 0,\n          status: \"uploading\" as const,\n          previewUrl,\n          targetFolderId,\n        };\n      });\n      setUploads((prev) => [...prev, ...items]);\n      items.forEach((item) => runOne(item.tempId, fileMap.current.get(item.tempId)!.file, item.targetFolderId));\n    },\n    [onUpload, runOne],\n  );\n\n  const retry = useCallback(\n    (tempId: string) => {\n      const entry = fileMap.current.get(tempId);\n      if (!entry) return;\n      setUploads((prev) =>\n        prev.map((u) =>\n          u.tempId === tempId ? { ...u, status: \"uploading\", pct: 0, error: undefined } : u,\n        ),\n      );\n      runOne(tempId, entry.file, entry.targetFolderId);\n    },\n    [runOne],\n  );\n\n  const dismiss = useCallback(\n    (tempId: string) => {\n      setUploads((prev) => {\n        const item = prev.find((u) => u.tempId === tempId);\n        revoke(item?.previewUrl);\n        return prev.filter((u) => u.tempId !== tempId);\n      });\n      fileMap.current.delete(tempId);\n    },\n    [revoke],\n  );\n\n  // Revoke any outstanding owned URLs on unmount.\n  useEffect(() => {\n    const owned = ownedUrls.current;\n    return () => {\n      owned.forEach((url) => URL.revokeObjectURL(url));\n      owned.clear();\n    };\n  }, []);\n\n  return { uploads, start, retry, dismiss };\n}\n",
      "type": "registry:component",
      "target": "components/media-library/hooks/use-upload.ts"
    },
    {
      "path": "src/registry/components/media/media-library/lib/drag.ts",
      "content": "import type { MediaNode } from \"../types\";\n\n/** Payload carried by a card drag (one or more selected node ids). */\nexport interface MediaDragData {\n  type: \"media-node\";\n  ids: string[];\n}\n\n/** Build a `parentId` lookup by walking the (possibly partial) tree. */\nexport function buildParentMap(nodes: MediaNode[]): Map<string, string | null> {\n  const map = new Map<string, string | null>();\n  const walk = (list: MediaNode[], parentId: string | null) => {\n    for (const n of list) {\n      map.set(n.id, n.parentId ?? parentId);\n      if (n.children) walk(n.children, n.id);\n    }\n  };\n  walk(nodes, null);\n  return map;\n}\n\n/** Is `maybeAncestorId` an ancestor of (or equal to) `nodeId`, per the parent map? */\nexport function isAncestor(\n  parentMap: Map<string, string | null>,\n  maybeAncestorId: string,\n  nodeId: string,\n): boolean {\n  let cur: string | null | undefined = nodeId;\n  const seen = new Set<string>();\n  while (cur != null) {\n    if (cur === maybeAncestorId) return true;\n    if (seen.has(cur)) break; // cycle guard\n    seen.add(cur);\n    cur = parentMap.get(cur);\n  }\n  return false;\n}\n\n/**\n * Can `draggedIds` drop into `targetFolderId` (null = root)?\n * Rejects: target not a folder; dropping a node onto itself; dropping a folder\n * into its own subtree; a pure no-op (already the current parent of all).\n */\nexport function canDropInto(\n  draggedIds: string[],\n  targetFolderId: string | null,\n  nodes: MediaNode[],\n  parentMap: Map<string, string | null> = buildParentMap(nodes),\n): boolean {\n  if (draggedIds.length === 0) return false;\n\n  if (targetFolderId !== null) {\n    const target = findNode(nodes, targetFolderId);\n    if (!target || target.type !== \"folder\") return false;\n    // can't drop a node onto itself, or a folder into its own descendant\n    for (const id of draggedIds) {\n      if (id === targetFolderId) return false;\n      if (isAncestor(parentMap, id, targetFolderId)) return false;\n    }\n  }\n\n  // reject a pure no-op (every dragged node already lives in the target)\n  const allAlreadyHere = draggedIds.every((id) => (parentMap.get(id) ?? null) === targetFolderId);\n  if (allAlreadyHere) return false;\n\n  return true;\n}\n\n/** Depth-first find by id across the (partial) tree. */\nexport function findNode(nodes: MediaNode[], id: string): MediaNode | undefined {\n  for (const n of nodes) {\n    if (n.id === id) return n;\n    if (n.children) {\n      const hit = findNode(n.children, id);\n      if (hit) return hit;\n    }\n  }\n  return undefined;\n}\n",
      "type": "registry:component",
      "target": "components/media-library/lib/drag.ts"
    },
    {
      "path": "src/registry/components/media/media-library/lib/filter-category.ts",
      "content": "import type { MediaFilterCategory, MediaNode, MediaPreviewKind } from \"../types\";\nimport { resolvePreviewKind } from \"./preview-kind\";\n\n/**\n * Map a preview kind to its type-filter chip (Q4 taxonomy):\n * images / video / pdfs / docs (= code, json, text, markdown + unknown/office).\n */\nexport function filterCategoryForKind(kind: MediaPreviewKind): Exclude<MediaFilterCategory, \"all\"> {\n  switch (kind) {\n    case \"image\":\n      return \"images\";\n    case \"video\":\n      return \"video\";\n    case \"pdf\":\n      return \"pdfs\";\n    default:\n      // code | json | text | markdown | unknown (office docs, archives, …)\n      return \"docs\";\n  }\n}\n\nexport function filterCategoryForNode(node: MediaNode): Exclude<MediaFilterCategory, \"all\"> {\n  return filterCategoryForKind(resolvePreviewKind(node));\n}\n\n/** Does a file node match the active chip? Folders are never filtered. */\nexport function nodeMatchesFilter(node: MediaNode, filter: MediaFilterCategory): boolean {\n  if (node.type === \"folder\") return true;\n  if (filter === \"all\") return true;\n  return filterCategoryForNode(node) === filter;\n}\n\nexport type MediaFilterCounts = Record<MediaFilterCategory, number>;\n\n/** Count files per chip for the given (current-folder) file list. */\nexport function computeFilterCounts(files: MediaNode[]): MediaFilterCounts {\n  const counts: MediaFilterCounts = {\n    all: 0,\n    images: 0,\n    video: 0,\n    pdfs: 0,\n    docs: 0,\n  };\n  for (const node of files) {\n    if (node.type === \"folder\") continue;\n    counts.all += 1;\n    counts[filterCategoryForNode(node)] += 1;\n  }\n  return counts;\n}\n\nexport const FILTER_ORDER: MediaFilterCategory[] = [\n  \"all\",\n  \"images\",\n  \"video\",\n  \"pdfs\",\n  \"docs\",\n];\n",
      "type": "registry:component",
      "target": "components/media-library/lib/filter-category.ts"
    },
    {
      "path": "src/registry/components/media/media-library/lib/format.ts",
      "content": "import { format } from \"date-fns\";\n\n/** Humanize a byte count → \"12.8 GB\", \"512 KB\", \"0 B\". */\nexport function formatBytes(bytes: number, decimals = 1): string {\n  if (!Number.isFinite(bytes) || bytes <= 0) return \"0 B\";\n  const units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\"];\n  const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);\n  const value = bytes / Math.pow(1024, i);\n  // whole bytes need no decimals; larger units get up to `decimals`\n  const str = i === 0 ? String(Math.round(value)) : value.toFixed(decimals).replace(/\\.0+$/, \"\");\n  return `${str} ${units[i]}`;\n}\n\n/**\n * Compact relative time → \"2h ago\", \"3d ago\", \"1w ago\"; older than ~5 weeks\n * falls back to an absolute \"Mar 24\" (date-fns). Returns \"\" for missing input.\n */\nexport function formatRelativeTime(iso?: string): string {\n  if (!iso) return \"\";\n  const then = new Date(iso);\n  const ms = then.getTime();\n  if (Number.isNaN(ms)) return \"\";\n  const diff = Date.now() - ms;\n  if (diff < 0) return \"just now\";\n\n  const sec = diff / 1000;\n  const min = sec / 60;\n  const hr = min / 60;\n  const day = hr / 24;\n  const week = day / 7;\n\n  if (min < 1) return \"just now\";\n  if (min < 60) return `${Math.floor(min)}m ago`;\n  if (hr < 24) return `${Math.floor(hr)}h ago`;\n  if (day < 7) return `${Math.floor(day)}d ago`;\n  if (week < 5) return `${Math.floor(week)}w ago`;\n  return format(then, \"MMM d\");\n}\n\n/** Fill a `{token}` template, e.g. tmpl(\"{used} of {total}\", {used:\"a\",total:\"b\"}). */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n  return template.replace(/\\{(\\w+)\\}/g, (_, key: string) =>\n    key in values ? String(values[key]) : `{${key}}`,\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/lib/format.ts"
    },
    {
      "path": "src/registry/components/media/media-library/lib/preview-kind.ts",
      "content": "import type { MediaNode, MediaPreviewKind } from \"../types\";\n\n/** Extension → preview kind. Lower-case, no leading dot. */\nconst EXT_TO_KIND: Record<string, MediaPreviewKind> = {\n  // image\n  jpg: \"image\", jpeg: \"image\", png: \"image\", gif: \"image\", webp: \"image\",\n  avif: \"image\", svg: \"image\", bmp: \"image\", ico: \"image\", heic: \"image\",\n  // video\n  mp4: \"video\", webm: \"video\", ogv: \"video\", mov: \"video\", m4v: \"video\",\n  mkv: \"video\", avi: \"video\",\n  // pdf\n  pdf: \"pdf\",\n  // json\n  json: \"json\", jsonc: \"json\", geojson: \"json\",\n  // markdown\n  md: \"markdown\", markdown: \"markdown\", mdx: \"markdown\",\n  // code\n  ts: \"code\", tsx: \"code\", js: \"code\", jsx: \"code\", mjs: \"code\", cjs: \"code\",\n  html: \"code\", htm: \"code\", css: \"code\", scss: \"code\", less: \"code\",\n  py: \"code\", rb: \"code\", go: \"code\", rs: \"code\", java: \"code\", php: \"code\",\n  swift: \"code\", kt: \"code\", c: \"code\", h: \"code\", cpp: \"code\", cc: \"code\",\n  cs: \"code\", sh: \"code\", bash: \"code\", zsh: \"code\", yaml: \"code\", yml: \"code\",\n  toml: \"code\", ini: \"code\", sql: \"code\", xml: \"code\", graphql: \"code\",\n  vue: \"code\", svelte: \"code\", dockerfile: \"code\",\n  // plain text\n  txt: \"text\", log: \"text\", csv: \"text\", tsv: \"text\", env: \"text\",\n};\n\n/** Extract the lower-cased extension from `node.ext` or the file name. */\nexport function extOf(node: { name: string; ext?: string }): string {\n  if (node.ext) return node.ext.replace(/^\\./, \"\").toLowerCase();\n  const dot = node.name.lastIndexOf(\".\");\n  return dot > 0 ? node.name.slice(dot + 1).toLowerCase() : \"\";\n}\n\n/**\n * Resolve which viewer a file maps to: `mimeType` is authoritative for the\n * broad buckets, `ext` refines/falls back, else `\"unknown\"` (download-only).\n */\nexport function resolvePreviewKind(node: MediaNode): MediaPreviewKind {\n  if (node.type === \"folder\") return \"unknown\";\n  const mime = node.mimeType?.toLowerCase();\n  const ext = extOf(node);\n\n  if (mime) {\n    if (mime.startsWith(\"image/\")) return \"image\";\n    if (mime.startsWith(\"video/\")) return \"video\";\n    if (mime === \"application/pdf\") return \"pdf\";\n    if (mime === \"application/json\") return \"json\";\n    if (mime === \"text/markdown\" || mime === \"text/x-markdown\") return \"markdown\";\n    if (\n      mime === \"text/html\" ||\n      mime === \"text/css\" ||\n      mime === \"text/javascript\" ||\n      mime === \"application/javascript\" ||\n      mime === \"application/typescript\" ||\n      mime === \"application/xml\" ||\n      mime === \"text/xml\"\n    ) {\n      return \"code\";\n    }\n    if (mime === \"text/plain\" || mime === \"text/csv\") {\n      // refine with ext when present (e.g. a `.ts` served as text/plain)\n      return EXT_TO_KIND[ext] ?? \"text\";\n    }\n  }\n\n  return EXT_TO_KIND[ext] ?? \"unknown\";\n}\n\n/** Text-based kinds the lib fetches content for. */\nexport function isTextKind(kind: MediaPreviewKind): boolean {\n  return kind === \"code\" || kind === \"json\" || kind === \"text\" || kind === \"markdown\";\n}\n",
      "type": "registry:component",
      "target": "components/media-library/lib/preview-kind.ts"
    },
    {
      "path": "src/registry/components/media/media-library/parts/breadcrumbs.tsx",
      "content": "\"use client\";\n\nimport { ChevronRight } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { useMediaLibrary } from \"../hooks/use-media-library\";\n\n/** Tier B — breadcrumb path bar. Walks the current folder's `parentId` chain. */\nexport function MediaLibraryBreadcrumbs({ className }: { className?: string }) {\n  const { path, navigateTo, labels } = useMediaLibrary();\n\n  return (\n    <nav\n      aria-label=\"Breadcrumb\"\n      className={cn(\"flex flex-wrap items-center gap-0.5 text-sm\", className)}\n    >\n      <button\n        type=\"button\"\n        onClick={() => navigateTo(null)}\n        aria-current={path.length === 0 ? \"page\" : undefined}\n        className={cn(\n          \"rounded px-1.5 py-0.5 font-medium hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          path.length === 0 ? \"text-foreground\" : \"text-muted-foreground\",\n        )}\n      >\n        {labels.libraryHeading}\n      </button>\n      {path.map((node, i) => {\n        const isLast = i === path.length - 1;\n        return (\n          <span key={node.id} className=\"flex items-center gap-0.5\">\n            <ChevronRight\n              className=\"size-3.5 shrink-0 text-muted-foreground/50\"\n              aria-hidden=\"true\"\n            />\n            <button\n              type=\"button\"\n              onClick={() => navigateTo(node.id)}\n              aria-current={isLast ? \"page\" : undefined}\n              className={cn(\n                \"max-w-[12rem] truncate rounded px-1.5 py-0.5 hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n                isLast ? \"font-medium text-foreground\" : \"text-muted-foreground\",\n              )}\n            >\n              {node.name}\n            </button>\n          </span>\n        );\n      })}\n    </nav>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/parts/breadcrumbs.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/context-menu.tsx",
      "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuSeparator,\n  ContextMenuTrigger,\n} from \"@/components/ui/context-menu\";\nimport { useMediaLibrary } from \"../hooks/use-media-library\";\nimport type { MediaNode } from \"../types\";\n\n/**\n * Wraps a card with a selection-aware right-click menu. When the node is part\n * of a multi-selection the verbs act on the whole selection; otherwise on the\n * node alone. Returns children untouched when `enableContextMenu` is false.\n */\nexport function MediaItemContextMenu({\n  node,\n  children,\n}: {\n  node: MediaNode;\n  children: ReactNode;\n}) {\n  const ctx = useMediaLibrary();\n  if (!ctx.enableContextMenu) return <>{children}</>;\n\n  const { labels, can, clipboard } = ctx;\n  const ids =\n    ctx.selectedIds.has(node.id) && ctx.selectedIds.size > 1\n      ? Array.from(ctx.selectedIds)\n      : [node.id];\n  const isFile = node.type === \"file\";\n  const isFolder = node.type === \"folder\";\n  const canPasteHere = can.move && isFolder && clipboard.kind === \"cut\" && clipboard.ids.length > 0;\n\n  return (\n    <ContextMenu>\n      {/* F-cross-13: no `asChild` — trigger is a display:contents wrapper;\n          right-click bubbles up from the card in both backends. */}\n      <ContextMenuTrigger className=\"contents\">{children}</ContextMenuTrigger>\n      <ContextMenuContent className=\"w-44\">\n        {isFolder ? (\n          <ContextMenuItem onSelect={() => ctx.navigateTo(node.id)}>\n            {labels.open}\n          </ContextMenuItem>\n        ) : (\n          <ContextMenuItem onSelect={() => ctx.openPreview(node.id)}>\n            {labels.preview}\n          </ContextMenuItem>\n        )}\n        {can.rename ? (\n          <ContextMenuItem onSelect={() => ctx.startRename(node.id)}>\n            {labels.rename}\n          </ContextMenuItem>\n        ) : null}\n        {can.move ? (\n          <ContextMenuItem onSelect={() => ctx.cut(ids)}>\n            {labels.cut}\n          </ContextMenuItem>\n        ) : null}\n        {canPasteHere ? (\n          <ContextMenuItem onSelect={() => ctx.moveTo(clipboard.ids, node.id)}>\n            {labels.paste}\n          </ContextMenuItem>\n        ) : null}\n        {can.download && isFile ? (\n          <ContextMenuItem onSelect={() => ctx.download(ids)}>\n            {labels.download}\n          </ContextMenuItem>\n        ) : null}\n        {can.delete ? (\n          <>\n            <ContextMenuSeparator />\n            {/* F-cross-13: style destructive via className, not a `variant`\n                prop — consumer primitives (base-nova / Base UI) may lack it. */}\n            <ContextMenuItem\n              className=\"text-destructive focus:bg-destructive/10 focus:text-destructive\"\n              onSelect={() => ctx.requestDelete(ids)}\n            >\n              {labels.delete}\n            </ContextMenuItem>\n          </>\n        ) : null}\n      </ContextMenuContent>\n    </ContextMenu>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/parts/context-menu.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/details-pane.tsx",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { Download, Maximize2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { formatBytes, formatRelativeTime } from \"../lib/format\";\nimport { useMediaLibrary } from \"../hooks/use-media-library\";\nimport { FilePreview } from \"./file-preview\";\n\n/** Tier B — right-side details pane showing the single selected file. */\nexport function MediaLibraryDetailsPane({ className }: { className?: string }) {\n  const ctx = useMediaLibrary();\n  const { selectedIds, files, labels, resolveTextContent, can } = ctx;\n\n  const node = useMemo(() => {\n    if (selectedIds.size !== 1) return null;\n    const [id] = Array.from(selectedIds);\n    return files.find((f) => f.id === id) ?? null;\n  }, [selectedIds, files]);\n\n  if (!node) {\n    return (\n      <aside\n        className={cn(\n          \"hidden w-72 shrink-0 flex-col rounded-xl border border-border bg-card p-4 text-center lg:flex\",\n          className,\n        )}\n      >\n        <p className=\"m-auto text-sm text-muted-foreground\">{labels.preview}</p>\n      </aside>\n    );\n  }\n\n  const meta: Array<[string, string]> = [];\n  if (node.width && node.height) meta.push([\"Dimensions\", `${node.width} × ${node.height}`]);\n  if (typeof node.size === \"number\") meta.push([\"Size\", formatBytes(node.size)]);\n  if (node.modifiedAt) meta.push([\"Modified\", formatRelativeTime(node.modifiedAt)]);\n  if (node.owner) meta.push([\"Owner\", node.owner]);\n\n  return (\n    <aside\n      className={cn(\n        \"hidden w-72 shrink-0 flex-col gap-3 rounded-xl border border-border bg-card p-3 lg:flex\",\n        className,\n      )}\n    >\n      <div className=\"h-44 overflow-hidden rounded-lg border border-border\">\n        <FilePreview\n          node={node}\n          variant=\"pane\"\n          resolveTextContent={resolveTextContent}\n          pdfWorkerSrc={ctx.pdfWorkerSrc}\n          labels={labels}\n          onDownload={can.download ? (n) => ctx.download([n.id]) : undefined}\n        />\n      </div>\n      <p className=\"truncate text-sm font-medium text-foreground\" title={node.name}>\n        {node.name}\n      </p>\n      <dl className=\"flex flex-col gap-1.5 text-xs\">\n        {meta.map(([k, v]) => (\n          <div key={k} className=\"flex items-center justify-between gap-2\">\n            <dt className=\"text-muted-foreground\">{k}</dt>\n            <dd className=\"truncate font-mono text-foreground\" title={v}>\n              {v}\n            </dd>\n          </div>\n        ))}\n      </dl>\n      <div className=\"mt-auto flex gap-2\">\n        {ctx.canPreview ? (\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            className=\"flex-1\"\n            onClick={() => ctx.openPreview(node.id)}\n          >\n            <Maximize2 className=\"size-4\" aria-hidden=\"true\" />\n            {labels.preview}\n          </Button>\n        ) : null}\n        {can.download ? (\n          <Button\n            variant=\"outline\"\n            size=\"sm\"\n            className=\"flex-1\"\n            onClick={() => ctx.download([node.id])}\n          >\n            <Download className=\"size-4\" aria-hidden=\"true\" />\n            {labels.download}\n          </Button>\n        ) : null}\n      </div>\n    </aside>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/parts/details-pane.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/file-card.tsx",
      "content": "\"use client\";\n\nimport { forwardRef, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { resolvePreviewKind } from \"../lib/preview-kind\";\nimport { extOf } from \"../lib/preview-kind\";\nimport type { MediaNode, MediaPreviewKind } from \"../types\";\nimport { FileKindBadge, PreviewKindIcon, STRIPE_STYLE } from \"./file-visuals\";\n\nexport interface FileCardProps extends HTMLAttributes<HTMLDivElement> {\n  node: MediaNode;\n  /** Pre-resolved kind; computed from the node when omitted. */\n  kind?: MediaPreviewKind;\n  selected?: boolean;\n  renaming?: ReactNode;\n  dragHandle?: ReactNode;\n  /** Extra overlay (e.g. an upload progress ring). */\n  overlay?: ReactNode;\n}\n\n/** Tier C — presentational file thumbnail tile. */\nexport const FileCard = forwardRef<HTMLDivElement, FileCardProps>(function FileCard(\n  { node, kind: kindProp, selected, renaming, dragHandle, overlay, className, ...rest },\n  ref,\n) {\n  const kind = kindProp ?? resolvePreviewKind(node);\n  const ext = extOf(node);\n  const thumb = node.thumbnailUrl ?? (kind === \"image\" ? node.url : undefined);\n  const dims =\n    node.width && node.height ? `${node.width} × ${node.height}` : null;\n\n  return (\n    <div\n      ref={ref}\n      className={cn(\n        \"group/file relative flex flex-col overflow-hidden rounded-xl border bg-card shadow-sm transition-[border-color,box-shadow] outline-none\",\n        \"focus-visible:ring-2 focus-visible:ring-ring\",\n        selected\n          ? \"border-primary ring-1 ring-primary\"\n          : \"border-border hover:border-foreground/20 hover:shadow-md\",\n        className,\n      )}\n      {...rest}\n    >\n      <div\n        className=\"relative aspect-4/3 w-full overflow-hidden bg-muted\"\n        style={!thumb ? STRIPE_STYLE : undefined}\n      >\n        {thumb ? (\n          <img\n            src={thumb}\n            alt=\"\"\n            loading=\"lazy\"\n            draggable={false}\n            className=\"size-full object-cover\"\n          />\n        ) : (\n          <span className=\"grid size-full place-items-center text-muted-foreground/70\">\n            <PreviewKindIcon kind={kind} className=\"size-9\" />\n          </span>\n        )}\n        <span className=\"absolute left-2 top-2\">\n          <FileKindBadge kind={kind} ext={ext} />\n        </span>\n        {dims ? (\n          <span className=\"absolute bottom-2 left-1/2 -translate-x-1/2 rounded-md bg-background/85 px-2 py-0.5 font-mono text-[11px] text-foreground shadow-sm ring-1 ring-border backdrop-blur-sm\">\n            {dims}\n          </span>\n        ) : null}\n        {overlay}\n      </div>\n      <div className=\"flex min-w-0 items-center gap-2 px-2.5 py-2\">\n        <FileKindBadge kind={kind} ext={ext} />\n        {renaming ?? (\n          <p className=\"truncate text-sm font-medium text-foreground\">{node.name}</p>\n        )}\n      </div>\n      {dragHandle}\n    </div>\n  );\n});\n",
      "type": "registry:component",
      "target": "components/media-library/parts/file-card.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/file-grid.tsx",
      "content": "\"use client\";\n\nimport { GripVertical, ImageOff, UploadCloud } from \"lucide-react\";\nimport type { KeyboardEvent, MouseEvent, PointerEventHandler } from \"react\";\nimport { useDraggable } from \"@dnd-kit/core\";\nimport { Button } from \"@/components/ui/button\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { cn } from \"@/lib/utils\";\nimport { resolvePreviewKind } from \"../lib/preview-kind\";\nimport { useMediaLibrary } from \"../hooks/use-media-library\";\nimport { MediaItemContextMenu } from \"./context-menu\";\nimport { FileCard } from \"./file-card\";\nimport { RenameInput } from \"./rename-input\";\nimport { UploadProgressCard } from \"./upload-progress\";\nimport type { MediaNode } from \"../types\";\n\nconst GRID_CLASS =\n  \"grid grid-cols-[repeat(auto-fill,minmax(9.5rem,1fr))] gap-3\";\n\nfunction ConnectedFileCard({ node }: { node: MediaNode }) {\n  const ctx = useMediaLibrary();\n  const selected = ctx.selection.isSelected(node.id);\n  const kind = resolvePreviewKind(node);\n  const ids =\n    ctx.selectedIds.has(node.id) && ctx.selectedIds.size > 1\n      ? Array.from(ctx.selectedIds)\n      : [node.id];\n\n  const { setNodeRef, listeners, attributes, isDragging } = useDraggable({\n    id: node.id,\n    data: { type: \"media-node\", ids },\n    disabled: !ctx.enableInternalDrag || !ctx.can.move,\n  });\n\n  const onClick = (e: MouseEvent) =>\n    ctx.selection.handleItemClick(\n      node.id,\n      { metaKey: e.metaKey, ctrlKey: e.ctrlKey, shiftKey: e.shiftKey },\n      ctx.visibleIds,\n    );\n  const onKeyDown = (e: KeyboardEvent) => {\n    if (e.key === \"Enter\") {\n      e.preventDefault();\n      if (ctx.canPreview) ctx.openPreview(node.id);\n    } else if (e.key === \" \") {\n      e.preventDefault();\n      ctx.selection.toggle(node.id);\n    }\n  };\n\n  return (\n    <MediaItemContextMenu node={node}>\n      <FileCard\n        ref={setNodeRef}\n        node={node}\n        kind={kind}\n        selected={selected}\n        role=\"button\"\n        tabIndex={0}\n        aria-pressed={selected}\n        aria-label={ctx.labels.itemAria\n          .replace(\"{name}\", node.name)\n          .replace(\"{kind}\", kind)}\n        onClick={onClick}\n        onDoubleClick={() => ctx.canPreview && ctx.openPreview(node.id)}\n        onKeyDown={onKeyDown}\n        onPointerDown={\n          listeners?.onPointerDown as PointerEventHandler<HTMLDivElement> | undefined\n        }\n        style={isDragging ? { opacity: 0.4 } : undefined}\n        renaming={\n          ctx.renamingId === node.id ? (\n            <RenameInput\n              initial={node.name}\n              error={ctx.renameError}\n              onSubmit={(v) => ctx.submitRename(node.id, v)}\n              onCancel={ctx.cancelRename}\n            />\n          ) : undefined\n        }\n        dragHandle={\n          ctx.enableInternalDrag && ctx.can.move ? (\n            <button\n              type=\"button\"\n              {...attributes}\n              {...listeners}\n              aria-label={`${ctx.labels.move}: ${node.name}`}\n              onClick={(e) => e.stopPropagation()}\n              className=\"absolute right-2 top-2 cursor-grab rounded bg-background/70 p-1 text-muted-foreground opacity-50 shadow-sm ring-1 ring-border backdrop-blur-sm transition-opacity hover:bg-background focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/file:opacity-100\"\n            >\n              <GripVertical className=\"size-3.5\" aria-hidden=\"true\" />\n            </button>\n          ) : undefined\n        }\n      />\n    </MediaItemContextMenu>\n  );\n}\n\n/** Tier B — FILES section: heading + thumbnail grid + uploads + empty/loading states. */\nexport function MediaLibraryFileGrid({ className }: { className?: string }) {\n  const {\n    files,\n    folders,\n    uploads,\n    labels,\n    loadingChildren,\n    loadError,\n    currentFolderId,\n    refresh,\n    can,\n    triggerUpload,\n    creatingFolder,\n  } = useMediaLibrary();\n\n  if (loadingChildren) {\n    return (\n      <section className={cn(\"flex flex-col gap-2\", className)}>\n        <h3 className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n          {labels.filesHeading}\n        </h3>\n        <div className={GRID_CLASS}>\n          {Array.from({ length: 5 }).map((_, i) => (\n            <Skeleton key={i} className=\"aspect-4/3 w-full rounded-xl\" />\n          ))}\n        </div>\n      </section>\n    );\n  }\n\n  if (loadError) {\n    return (\n      <div className=\"flex flex-col items-center gap-3 rounded-xl border border-destructive/40 bg-card p-8 text-center\">\n        <p className=\"text-sm text-destructive\">{loadError}</p>\n        <Button variant=\"outline\" size=\"sm\" onClick={() => refresh(currentFolderId)}>\n          {labels.retry}\n        </Button>\n      </div>\n    );\n  }\n\n  const totallyEmpty =\n    folders.length === 0 && files.length === 0 && uploads.length === 0 && !creatingFolder;\n\n  if (totallyEmpty) {\n    const isRoot = currentFolderId == null;\n    return (\n      <div className=\"flex flex-col items-center gap-3 rounded-xl border border-dashed border-border bg-card/50 p-12 text-center\">\n        <span className=\"grid size-12 place-items-center rounded-full bg-muted text-muted-foreground\">\n          {isRoot ? (\n            <UploadCloud className=\"size-6\" aria-hidden=\"true\" />\n          ) : (\n            <ImageOff className=\"size-6\" aria-hidden=\"true\" />\n          )}\n        </span>\n        <p className=\"text-sm text-muted-foreground\">\n          {isRoot ? labels.emptyLibrary : labels.emptyFolder}\n        </p>\n        {can.upload ? (\n          <Button size=\"sm\" onClick={triggerUpload}>\n            <UploadCloud className=\"size-4\" aria-hidden=\"true\" />\n            {labels.uploadButton}\n          </Button>\n        ) : null}\n      </div>\n    );\n  }\n\n  if (files.length === 0 && uploads.length === 0) return null;\n\n  return (\n    <section className={cn(\"flex flex-col gap-2\", className)}>\n      <h3 className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n        {labels.filesHeading}\n      </h3>\n      <div className={GRID_CLASS}>\n        {uploads.map((item) => (\n          <UploadProgressCard key={item.tempId} item={item} />\n        ))}\n        {files.map((node) => (\n          <ConnectedFileCard key={node.id} node={node} />\n        ))}\n      </div>\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/parts/file-grid.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/file-preview.tsx",
      "content": "\"use client\";\n\nimport { Component, lazy, Suspense, useState, type ReactNode } from \"react\";\nimport { Download, Loader2 } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { isTextKind, resolvePreviewKind } from \"../lib/preview-kind\";\nimport { useTextContent } from \"../hooks/use-text-content\";\nimport {\n  DEFAULT_MEDIA_LIBRARY_LABELS,\n  type MediaLibraryLabels,\n  type MediaNode,\n  type MediaPreviewKind,\n} from \"../types\";\nimport { PreviewKindIcon } from \"./file-visuals\";\n\n// Lazy viewers — code-split so a consumer who never previews a PDF/code/markdown\n// file never ships pdf.js / CodeMirror / shiki / marked. Import convention\n// (VERIFIED by consumer smoke):\n//   • SAME-category procomps (media→media) → dynamic-import a RELATIVE path. The\n//     `media/` segment is implied by `../../`, so it survives the consumer's\n//     flat layout; relative also dodges the F-S1 same-category slug bug.\n//   • CROSS-category procomps → dynamic-import a LOCAL re-export wrapper\n//     (`./lazy-*`). shadcn's rewriter strips the category for STATIC\n//     `@/registry/...` imports (inside the wrapper) but NOT for dynamic\n//     import() calls — so a direct dynamic cross-category import keeps a dead\n//     `code/`/`forms/` segment and breaks the install. The local indirection\n//     fixes it while preserving the code-split.\nconst LazyPdf = lazy(() =>\n  import(\"../../pdf-viewer/pdf-viewer\").then((m) => ({ default: m.PdfViewer })),\n);\nconst LazyVideo = lazy(() =>\n  import(\"../../video-player/video-player\").then((m) => ({ default: m.default })),\n);\nconst LazyCode = lazy(() => import(\"./lazy-code-block\"));\nconst LazyMarkdown = lazy(() => import(\"./lazy-markdown-editor\"));\n\nconst NOOP = () => {};\n\n/**\n * Catches a viewer that throws (network failure, corrupt file, pdf.js worker\n * error) so one bad file degrades to a graceful fallback instead of taking\n * down the whole library. Resets when the previewed node changes.\n */\nclass PreviewErrorBoundary extends Component<\n  { resetKey: string; fallback: ReactNode; children: ReactNode },\n  { hasError: boolean }\n> {\n  override state = { hasError: false };\n  static getDerivedStateFromError() {\n    return { hasError: true };\n  }\n  override componentDidUpdate(prev: { resetKey: string }) {\n    if (prev.resetKey !== this.props.resetKey && this.state.hasError) {\n      this.setState({ hasError: false });\n    }\n  }\n  override render() {\n    return this.state.hasError ? this.props.fallback : this.props.children;\n  }\n}\n\nexport interface FilePreviewProps {\n  node: MediaNode;\n  /** \"pane\" = constrained side panel; \"full\" = fills its container (lightbox). */\n  variant?: \"pane\" | \"full\";\n  resolveTextContent?: (node: MediaNode) => Promise<string>;\n  /** Override the pdf.js worker URL (forwarded to the composed pdf-viewer). */\n  pdfWorkerSrc?: string;\n  labels?: Partial<MediaLibraryLabels>;\n  onDownload?: (node: MediaNode) => void;\n  className?: string;\n}\n\nfunction PreviewFallback({ label }: { label: string }) {\n  return (\n    <div className=\"flex h-full min-h-32 flex-col items-center justify-center gap-2 text-muted-foreground\">\n      <Loader2 className=\"size-6 animate-spin text-primary\" aria-hidden=\"true\" />\n      <span className=\"text-sm\">{label}</span>\n    </div>\n  );\n}\n\nfunction PreviewMessage({\n  kind,\n  message,\n  node,\n  onDownload,\n  downloadLabel,\n  tone = \"muted\",\n}: {\n  kind: MediaPreviewKind;\n  message: string;\n  node: MediaNode;\n  onDownload?: (node: MediaNode) => void;\n  downloadLabel: string;\n  tone?: \"muted\" | \"error\";\n}) {\n  return (\n    <div className=\"flex h-full min-h-32 flex-col items-center justify-center gap-3 p-6 text-center\">\n      <span\n        className={cn(\n          \"grid size-12 place-items-center rounded-full\",\n          tone === \"error\" ? \"bg-destructive/10 text-destructive\" : \"bg-muted text-muted-foreground\",\n        )}\n      >\n        <PreviewKindIcon kind={kind} className=\"size-6\" />\n      </span>\n      <p className={cn(\"text-sm\", tone === \"error\" ? \"text-destructive\" : \"text-muted-foreground\")}>\n        {message}\n      </p>\n      {onDownload && node.url ? (\n        <Button variant=\"outline\" size=\"sm\" onClick={() => onDownload(node)}>\n          <Download className=\"size-4\" aria-hidden=\"true\" />\n          {downloadLabel}\n        </Button>\n      ) : null}\n    </div>\n  );\n}\n\nfunction TextPreview({\n  node,\n  kind,\n  resolveTextContent,\n  labels,\n}: {\n  node: MediaNode;\n  kind: MediaPreviewKind;\n  resolveTextContent?: (node: MediaNode) => Promise<string>;\n  labels: Required<MediaLibraryLabels>;\n}) {\n  const { text, loading, error } = useTextContent(node, true, resolveTextContent);\n\n  if (loading) return <PreviewFallback label={labels.previewLoading} />;\n  if (error != null || text == null) {\n    return (\n      <PreviewMessage\n        kind={kind}\n        node={node}\n        message={labels.previewError}\n        downloadLabel={labels.download}\n        tone=\"error\"\n      />\n    );\n  }\n\n  if (kind === \"markdown\") {\n    return (\n      <Suspense fallback={<PreviewFallback label={labels.previewLoading} />}>\n        <div className=\"h-full overflow-auto p-4\">\n          <LazyMarkdown\n            value={text}\n            onChange={NOOP}\n            readOnly\n            view=\"preview\"\n            toolbar={false}\n            showPreviewToggle={false}\n          />\n        </div>\n      </Suspense>\n    );\n  }\n\n  return (\n    <Suspense fallback={<PreviewFallback label={labels.previewLoading} />}>\n      <div className=\"h-full overflow-auto\">\n        <LazyCode value={text} filename={node.name} mode=\"view\" />\n      </div>\n    </Suspense>\n  );\n}\n\n/**\n * PDF preview that turns react-pdf's async `onError` (failed document / worker\n * fetch) into the graceful fallback — an error boundary can't catch the async\n * failure, so we surface it explicitly. Reset by keying on `node.id`.\n */\nfunction PdfPreview({\n  node,\n  labels,\n  workerSrc,\n  onDownload,\n}: {\n  node: MediaNode;\n  labels: Required<MediaLibraryLabels>;\n  workerSrc?: string;\n  onDownload?: (node: MediaNode) => void;\n}) {\n  const [failed, setFailed] = useState(false);\n  if (failed) {\n    return (\n      <div className=\"flex h-full w-full items-center justify-center bg-muted/30\">\n        <PreviewMessage\n          kind=\"pdf\"\n          node={node}\n          message={labels.previewError}\n          downloadLabel={labels.download}\n          onDownload={onDownload}\n          tone=\"error\"\n        />\n      </div>\n    );\n  }\n  return (\n    <Suspense fallback={<PreviewFallback label={labels.previewLoading} />}>\n      <LazyPdf source={node.url} workerSrc={workerSrc} onError={() => setFailed(true)} />\n    </Suspense>\n  );\n}\n\n/** Tier C — the multi-type preview dispatcher. Usable standalone (no context). */\nexport function FilePreview({\n  node,\n  variant = \"full\",\n  resolveTextContent,\n  pdfWorkerSrc,\n  labels: labelOverrides,\n  onDownload,\n  className,\n}: FilePreviewProps) {\n  const labels = { ...DEFAULT_MEDIA_LIBRARY_LABELS, ...labelOverrides };\n  const kind = resolvePreviewKind(node);\n\n  const wrap = (children: React.ReactNode) => (\n    <div\n      className={cn(\n        \"flex h-full w-full items-center justify-center overflow-hidden bg-muted/30\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n\n  if (!node.url && !isTextKind(kind)) {\n    return wrap(\n      <PreviewMessage\n        kind={kind}\n        node={node}\n        message={labels.noPreview}\n        downloadLabel={labels.download}\n        onDownload={onDownload}\n      />,\n    );\n  }\n\n  let content: ReactNode;\n  switch (kind) {\n    case \"image\":\n      content = wrap(\n        <img\n          src={node.url}\n          alt={node.name}\n          className=\"max-h-full max-w-full object-contain\"\n        />,\n      );\n      break;\n    case \"video\":\n      content = wrap(\n        <Suspense fallback={<PreviewFallback label={labels.previewLoading} />}>\n          <div className={cn(\"w-full\", variant === \"full\" ? \"max-w-4xl\" : \"\")}>\n            <LazyVideo src={node.url!} poster={node.thumbnailUrl} controls />\n          </div>\n        </Suspense>,\n      );\n      break;\n    case \"pdf\":\n      content = (\n        <div className={cn(\"h-full w-full overflow-hidden\", className)}>\n          <PdfPreview\n            key={node.id}\n            node={node}\n            labels={labels}\n            workerSrc={pdfWorkerSrc}\n            onDownload={onDownload}\n          />\n        </div>\n      );\n      break;\n    case \"code\":\n    case \"json\":\n    case \"text\":\n    case \"markdown\":\n      content = (\n        <div className={cn(\"h-full w-full overflow-hidden bg-card\", className)}>\n          <TextPreview\n            node={node}\n            kind={kind}\n            resolveTextContent={resolveTextContent}\n            labels={labels}\n          />\n        </div>\n      );\n      break;\n    default:\n      return wrap(\n        <PreviewMessage\n          kind={kind}\n          node={node}\n          message={labels.noPreview}\n          downloadLabel={labels.download}\n          onDownload={onDownload}\n        />,\n      );\n  }\n\n  // A viewer that throws (network / worker / corrupt file) degrades gracefully\n  // instead of crashing the library; resets when the previewed node changes.\n  return (\n    <PreviewErrorBoundary\n      resetKey={node.id}\n      fallback={wrap(\n        <PreviewMessage\n          kind={kind}\n          node={node}\n          message={labels.previewError}\n          downloadLabel={labels.download}\n          onDownload={onDownload}\n          tone=\"error\"\n        />,\n      )}\n    >\n      {content}\n    </PreviewErrorBoundary>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/parts/file-preview.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/file-visuals.tsx",
      "content": "\"use client\";\n\nimport {\n  File,\n  FileCode,\n  FileJson,\n  FileText,\n  FileType2,\n  Film,\n  Image as ImageIcon,\n} from \"lucide-react\";\nimport type { ComponentType } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport type { MediaPreviewKind } from \"../types\";\n\nconst KIND_ICON: Record<MediaPreviewKind, ComponentType<{ className?: string }>> = {\n  image: ImageIcon,\n  video: Film,\n  pdf: FileType2,\n  code: FileCode,\n  json: FileJson,\n  text: FileText,\n  markdown: FileText,\n  unknown: File,\n};\n\nexport function PreviewKindIcon({\n  kind,\n  className,\n}: {\n  kind: MediaPreviewKind;\n  className?: string;\n}) {\n  const Icon = KIND_ICON[kind] ?? File;\n  return <Icon className={className} />;\n}\n\nconst KIND_BADGE: Record<MediaPreviewKind, string> = {\n  image: \"bg-violet-500/15 text-violet-700 ring-violet-500/25 dark:text-violet-300\",\n  video: \"bg-orange-500/15 text-orange-700 ring-orange-500/25 dark:text-orange-300\",\n  pdf: \"bg-red-500/15 text-red-700 ring-red-500/25 dark:text-red-300\",\n  code: \"bg-sky-500/15 text-sky-700 ring-sky-500/25 dark:text-sky-300\",\n  json: \"bg-sky-500/15 text-sky-700 ring-sky-500/25 dark:text-sky-300\",\n  markdown: \"bg-sky-500/15 text-sky-700 ring-sky-500/25 dark:text-sky-300\",\n  text: \"bg-slate-500/15 text-slate-700 ring-slate-500/25 dark:text-slate-300\",\n  unknown: \"bg-muted text-muted-foreground ring-border\",\n};\n\n/** Small monospace extension pill (\"JPG\", \"PDF\", \"MD\"). */\nexport function FileKindBadge({\n  kind,\n  ext,\n  className,\n}: {\n  kind: MediaPreviewKind;\n  ext?: string;\n  className?: string;\n}) {\n  const label = (ext || kind).replace(/^\\./, \"\").toUpperCase().slice(0, 4);\n  return (\n    <span\n      className={cn(\n        \"inline-flex shrink-0 items-center rounded-md px-1.5 py-0.5 font-mono text-[10px] font-semibold uppercase ring-1 ring-inset\",\n        KIND_BADGE[kind] ?? KIND_BADGE.unknown,\n        className,\n      )}\n    >\n      {label}\n    </span>\n  );\n}\n\n/** Subtle diagonal-stripe backdrop for visual files lacking a thumbnail. */\nexport const STRIPE_STYLE: React.CSSProperties = {\n  backgroundImage:\n    \"repeating-linear-gradient(-45deg, color-mix(in oklch, var(--muted-foreground) 8%, transparent) 0 10px, transparent 10px 20px)\",\n};\n",
      "type": "registry:component",
      "target": "components/media-library/parts/file-visuals.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/folder-card.tsx",
      "content": "\"use client\";\n\nimport { forwardRef, type HTMLAttributes, type ReactNode } from \"react\";\nimport { ChevronRight, Folder } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { formatRelativeTime } from \"../lib/format\";\nimport type { MediaNode } from \"../types\";\n\nexport interface FolderCardProps extends HTMLAttributes<HTMLDivElement> {\n  node: MediaNode;\n  selected?: boolean;\n  /** Number of items inside, when known. */\n  itemCount?: number;\n  /** Highlight while a drag hovers over it. */\n  isDropTarget?: boolean;\n  /** Replaces the name with an inline editor when renaming. */\n  renaming?: ReactNode;\n  /** Optional drag-handle node (keyboard-accessible move). */\n  dragHandle?: ReactNode;\n}\n\n/** Tier C — presentational folder tile. dnd / selection wiring lives in the connected row. */\nexport const FolderCard = forwardRef<HTMLDivElement, FolderCardProps>(\n  function FolderCard(\n    { node, selected, itemCount, isDropTarget, renaming, dragHandle, className, ...rest },\n    ref,\n  ) {\n    const meta = [\n      typeof itemCount === \"number\" ? `${itemCount} item${itemCount === 1 ? \"\" : \"s\"}` : null,\n      node.modifiedAt ? formatRelativeTime(node.modifiedAt) : null,\n    ]\n      .filter(Boolean)\n      .join(\" · \");\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"group/folder relative flex items-center gap-3 rounded-xl border bg-card p-3 shadow-sm transition-[border-color,box-shadow] outline-none\",\n          \"focus-visible:ring-2 focus-visible:ring-ring\",\n          selected\n            ? \"border-primary ring-1 ring-primary\"\n            : \"border-border hover:border-foreground/20 hover:shadow-md\",\n          isDropTarget && \"border-primary bg-primary/5 ring-2 ring-primary\",\n          className,\n        )}\n        {...rest}\n      >\n        <span className=\"grid size-10 shrink-0 place-items-center rounded-lg bg-primary/10 text-primary\">\n          <Folder className=\"size-5\" aria-hidden=\"true\" />\n        </span>\n        <div className=\"min-w-0 flex-1\">\n          {renaming ?? (\n            <p className=\"truncate text-sm font-medium text-foreground\">{node.name}</p>\n          )}\n          {meta ? <p className=\"truncate text-xs text-muted-foreground\">{meta}</p> : null}\n        </div>\n        {dragHandle}\n        <ChevronRight\n          className=\"size-4 shrink-0 text-muted-foreground/40 transition-colors group-hover/folder:text-muted-foreground\"\n          aria-hidden=\"true\"\n        />\n      </div>\n    );\n  },\n);\n",
      "type": "registry:component",
      "target": "components/media-library/parts/folder-card.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/folder-row.tsx",
      "content": "\"use client\";\n\nimport { GripVertical } from \"lucide-react\";\nimport type { KeyboardEvent, MouseEvent, PointerEventHandler, Ref } from \"react\";\nimport { useDraggable, useDroppable } from \"@dnd-kit/core\";\nimport { cn } from \"@/lib/utils\";\nimport { useMediaLibrary } from \"../hooks/use-media-library\";\nimport { DROP_PREFIX } from \"./media-library-root\";\nimport { MediaItemContextMenu } from \"./context-menu\";\nimport { FolderCard } from \"./folder-card\";\nimport { RenameInput } from \"./rename-input\";\nimport type { MediaNode } from \"../types\";\n\nfunction composeRefs<T>(...refs: Array<Ref<T> | undefined>) {\n  return (node: T | null) => {\n    for (const ref of refs) {\n      if (typeof ref === \"function\") ref(node);\n      else if (ref && \"current\" in ref) (ref as { current: T | null }).current = node;\n    }\n  };\n}\n\nfunction ConnectedFolderCard({ node }: { node: MediaNode }) {\n  const ctx = useMediaLibrary();\n  const selected = ctx.selection.isSelected(node.id);\n  const ids =\n    ctx.selectedIds.has(node.id) && ctx.selectedIds.size > 1\n      ? Array.from(ctx.selectedIds)\n      : [node.id];\n\n  const { setNodeRef: setDragRef, listeners, attributes, isDragging } = useDraggable({\n    id: node.id,\n    data: { type: \"media-node\", ids },\n    disabled: !ctx.enableInternalDrag || !ctx.can.move,\n  });\n  const { setNodeRef: setDropRef, isOver } = useDroppable({\n    id: `${DROP_PREFIX}${node.id}`,\n  });\n\n  const isDropTarget =\n    isOver && ctx.activeDragIds != null && ctx.canDrop(ctx.activeDragIds, node.id);\n\n  const onClick = (e: MouseEvent) =>\n    ctx.selection.handleItemClick(\n      node.id,\n      { metaKey: e.metaKey, ctrlKey: e.ctrlKey, shiftKey: e.shiftKey },\n      ctx.visibleIds,\n    );\n  const onKeyDown = (e: KeyboardEvent) => {\n    if (e.key === \"Enter\") {\n      e.preventDefault();\n      ctx.navigateTo(node.id);\n    } else if (e.key === \" \") {\n      e.preventDefault();\n      ctx.selection.toggle(node.id);\n    }\n  };\n\n  return (\n    <MediaItemContextMenu node={node}>\n      <FolderCard\n        ref={composeRefs(setDragRef, setDropRef)}\n        node={node}\n        selected={selected}\n        itemCount={node.children?.length}\n        isDropTarget={isDropTarget}\n        role=\"button\"\n        tabIndex={0}\n        aria-pressed={selected}\n        aria-label={ctx.labels.itemAria\n          .replace(\"{name}\", node.name)\n          .replace(\"{kind}\", \"folder\")}\n        onClick={onClick}\n        onDoubleClick={() => ctx.navigateTo(node.id)}\n        onKeyDown={onKeyDown}\n        onPointerDown={\n          listeners?.onPointerDown as PointerEventHandler<HTMLDivElement> | undefined\n        }\n        style={isDragging ? { opacity: 0.4 } : undefined}\n        renaming={\n          ctx.renamingId === node.id ? (\n            <RenameInput\n              initial={node.name}\n              error={ctx.renameError}\n              onSubmit={(v) => ctx.submitRename(node.id, v)}\n              onCancel={ctx.cancelRename}\n            />\n          ) : undefined\n        }\n        dragHandle={\n          ctx.enableInternalDrag && ctx.can.move ? (\n            <button\n              type=\"button\"\n              {...attributes}\n              {...listeners}\n              aria-label={`${ctx.labels.move}: ${node.name}`}\n              className=\"shrink-0 cursor-grab rounded p-1 text-muted-foreground/40 transition-colors hover:bg-accent hover:text-muted-foreground focus-visible:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/folder:text-muted-foreground/70\"\n              onClick={(e) => e.stopPropagation()}\n            >\n              <GripVertical className=\"size-4\" aria-hidden=\"true\" />\n            </button>\n          ) : undefined\n        }\n      />\n    </MediaItemContextMenu>\n  );\n}\n\n/** Tier B — FOLDERS section: heading + folder-card grid + inline new-folder editor. */\nexport function MediaLibraryFolderRow({ className }: { className?: string }) {\n  const { folders, labels, creatingFolder, submitCreateFolder, cancelCreateFolder } =\n    useMediaLibrary();\n\n  if (folders.length === 0 && !creatingFolder) return null;\n\n  return (\n    <section className={cn(\"flex flex-col gap-2\", className)}>\n      <h3 className=\"text-xs font-semibold uppercase tracking-wider text-muted-foreground\">\n        {labels.foldersHeading}\n      </h3>\n      <div className=\"grid grid-cols-[repeat(auto-fill,minmax(11rem,1fr))] gap-3\">\n        {creatingFolder ? (\n          <div className=\"flex items-center gap-3 rounded-xl border border-dashed border-primary/50 bg-card p-3 shadow-sm\">\n            <span className=\"size-10 shrink-0 rounded-lg bg-primary/10\" />\n            <RenameInput\n              initial=\"\"\n              placeholder={labels.newFolderButton}\n              onSubmit={submitCreateFolder}\n              onCancel={cancelCreateFolder}\n              className=\"flex-1\"\n            />\n          </div>\n        ) : null}\n        {folders.map((node) => (\n          <ConnectedFolderCard key={node.id} node={node} />\n        ))}\n      </div>\n    </section>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/parts/folder-row.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/library-toolbar.tsx",
      "content": "\"use client\";\n\nimport { FolderPlus, Search, Upload } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport { useMediaLibrary } from \"../hooks/use-media-library\";\n\n/** Tier B — title + search + New folder + Upload. */\nexport function MediaLibraryToolbar({ className }: { className?: string }) {\n  const {\n    labels,\n    can,\n    triggerUpload,\n    startCreateFolder,\n    searchQuery,\n    setSearchQuery,\n  } = useMediaLibrary();\n\n  return (\n    <div className={cn(\"flex flex-wrap items-center justify-between gap-3\", className)}>\n      <h2 className=\"text-2xl font-semibold tracking-tight text-foreground\">\n        {labels.title}\n      </h2>\n      <div className=\"flex flex-wrap items-center gap-2\">\n        <div className=\"relative\">\n          <Search\n            className=\"pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground\"\n            aria-hidden=\"true\"\n          />\n          <Input\n            value={searchQuery}\n            onChange={(e) => setSearchQuery(e.target.value)}\n            placeholder={labels.searchPlaceholder}\n            aria-label={labels.searchPlaceholder}\n            className=\"h-9 w-44 pl-8 sm:w-64\"\n          />\n        </div>\n        {can.createFolder ? (\n          <Button\n            type=\"button\"\n            variant=\"outline\"\n            size=\"sm\"\n            className=\"h-9\"\n            onClick={startCreateFolder}\n          >\n            <FolderPlus className=\"size-4\" aria-hidden=\"true\" />\n            {labels.newFolderButton}\n          </Button>\n        ) : null}\n        {can.upload ? (\n          <Button type=\"button\" size=\"sm\" className=\"h-9\" onClick={triggerUpload}>\n            <Upload className=\"size-4\" aria-hidden=\"true\" />\n            {labels.uploadButton}\n          </Button>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/parts/library-toolbar.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/media-library-root.tsx",
      "content": "\"use client\";\n\nimport {\n  forwardRef,\n  useCallback,\n  useId,\n  useImperativeHandle,\n  useRef,\n  type DragEvent as ReactDragEvent,\n} from \"react\";\nimport {\n  DndContext,\n  DragOverlay,\n  KeyboardSensor,\n  PointerSensor,\n  pointerWithin,\n  useSensor,\n  useSensors,\n  type DragEndEvent,\n  type DragStartEvent,\n} from \"@dnd-kit/core\";\nimport { Files } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  MediaLibraryContext,\n  useMediaLibraryStore,\n} from \"../hooks/use-media-library\";\nimport type { MediaDragData } from \"../lib/drag\";\nimport type { MediaLibraryHandle, MediaLibraryRootProps } from \"../types\";\nimport { MediaLibraryUploadOverlay } from \"./upload-overlay\";\n\nexport const DROP_PREFIX = \"drop:\";\n\n/**\n * Headless provider (Tier B). Owns the store/context, the single `DndContext`\n * for internal move, OS file-drop wiring, the hidden upload input, and the\n * imperative handle. Renders `children` so consumers compose freely.\n */\nexport const MediaLibraryRoot = forwardRef<\n  MediaLibraryHandle,\n  MediaLibraryRootProps\n>(function MediaLibraryRoot(props, ref) {\n  const store = useMediaLibraryStore(props);\n  const { children, className } = props;\n  const dragDepth = useRef(0);\n  // Stable, SSR-safe id for DndContext — without it, @dnd-kit derives its\n  // `aria-describedby` (\"DndDescribedBy-N\") from a module-level counter that\n  // increments in a different order on the server vs the client → hydration\n  // mismatch. useId is identical across SSR/CSR and unique per Root instance.\n  const dndId = useId();\n\n  useImperativeHandle(\n    ref,\n    () => ({\n      navigateTo: store.navigateTo,\n      refresh: store.refresh,\n      openPreview: store.openPreview,\n      closePreview: store.closePreview,\n      triggerUpload: store.triggerUpload,\n      getSelectedIds: () => Array.from(store.selectedIds),\n      clearSelection: store.selection.clear,\n    }),\n    [store],\n  );\n\n  const sensors = useSensors(\n    useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),\n    useSensor(KeyboardSensor),\n  );\n\n  const handleDragStart = useCallback(\n    (event: DragStartEvent) => {\n      if (!store.enableInternalDrag) return;\n      const id = String(event.active.id);\n      const data = event.active.data.current as MediaDragData | undefined;\n      const ids = data?.ids?.length ? data.ids : [id];\n      store.setActiveDragIds(ids);\n    },\n    [store],\n  );\n\n  const handleDragEnd = useCallback(\n    (event: DragEndEvent) => {\n      store.setActiveDragIds(null);\n      const overId = event.over ? String(event.over.id) : null;\n      if (!overId || !overId.startsWith(DROP_PREFIX)) return;\n      // Read the dragged ids straight off the event (set at drag-start) rather\n      // than from React state, which can lag behind the drop.\n      const data = event.active.data.current as MediaDragData | undefined;\n      const ids = data?.ids?.length ? data.ids : [String(event.active.id)];\n      const rawTarget = overId.slice(DROP_PREFIX.length);\n      const target = rawTarget === \"root\" ? null : rawTarget;\n      store.moveTo(ids, target);\n    },\n    [store],\n  );\n\n  // ---- OS file drop (native HTML5 drag of files from the desktop) ----\n  const isFileDrag = (e: ReactDragEvent) =>\n    Array.from(e.dataTransfer?.types ?? []).includes(\"Files\");\n\n  const onDragEnter = useCallback(\n    (e: ReactDragEvent) => {\n      if (!store.enableUploadDrop || !store.can.upload || !isFileDrag(e)) return;\n      e.preventDefault();\n      dragDepth.current += 1;\n      store.setDraggingFiles(true);\n    },\n    [store],\n  );\n  const onDragOver = useCallback(\n    (e: ReactDragEvent) => {\n      if (!store.enableUploadDrop || !store.can.upload || !isFileDrag(e)) return;\n      e.preventDefault();\n      e.dataTransfer.dropEffect = \"copy\";\n    },\n    [store],\n  );\n  const onDragLeave = useCallback(\n    (e: ReactDragEvent) => {\n      if (!store.enableUploadDrop || !store.can.upload || !isFileDrag(e)) return;\n      dragDepth.current = Math.max(0, dragDepth.current - 1);\n      if (dragDepth.current === 0) store.setDraggingFiles(false);\n    },\n    [store],\n  );\n  const onDrop = useCallback(\n    (e: ReactDragEvent) => {\n      if (!store.enableUploadDrop || !store.can.upload || !isFileDrag(e)) return;\n      e.preventDefault();\n      dragDepth.current = 0;\n      store.setDraggingFiles(false);\n      const files = Array.from(e.dataTransfer.files ?? []);\n      if (files.length) store.uploadFiles(files);\n    },\n    [store],\n  );\n\n  const onFileInputChange = useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      // Snapshot BEFORE resetting value (the browse-FileList lesson, v0.1.2).\n      const files = Array.from(e.target.files ?? []);\n      e.target.value = \"\";\n      if (files.length) store.uploadFiles(files);\n    },\n    [store],\n  );\n\n  return (\n    <MediaLibraryContext.Provider value={store}>\n      <DndContext\n        id={dndId}\n        sensors={sensors}\n        collisionDetection={pointerWithin}\n        onDragStart={handleDragStart}\n        onDragEnd={handleDragEnd}\n        onDragCancel={() => store.setActiveDragIds(null)}\n      >\n        <div\n          className={cn(\n            \"relative flex flex-col gap-4 text-foreground\",\n            \"motion-safe:animate-in motion-safe:fade-in-0 motion-safe:duration-300\",\n            className,\n          )}\n          onDragEnter={onDragEnter}\n          onDragOver={onDragOver}\n          onDragLeave={onDragLeave}\n          onDrop={onDrop}\n        >\n          <input\n            ref={store.fileInputRef}\n            type=\"file\"\n            multiple\n            accept=\"image/*,video/*,application/pdf,text/*,.md,.markdown,.json\"\n            className=\"sr-only\"\n            tabIndex={-1}\n            aria-hidden=\"true\"\n            onChange={onFileInputChange}\n          />\n          {children}\n          {store.enableUploadDrop && store.can.upload ? (\n            <MediaLibraryUploadOverlay />\n          ) : null}\n          <DragOverlay dropAnimation={null}>\n            {store.activeDragIds ? (\n              <div className=\"flex items-center gap-2 rounded-md border border-border bg-popover px-3 py-2 text-sm font-medium shadow-lg\">\n                <Files className=\"size-4 text-primary\" aria-hidden=\"true\" />\n                {store.activeDragIds.length}\n              </div>\n            ) : null}\n          </DragOverlay>\n        </div>\n      </DndContext>\n    </MediaLibraryContext.Provider>\n  );\n});\n",
      "type": "registry:component",
      "target": "components/media-library/parts/media-library-root.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/preview-lightbox.tsx",
      "content": "\"use client\";\n\nimport { useCallback } from \"react\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  DEFAULT_MEDIA_LIBRARY_LABELS,\n  type MediaLibraryLabels,\n  type MediaNode,\n} from \"../types\";\nimport { FilePreview } from \"./file-preview\";\nimport { useMediaLibrary } from \"../hooks/use-media-library\";\n\nexport interface FilePreviewLightboxProps {\n  nodes: MediaNode[];\n  activeId: string | null;\n  onClose: () => void;\n  onNavigate: (id: string) => void;\n  resolveTextContent?: (node: MediaNode) => Promise<string>;\n  pdfWorkerSrc?: string;\n  onDownload?: (node: MediaNode) => void;\n  labels?: Partial<MediaLibraryLabels>;\n}\n\n/** Tier C — controlled full-screen lightbox stepping through `nodes`. */\nexport function FilePreviewLightbox({\n  nodes,\n  activeId,\n  onClose,\n  onNavigate,\n  resolveTextContent,\n  pdfWorkerSrc,\n  onDownload,\n  labels: labelOverrides,\n}: FilePreviewLightboxProps) {\n  const labels = { ...DEFAULT_MEDIA_LIBRARY_LABELS, ...labelOverrides };\n  const index = activeId == null ? -1 : nodes.findIndex((n) => n.id === activeId);\n  const node = index >= 0 ? nodes[index] : null;\n  const hasPrev = index > 0;\n  const hasNext = index >= 0 && index < nodes.length - 1;\n\n  const goPrev = useCallback(() => {\n    if (hasPrev) onNavigate(nodes[index - 1].id);\n  }, [hasPrev, nodes, index, onNavigate]);\n  const goNext = useCallback(() => {\n    if (hasNext) onNavigate(nodes[index + 1].id);\n  }, [hasNext, nodes, index, onNavigate]);\n\n  return (\n    <Dialog\n      open={node != null}\n      onOpenChange={(open) => {\n        if (!open) onClose();\n      }}\n    >\n      <DialogContent\n        className=\"h-[88vh] w-[96vw] max-w-[min(96vw,1100px)]! gap-0 overflow-hidden p-0\"\n        onKeyDown={(e) => {\n          if (e.key === \"ArrowLeft\") goPrev();\n          else if (e.key === \"ArrowRight\") goNext();\n        }}\n      >\n        <DialogTitle className=\"sr-only\">{node?.name ?? labels.preview}</DialogTitle>\n        <DialogDescription className=\"sr-only\">{labels.preview}</DialogDescription>\n        {node ? (\n          <div className=\"flex h-full w-full flex-col\">\n            <div className=\"flex items-center justify-between gap-2 border-b border-border px-4 py-2.5 pr-12\">\n              <p className=\"truncate text-sm font-medium text-foreground\">{node.name}</p>\n              <span className=\"shrink-0 font-mono text-xs text-muted-foreground tabular-nums\">\n                {index + 1} / {nodes.length}\n              </span>\n            </div>\n            <div className=\"relative min-h-0 flex-1\">\n              <FilePreview\n                node={node}\n                variant=\"full\"\n                resolveTextContent={resolveTextContent}\n                pdfWorkerSrc={pdfWorkerSrc}\n                labels={labels}\n                onDownload={onDownload}\n                className=\"h-full\"\n              />\n              {hasPrev ? (\n                <Button\n                  variant=\"secondary\"\n                  size=\"icon\"\n                  aria-label={labels.prev}\n                  onClick={goPrev}\n                  className=\"absolute left-3 top-1/2 size-9 -translate-y-1/2 rounded-full shadow-md\"\n                >\n                  <ChevronLeft className=\"size-5\" aria-hidden=\"true\" />\n                </Button>\n              ) : null}\n              {hasNext ? (\n                <Button\n                  variant=\"secondary\"\n                  size=\"icon\"\n                  aria-label={labels.next}\n                  onClick={goNext}\n                  className={cn(\n                    \"absolute right-3 top-1/2 size-9 -translate-y-1/2 rounded-full shadow-md\",\n                  )}\n                >\n                  <ChevronRight className=\"size-5\" aria-hidden=\"true\" />\n                </Button>\n              ) : null}\n            </div>\n          </div>\n        ) : null}\n      </DialogContent>\n    </Dialog>\n  );\n}\n\n/** Tier B — context-connected lightbox (steps through the current folder's files). */\nexport function MediaLibraryLightbox() {\n  const ctx = useMediaLibrary();\n  return (\n    <FilePreviewLightbox\n      nodes={ctx.files}\n      activeId={ctx.previewId}\n      onClose={ctx.closePreview}\n      onNavigate={ctx.openPreview}\n      resolveTextContent={ctx.resolveTextContent}\n      pdfWorkerSrc={ctx.pdfWorkerSrc}\n      onDownload={ctx.can.download ? (n) => ctx.download([n.id]) : undefined}\n      labels={ctx.labels}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/parts/preview-lightbox.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/quota-bar.tsx",
      "content": "\"use client\";\n\nimport { HardDrive } from \"lucide-react\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { cn } from \"@/lib/utils\";\nimport { DEFAULT_MEDIA_LIBRARY_LABELS } from \"../types\";\nimport { fillTemplate, formatBytes } from \"../lib/format\";\nimport { useMediaLibrary } from \"../hooks/use-media-library\";\n\nexport interface QuotaBarProps {\n  used: number;\n  total: number;\n  /** Full caption override; defaults to the filled `quotaUsed` template. */\n  label?: string;\n  className?: string;\n}\n\n/** Tier C — standalone storage-quota bar (no context). */\nexport function QuotaBar({ used, total, label, className }: QuotaBarProps) {\n  const pct = total > 0 ? Math.min(100, Math.round((used / total) * 100)) : 0;\n  const caption =\n    label ??\n    fillTemplate(DEFAULT_MEDIA_LIBRARY_LABELS.quotaUsed, {\n      used: formatBytes(used),\n      total: formatBytes(total),\n    });\n\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-4 rounded-xl border border-border bg-card px-4 py-3 shadow-sm\",\n        className,\n      )}\n    >\n      <span className=\"grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-muted-foreground\">\n        <HardDrive className=\"size-5\" aria-hidden=\"true\" />\n      </span>\n      <div className=\"min-w-0 flex-1\">\n        <div className=\"flex items-baseline justify-between gap-3\">\n          <span className=\"truncate text-sm text-foreground\">{caption}</span>\n          <span className=\"shrink-0 font-mono text-xs text-muted-foreground\">\n            {pct}%\n          </span>\n        </div>\n        <Progress value={pct} className=\"mt-2 h-1.5\" />\n      </div>\n    </div>\n  );\n}\n\n/** Tier B — context-connected quota bar. Renders nothing if `storage` is unset. */\nexport function MediaLibraryQuotaBar({ className }: { className?: string }) {\n  const { storage, labels } = useMediaLibrary();\n  if (!storage) return null;\n  const caption = fillTemplate(labels.quotaUsed, {\n    used: formatBytes(storage.used),\n    total: formatBytes(storage.total),\n  });\n  return (\n    <QuotaBar\n      used={storage.used}\n      total={storage.total}\n      label={caption}\n      className={className}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/parts/quota-bar.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/rename-input.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\n/** Inline rename / new-folder editor. Enter commits, Escape cancels, blur commits. */\nexport function RenameInput({\n  initial,\n  error,\n  placeholder,\n  onSubmit,\n  onCancel,\n  className,\n}: {\n  initial: string;\n  error?: string | null;\n  placeholder?: string;\n  onSubmit: (value: string) => void;\n  onCancel: () => void;\n  className?: string;\n}) {\n  const [value, setValue] = useState(initial);\n  const ref = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    ref.current?.focus();\n    ref.current?.select();\n  }, []);\n\n  return (\n    <Input\n      ref={ref}\n      value={value}\n      placeholder={placeholder}\n      aria-invalid={error ? true : undefined}\n      aria-label={placeholder}\n      onClick={(e) => e.stopPropagation()}\n      onDoubleClick={(e) => e.stopPropagation()}\n      onChange={(e) => setValue(e.target.value)}\n      onKeyDown={(e) => {\n        if (e.key === \"Enter\") {\n          e.preventDefault();\n          onSubmit(value);\n        } else if (e.key === \"Escape\") {\n          e.preventDefault();\n          onCancel();\n        }\n        e.stopPropagation();\n      }}\n      onBlur={() => onSubmit(value)}\n      className={cn(\"h-7 px-2 text-sm\", error && \"border-destructive\", className)}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/parts/rename-input.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/sidebar.tsx",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { cn } from \"@/lib/utils\";\n// VALUE import from file-tree's .tsx entry. CROSS-category (media→navigation) →\n// `@/registry/...` ALIAS so shadcn's rewriter strips the category in the consumer\n// (`@/components/file-tree/file-tree`); a relative `../../../navigation/...` would\n// keep the dead `navigation/` segment and break the install (consumer-smoke proven).\n// Structural typing bridges MediaNode[] → FsNode[] (no cross-procomp type import).\nimport { FileTree } from \"@/registry/components/navigation/file-tree/file-tree\";\nimport { findNode } from \"../lib/drag\";\nimport { useMediaLibrary } from \"../hooks/use-media-library\";\nimport type { MediaNode } from \"../types\";\n\n/** Synthetic root row so the tree shows (and can navigate back to) the library root. */\nconst ROOT_ID = \"__media_library_root__\";\n\n/** Tier B — navigation + move sidebar (composes the shipped `file-tree`). */\nexport function MediaLibrarySidebar({ className }: { className?: string }) {\n  const ctx = useMediaLibrary();\n\n  // A \"Library\" root node whose children are the whole tree, so the root is\n  // visible/selectable and folders show their real contents (no false \"empty\").\n  const tree = useMemo<MediaNode[]>(\n    () => [\n      {\n        id: ROOT_ID,\n        name: ctx.labels.libraryHeading,\n        type: \"folder\",\n        children: ctx.nodes,\n      },\n    ],\n    [ctx.nodes, ctx.labels.libraryHeading],\n  );\n\n  const selectedIds = useMemo(\n    () => new Set([ctx.currentFolderId ?? ROOT_ID]),\n    [ctx.currentFolderId],\n  );\n  const defaultExpandedIds = useMemo(() => new Set([ROOT_ID]), []);\n\n  // Folder → navigate (ROOT → root); file → preview.\n  const activate = (id: string) => {\n    if (id === ROOT_ID) {\n      ctx.navigateTo(null);\n      return;\n    }\n    const node = findNode(ctx.nodes, id);\n    if (!node) return;\n    if (node.type === \"folder\") ctx.navigateTo(id);\n    else if (ctx.canPreview) ctx.openPreview(id);\n  };\n\n  return (\n    <aside\n      className={cn(\n        \"hidden w-56 shrink-0 overflow-auto rounded-xl border border-border bg-card p-2 md:block\",\n        className,\n      )}\n    >\n      <FileTree\n        nodes={tree}\n        selectionMode=\"single\"\n        selectedIds={selectedIds}\n        defaultExpandedIds={defaultExpandedIds}\n        header={false}\n        contextMenu={false}\n        enableInternalDrag={ctx.enableInternalDrag && ctx.can.move}\n        onOpen={(args) => activate(args.node.id)}\n        onSelectedChange={(args) => {\n          const first = Array.from(args.ids)[0];\n          if (first !== undefined) activate(first);\n        }}\n        onMove={(args) => {\n          // Only \"inside\" = re-parent into a folder (we don't do sibling reorder).\n          if (args.position !== \"inside\") return;\n          const ids = args.ids.filter((id) => id !== ROOT_ID);\n          if (ids.length === 0) return;\n          ctx.moveTo(ids, args.targetId === ROOT_ID ? null : args.targetId);\n        }}\n      />\n    </aside>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/parts/sidebar.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/type-filters.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { FILTER_ORDER } from \"../lib/filter-category\";\nimport { useMediaLibrary } from \"../hooks/use-media-library\";\nimport type { MediaFilterCategory } from \"../types\";\n\n/** Small category dots mirroring the reference design (semantic, not the brand accent). */\nconst DOT: Record<MediaFilterCategory, string> = {\n  all: \"bg-muted-foreground/50\",\n  images: \"bg-violet-500\",\n  video: \"bg-orange-500\",\n  pdfs: \"bg-red-500\",\n  docs: \"bg-sky-500\",\n};\n\n/** Tier B — type-filter chips with live counts. */\nexport function MediaLibraryTypeFilters({ className }: { className?: string }) {\n  const { filter, setFilter, filterCounts, labels } = useMediaLibrary();\n\n  const labelFor: Record<MediaFilterCategory, string> = {\n    all: labels.filterAll,\n    images: labels.filterImages,\n    video: labels.filterVideo,\n    pdfs: labels.filterPdfs,\n    docs: labels.filterDocs,\n  };\n\n  return (\n    <div\n      role=\"group\"\n      aria-label={labels.filesHeading}\n      className={cn(\"flex flex-wrap items-center gap-2\", className)}\n    >\n      {FILTER_ORDER.map((cat) => {\n        const active = filter === cat;\n        return (\n          <button\n            key={cat}\n            type=\"button\"\n            aria-pressed={active}\n            onClick={() => setFilter(cat)}\n            className={cn(\n              \"inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm transition-colors\",\n              \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n              active\n                ? \"border-primary bg-primary/15 font-medium text-foreground\"\n                : \"border-border bg-card text-muted-foreground hover:bg-accent hover:text-foreground\",\n            )}\n          >\n            <span className={cn(\"size-2 rounded-full\", DOT[cat])} aria-hidden=\"true\" />\n            {labelFor[cat]}\n            <span\n              className={cn(\n                \"font-mono text-xs tabular-nums\",\n                active ? \"text-foreground/70\" : \"text-muted-foreground/70\",\n              )}\n            >\n              {filterCounts[cat]}\n            </span>\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/parts/type-filters.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/upload-overlay.tsx",
      "content": "\"use client\";\n\nimport { UploadCloud } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { useMediaLibrary } from \"../hooks/use-media-library\";\n\n/** Tier C — dumb drop scrim. */\nexport function UploadOverlay({\n  visible,\n  label,\n  className,\n}: {\n  visible: boolean;\n  label: string;\n  className?: string;\n}) {\n  if (!visible) return null;\n  return (\n    <div\n      className={cn(\n        \"pointer-events-none absolute inset-0 z-30 grid place-items-center rounded-xl border-2 border-dashed border-primary bg-primary/10 backdrop-blur-[2px]\",\n        \"motion-safe:animate-in motion-safe:fade-in-0\",\n        className,\n      )}\n    >\n      <div className=\"flex flex-col items-center gap-2\">\n        <span className=\"grid size-12 place-items-center rounded-full bg-primary text-primary-foreground\">\n          <UploadCloud className=\"size-6\" aria-hidden=\"true\" />\n        </span>\n        <span className=\"text-sm font-medium text-foreground\">{label}</span>\n      </div>\n    </div>\n  );\n}\n\n/** Tier B — context-connected drop scrim (driven by the root's drag handlers). */\nexport function MediaLibraryUploadOverlay({ className }: { className?: string }) {\n  const { isDraggingFiles, labels } = useMediaLibrary();\n  return (\n    <UploadOverlay\n      visible={isDraggingFiles}\n      label={labels.dropToUpload}\n      className={className}\n    />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/parts/upload-overlay.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/upload-progress.tsx",
      "content": "\"use client\";\n\nimport { AlertCircle, Loader2, X } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { cn } from \"@/lib/utils\";\nimport { fillTemplate } from \"../lib/format\";\nimport { useMediaLibrary } from \"../hooks/use-media-library\";\nimport type { MediaUploadItem } from \"../types\";\nimport { FileKindBadge } from \"./file-visuals\";\n\n/** A tile for an in-flight optimistic upload (progress ring + error/retry). */\nexport function UploadProgressCard({ item }: { item: MediaUploadItem }) {\n  const { labels, retryUpload, dismissUpload } = useMediaLibrary();\n  const isError = item.status === \"error\";\n\n  return (\n    <div\n      className={cn(\n        \"relative flex flex-col overflow-hidden rounded-xl border bg-card shadow-sm\",\n        isError ? \"border-destructive/50\" : \"border-border\",\n      )}\n      aria-label={fillTemplate(labels.uploadingAria, { name: item.name })}\n      aria-busy={!isError}\n    >\n      <div className=\"relative aspect-4/3 w-full overflow-hidden bg-muted\">\n        {item.previewUrl ? (\n          <img src={item.previewUrl} alt=\"\" className=\"size-full object-cover opacity-50\" />\n        ) : null}\n        <div className=\"absolute inset-0 grid place-items-center\">\n          {isError ? (\n            <AlertCircle className=\"size-7 text-destructive\" aria-hidden=\"true\" />\n          ) : (\n            <div className=\"flex flex-col items-center gap-1 text-foreground\">\n              <Loader2 className=\"size-6 animate-spin text-primary\" aria-hidden=\"true\" />\n              <span className=\"font-mono text-xs tabular-nums\">{item.pct}%</span>\n            </div>\n          )}\n        </div>\n      </div>\n      <div className=\"flex min-w-0 flex-col gap-1 px-2.5 py-2\">\n        <div className=\"flex min-w-0 items-center gap-2\">\n          <FileKindBadge kind={item.kind} />\n          <p className=\"truncate text-sm font-medium text-foreground\">{item.name}</p>\n        </div>\n        {isError ? (\n          <div className=\"flex items-center justify-between gap-2\">\n            <span className=\"truncate text-xs text-destructive\">{item.error}</span>\n            <div className=\"flex shrink-0 gap-1\">\n              <Button\n                size=\"sm\"\n                variant=\"outline\"\n                className=\"h-6 px-2 text-xs\"\n                onClick={() => retryUpload(item.tempId)}\n              >\n                {labels.retry}\n              </Button>\n              <Button\n                size=\"icon\"\n                variant=\"ghost\"\n                className=\"size-6\"\n                aria-label={labels.close}\n                onClick={() => dismissUpload(item.tempId)}\n              >\n                <X className=\"size-3.5\" aria-hidden=\"true\" />\n              </Button>\n            </div>\n          </div>\n        ) : (\n          <Progress value={item.pct} className=\"h-1\" />\n        )}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/media-library/parts/upload-progress.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/lazy-code-block.tsx",
      "content": "// Local lazy-boundary wrapper for the CROSS-category code-block viewer.\n//\n// React.lazy can't reliably dynamic-import a cross-category procomp directly:\n// shadcn's path rewriter strips the category for STATIC `import` declarations\n// but NOT for (a) dynamic `import()` calls, nor (b) `export ... from` re-exports\n// (both consumer-smoke proven). So this file uses a plain `import` (which the\n// rewriter strips: `@/registry/components/code/...` → `@/components/code-block/...`)\n// plus a separate `export default`, and file-preview dynamic-imports THIS local\n// relative path (left untouched by the rewriter). Code-split preserved —\n// CodeMirror/shiki stay in this lazily-loaded chunk.\nimport { CodeBlock } from \"@/registry/components/code/code-block/code-block\";\n\nexport default CodeBlock;\n",
      "type": "registry:component",
      "target": "components/media-library/parts/lazy-code-block.tsx"
    },
    {
      "path": "src/registry/components/media/media-library/parts/lazy-markdown-editor.tsx",
      "content": "// Local lazy-boundary wrapper for the CROSS-category markdown-editor viewer.\n// See lazy-code-block.tsx for why this indirection + the `import` (not\n// `export ... from`) form is required. `marked` + CodeMirror stay in this\n// lazily-loaded chunk.\nimport { MarkdownEditor } from \"@/registry/components/forms/markdown-editor/markdown-editor\";\n\nexport default MarkdownEditor;\n",
      "type": "registry:component",
      "target": "components/media-library/parts/lazy-markdown-editor.tsx"
    }
  ],
  "categories": [
    "media"
  ],
  "type": "registry:block"
}