{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "file-manager", "title": "File Manager", "description": "A filesystem-neutral file workspace with scoped state, navigation, search, list and grid views, CRUD, upload, download, copy and move operations, and bounded previews", "dependencies": [ "@base-ui/react@^1.6.0", "daisyui", "emittery@^2.0.0", "lucide-react", "mutative@^1.3.0", "react-resizable-panels@^4.12.1", "zustand@^5.0.14", "zustand-mutative@^1.3.1" ], "files": [ { "path": "registry/default/ui/addressable-frame.tsx", "content": "import type { ComponentPropsWithRef, ReactNode } from 'react';\nimport { cn } from '@/lib/utils';\n\nexport type AddressableFrameProps = ComponentPropsWithRef<'section'>;\n\nexport function AddressableFrame({ className, ...props }: AddressableFrameProps) {\n\treturn (\n\t\t\n\t);\n}\n\nexport type AddressableFrameBarProps = ComponentPropsWithRef<'div'>;\n\nexport function AddressableFrameBar({ className, ...props }: AddressableFrameBarProps) {\n\treturn (\n\t\t\n\t);\n}\n\nexport type AddressableFrameNavigationProps = ComponentPropsWithRef<'nav'>;\n\nexport function AddressableFrameNavigation({ className, ...props }: AddressableFrameNavigationProps) {\n\treturn (\n\t\t\n\t);\n}\n\nexport type AddressableFrameAddressProps = ComponentPropsWithRef<'div'> & {\n\ticon?: ReactNode;\n};\n\nexport function AddressableFrameAddress({ icon, children, className, ...props }: AddressableFrameAddressProps) {\n\treturn (\n\t\t\n\t\t\t{icon ? (\n\t\t\t\t\n\t\t\t\t\t{icon}\n\t\t\t\t\n\t\t\t) : null}\n\t\t\t
\n\t\t\t\t{children}\n\t\t\t
\n\t\t\n\t);\n}\n\nexport type AddressableFrameAddressInputProps = ComponentPropsWithRef<'input'>;\n\nexport function AddressableFrameAddressInput({ className, ...props }: AddressableFrameAddressInputProps) {\n\treturn (\n\t\t\n\t);\n}\n\nexport type AddressableFrameActionsProps = ComponentPropsWithRef<'div'>;\n\nexport function AddressableFrameActions({ className, ...props }: AddressableFrameActionsProps) {\n\treturn (\n\t\t\n\t);\n}\n\nexport type AddressableFrameTitleElement = 'h2' | 'h3' | 'h4' | 'h5' | 'h6';\n\nexport type AddressableFrameHeaderProps = Omit, 'title'> & {\n\ttitle?: ReactNode;\n\ttitleAs?: AddressableFrameTitleElement;\n\ttitleId?: string;\n\tdescription?: ReactNode;\n\tactions?: ReactNode;\n};\n\nexport function AddressableFrameHeader({\n\ttitle,\n\ttitleAs: Title = 'h2',\n\ttitleId,\n\tdescription,\n\tactions,\n\tchildren,\n\tclassName,\n\t...props\n}: AddressableFrameHeaderProps) {\n\treturn (\n\t\t\n\t\t\t{title || description ? (\n\t\t\t\t
\n\t\t\t\t\t{title ? (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{title}\n\t\t\t\t\t\t\n\t\t\t\t\t) : null}\n\t\t\t\t\t{description ? (\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t{description}\n\t\t\t\t\t\t
\n\t\t\t\t\t) : null}\n\t\t\t\t\n\t\t\t) : null}\n\t\t\t{children}\n\t\t\t{actions ?
{actions}
: null}\n\t\t\n\t);\n}\n\nexport type AddressableFrameContentProps = ComponentPropsWithRef<'div'>;\n\nexport function AddressableFrameContent({ className, ...props }: AddressableFrameContentProps) {\n\treturn (\n\t\t\n\t);\n}\n\nexport type AddressableFrameFooterProps = ComponentPropsWithRef<'footer'>;\n\nexport function AddressableFrameFooter({ className, ...props }: AddressableFrameFooterProps) {\n\treturn (\n\t\t\n\t);\n}\n", "type": "registry:ui", "target": "@ui/addressable-frame.tsx" }, { "path": "registry/default/ui/header-content-footer-layout.tsx", "content": "import type { ComponentPropsWithRef, ReactNode } from 'react';\nimport { cn } from '@/lib/utils';\n\nexport type HeaderContentFooterLayoutProps = ComponentPropsWithRef<'div'> & {\n\tcontentClassName?: string;\n\tfooter?: ReactNode;\n\theader?: ReactNode;\n\tscrollContent?: boolean;\n};\n\nexport function HeaderContentFooterLayout({\n\tchildren,\n\tclassName,\n\tcontentClassName,\n\tfooter,\n\theader,\n\tscrollContent = true,\n\t...props\n}: HeaderContentFooterLayoutProps) {\n\treturn (\n\t\t
\n\t\t\t{header}\n\t\t\t\n\t\t\t\t{children}\n\t\t\t
\n\t\t\t{footer}\n\t\t\n\t);\n}\n", "type": "registry:ui", "target": "@ui/header-content-footer-layout.tsx" }, { "path": "registry/default/ui/resizable.tsx", "content": "'use client';\n\nimport { GripVerticalIcon } from 'lucide-react';\nimport * as ResizablePrimitive from 'react-resizable-panels';\nimport { cn } from '@/lib/utils';\n\nexport function ResizablePanelGroup({ className, ...props }: ResizablePrimitive.GroupProps) {\n\treturn (\n\t\t\n\t);\n}\n\nexport function ResizablePanel(props: ResizablePrimitive.PanelProps) {\n\treturn ;\n}\n\nexport function ResizableHandle({\n\twithHandle,\n\tclassName,\n\t...props\n}: ResizablePrimitive.SeparatorProps & { withHandle?: boolean }) {\n\treturn (\n\t\tdiv]:rotate-90',\n\t\t\t\tclassName,\n\t\t\t)}\n\t\t\t{...props}\n\t\t>\n\t\t\t{withHandle ? (\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t) : null}\n\t\t\n\t);\n}\n", "type": "registry:ui", "target": "@ui/resizable.tsx" }, { "path": "registry/default/ui/path-address-bar/path-address-model.ts", "content": "import type {\n\tBuildPathAddressSegmentsOptions,\n\tPathAddressSegment,\n\tPathAddressVisibilityOptions,\n} from './path-address-bar-types';\n\nconst DEFAULT_ROOT_LABEL = '根目录';\n\nexport function buildPathAddressSegments(\n\tpath: string,\n\toptions: BuildPathAddressSegmentsOptions = {},\n): PathAddressSegment[] {\n\tconst rootPath = normalizeAbsolutePath(options.rootPath ?? '/');\n\tconst currentPath = normalizeAbsolutePath(path);\n\tconst rootSegment: PathAddressSegment = {\n\t\tpath: rootPath,\n\t\tlabel: options.rootLabel ?? DEFAULT_ROOT_LABEL,\n\t\tisRoot: true,\n\t\tisCurrent: currentPath === rootPath || !isPathWithinRoot(currentPath, rootPath),\n\t};\n\n\tif (!isPathWithinRoot(currentPath, rootPath) || currentPath === rootPath) return [rootSegment];\n\n\tconst relativePath = rootPath === '/' ? currentPath.slice(1) : currentPath.slice(rootPath.length + 1);\n\tconst relativeSegments = relativePath.split('/');\n\tconst segments: PathAddressSegment[] = [rootSegment];\n\tlet segmentPath = rootPath;\n\n\tfor (const [index, label] of relativeSegments.entries()) {\n\t\tsegmentPath = segmentPath === '/' ? `/${label}` : `${segmentPath}/${label}`;\n\t\tsegments.push({\n\t\t\tpath: segmentPath,\n\t\t\tlabel,\n\t\t\tisRoot: false,\n\t\t\tisCurrent: index === relativeSegments.length - 1,\n\t\t});\n\t}\n\n\treturn segments;\n}\n\nexport function calculatePathAddressVisibleStart(\n\tsegmentWidths: readonly number[],\n\tavailableWidth: number,\n\toptions: PathAddressVisibilityOptions = {},\n): number {\n\tif (segmentWidths.length <= 1) return 0;\n\n\tconst widths = segmentWidths.map(toNonNegativeWidth);\n\tconst width = toNonNegativeWidth(availableWidth);\n\tconst separatorWidth = toNonNegativeWidth(options.separatorWidth ?? 0);\n\tconst overflowWidth = toNonNegativeWidth(options.overflowWidth ?? 0);\n\tconst fullWidth = sumWidths(widths, 0) + separatorWidth * (widths.length - 1);\n\n\tif (fullWidth <= width) return 0;\n\n\tfor (let start = 1; start < widths.length; start += 1) {\n\t\tconst visibleCount = widths.length - start;\n\t\tconst suffixWidth = sumWidths(widths, start) + separatorWidth * Math.max(visibleCount - 1, 0);\n\t\tif (overflowWidth + separatorWidth + suffixWidth <= width) return start;\n\t}\n\n\treturn widths.length - 1;\n}\n\nfunction normalizeAbsolutePath(path: string): string {\n\tconst segments: string[] = [];\n\tconst normalizedSeparators = path.replaceAll('\\\\', '/');\n\n\tfor (const segment of normalizedSeparators.split('/')) {\n\t\tif (!segment || segment === '.') continue;\n\t\tif (segment === '..') {\n\t\t\tsegments.pop();\n\t\t\tcontinue;\n\t\t}\n\t\tsegments.push(segment);\n\t}\n\n\treturn `/${segments.join('/')}`;\n}\n\nfunction isPathWithinRoot(path: string, rootPath: string): boolean {\n\treturn rootPath === '/' || path === rootPath || path.startsWith(`${rootPath}/`);\n}\n\nfunction toNonNegativeWidth(value: number): number {\n\treturn Number.isFinite(value) && value > 0 ? value : 0;\n}\n\nfunction sumWidths(widths: readonly number[], start: number): number {\n\tlet total = 0;\n\tfor (let index = start; index < widths.length; index += 1) total += widths[index];\n\treturn total;\n}\n", "type": "registry:lib", "target": "@components/path-address-bar/path-address-model.ts" }, { "path": "registry/default/ui/path-address-bar/path-address-bar-types.ts", "content": "import type { ComponentPropsWithRef, ReactNode } from 'react';\n\nexport type PathAddressSegment = {\n\tpath: string;\n\tlabel: string;\n\tisRoot: boolean;\n\tisCurrent: boolean;\n};\n\nexport type BuildPathAddressSegmentsOptions = {\n\trootPath?: string;\n\trootLabel?: string;\n};\n\nexport type PathAddressVisibilityOptions = {\n\tseparatorWidth?: number;\n\toverflowWidth?: number;\n};\n\nexport type PathAddressBarMessages = {\n\tnavigationLabel: string;\n\teditLabel: string;\n\tinputLabel: string;\n\toverflowLabel: string;\n\tloadingLabel: string;\n\topenPathLabel: (label: string) => string;\n\tcurrentMenuLabel: (label: string) => string;\n};\n\nexport type PathAddressBarProps = ComponentPropsWithRef<'div'> & {\n\tpath: string;\n\tvalue?: string;\n\tonValueChange?: (value: string) => void;\n\tonValueCommit?: (value: string) => void;\n\tonPathChange?: (path: string) => void;\n\trootPath?: string;\n\trootLabel?: string;\n\teditable?: boolean;\n\tdisabled?: boolean;\n\tloading?: boolean;\n\terror?: ReactNode;\n\tcurrentMenu?: ReactNode;\n\tmessages?: Partial;\n};\n", "type": "registry:lib", "target": "@components/path-address-bar/path-address-bar-types.ts" }, { "path": "registry/default/ui/path-address-bar/path-address-bar-menu-item.tsx", "content": "import { Menu as BaseMenu } from '@base-ui/react/menu';\nimport type { ComponentPropsWithRef } from 'react';\nimport { cn } from '@/lib/utils';\n\nexport type PathAddressBarMenuItemProps = ComponentPropsWithRef;\n\nexport function PathAddressBarMenuItem({ className, ...props }: PathAddressBarMenuItemProps) {\n\treturn (\n\t\t\n\t);\n}\n", "type": "registry:component", "target": "@components/path-address-bar/path-address-bar-menu-item.tsx" }, { "path": "registry/default/ui/path-address-bar/path-address-bar.tsx", "content": "'use client';\n\nimport { Menu as BaseMenu } from '@base-ui/react/menu';\nimport { ChevronDown, ChevronRight, CircleAlert, Ellipsis, Folder, LoaderCircle, Pencil } from 'lucide-react';\nimport type { ChangeEvent, KeyboardEvent, ReactNode } from 'react';\nimport { useEffect, useMemo, useRef, useState } from 'react';\nimport { cn } from '@/lib/utils';\nimport { PathAddressBarMenuItem } from './path-address-bar-menu-item';\nimport type { PathAddressBarMessages, PathAddressBarProps, PathAddressSegment } from './path-address-bar-types';\nimport { buildPathAddressSegments, calculatePathAddressVisibleStart } from './path-address-model';\n\nconst defaultMessages: PathAddressBarMessages = {\n\tnavigationLabel: '当前位置',\n\teditLabel: '编辑路径',\n\tinputLabel: '路径',\n\toverflowLabel: '更多上级目录',\n\tloadingLabel: '正在加载路径',\n\topenPathLabel: (label) => `打开 ${label}`,\n\tcurrentMenuLabel: (label) => `打开 ${label} 菜单`,\n};\n\nconst segmentClassName =\n\t'focus-visible:ring-primary inline-flex h-7 min-w-0 max-w-48 items-center rounded-sm px-2 text-sm font-medium outline-none focus-visible:ring-2';\nconst actionableSegmentClassName = 'hover:bg-base-200 disabled:pointer-events-none disabled:opacity-40';\nconst separatorClassName = 'text-base-content/35 flex size-4 shrink-0 items-center justify-center';\nconst overflowClassName =\n\t'hover:bg-base-200 focus-visible:ring-primary inline-flex size-7 shrink-0 items-center justify-center rounded-sm outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-40';\n\nexport function PathAddressBar({\n\tpath,\n\tvalue,\n\tonValueChange,\n\tonValueCommit,\n\tonPathChange,\n\trootPath = '/',\n\trootLabel = '根目录',\n\teditable = true,\n\tdisabled = false,\n\tloading = false,\n\terror,\n\tcurrentMenu,\n\tmessages: messageOverrides,\n\tclassName,\n\tchildren,\n\tonKeyDown,\n\t'aria-label': ariaLabel,\n\t...props\n}: PathAddressBarProps) {\n\tconst messages = useMemo(() => ({ ...defaultMessages, ...messageOverrides }), [messageOverrides]);\n\tconst segments = useMemo(() => buildPathAddressSegments(path, { rootPath, rootLabel }), [path, rootLabel, rootPath]);\n\tconst visibilityKey = useMemo(\n\t\t() => segments.map((segment) => `${segment.path}\\x00${segment.label}`).join('\\x01'),\n\t\t[segments],\n\t);\n\tconst [visibility, setVisibility] = useState({ key: visibilityKey, start: 0 });\n\tconst visibleStart = visibility.key === visibilityKey ? Math.min(visibility.start, segments.length - 1) : 0;\n\tconst hiddenSegments = visibleStart > 0 ? segments.slice(0, visibleStart) : [];\n\tconst visibleSegments = segments.slice(visibleStart);\n\tconst [editing, setEditing] = useState(false);\n\tconst [internalValue, setInternalValue] = useState(path);\n\tconst editValue = value ?? internalValue;\n\tconst inputRef = useRef(null);\n\tconst previousPathRef = useRef(path);\n\tconst commitHandledRef = useRef(false);\n\tconst viewportRef = useRef(null);\n\tconst segmentMeasureRefs = useRef>([]);\n\tconst separatorMeasureRef = useRef(null);\n\tconst overflowMeasureRef = useRef(null);\n\n\tuseEffect(() => {\n\t\tif (previousPathRef.current === path) return;\n\t\tpreviousPathRef.current = path;\n\t\tcommitHandledRef.current = false;\n\t\tsetInternalValue(path);\n\t\tsetEditing(false);\n\t}, [path]);\n\n\tuseEffect(() => {\n\t\tif (!editing) return;\n\t\tinputRef.current?.focus();\n\t\tinputRef.current?.select();\n\t}, [editing]);\n\n\tuseEffect(() => {\n\t\tif (typeof ResizeObserver === 'undefined') {\n\t\t\tsetVisibility({ key: visibilityKey, start: 0 });\n\t\t\treturn;\n\t\t}\n\n\t\tconst viewport = viewportRef.current;\n\t\tconst measuredSegments = segmentMeasureRefs.current.slice(0, segments.length);\n\t\tconst separator = separatorMeasureRef.current;\n\t\tconst overflow = overflowMeasureRef.current;\n\t\tif (!viewport || !separator || !overflow || measuredSegments.some((segment) => !segment)) return;\n\n\t\tconst measure = () => {\n\t\t\tconst widths = measuredSegments.map((segment) => segment?.getBoundingClientRect().width ?? 0);\n\t\t\tconst start = calculatePathAddressVisibleStart(widths, viewport.getBoundingClientRect().width, {\n\t\t\t\tseparatorWidth: separator.getBoundingClientRect().width,\n\t\t\t\toverflowWidth: overflow.getBoundingClientRect().width,\n\t\t\t});\n\t\t\tsetVisibility((current) =>\n\t\t\t\tcurrent.key === visibilityKey && current.start === start ? current : { key: visibilityKey, start },\n\t\t\t);\n\t\t};\n\t\tconst observer = new ResizeObserver(measure);\n\t\tobserver.observe(viewport);\n\t\tobserver.observe(separator);\n\t\tobserver.observe(overflow);\n\t\tfor (const segment of measuredSegments) {\n\t\t\tif (segment) observer.observe(segment);\n\t\t}\n\t\tmeasure();\n\t\treturn () => observer.disconnect();\n\t}, [segments, visibilityKey]);\n\n\tconst beginEditing = () => {\n\t\tif (!editable || disabled) return;\n\t\tcommitHandledRef.current = false;\n\t\tsetInternalValue(path);\n\t\tsetEditing(true);\n\t};\n\tconst resetEditing = () => {\n\t\tif (commitHandledRef.current) return;\n\t\tif (editValue !== path) onValueChange?.(path);\n\t\tsetInternalValue(path);\n\t\tsetEditing(false);\n\t};\n\tconst commitEditing = () => {\n\t\tif (commitHandledRef.current || disabled) return;\n\t\tcommitHandledRef.current = true;\n\t\tif (onValueCommit) onValueCommit(editValue);\n\t\telse if (editValue !== path) onPathChange?.(editValue);\n\t\tsetEditing(false);\n\t};\n\tconst handleValueChange = (event: ChangeEvent) => {\n\t\tconst nextValue = event.currentTarget.value;\n\t\tif (value === undefined) setInternalValue(nextValue);\n\t\tonValueChange?.(nextValue);\n\t};\n\tconst handleInputKeyDown = (event: KeyboardEvent) => {\n\t\tif (event.key === 'Enter' && !event.nativeEvent.isComposing) {\n\t\t\tevent.preventDefault();\n\t\t\tcommitEditing();\n\t\t\treturn;\n\t\t}\n\t\tif (event.key === 'Escape') {\n\t\t\tevent.preventDefault();\n\t\t\tevent.stopPropagation();\n\t\t\tresetEditing();\n\t\t}\n\t};\n\tconst handleRootKeyDown = (event: KeyboardEvent) => {\n\t\tonKeyDown?.(event);\n\t\tif (event.defaultPrevented) return;\n\t\tif ((event.ctrlKey || event.metaKey) && !event.altKey && event.key.toLocaleLowerCase() === 'l') {\n\t\t\tevent.preventDefault();\n\t\t\tif (editing) inputRef.current?.select();\n\t\t\telse beginEditing();\n\t\t}\n\t};\n\n\treturn (\n\t\t\n\t\t\t{editing ? (\n\t\t\t\t\n\t\t\t) : (\n\t\t\t\t<>\n\t\t\t\t\t\n\t\t\t\t\t\t{hiddenSegments.length ? (\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t{visibleSegments.map((segment, index) => (\n\t\t\t\t\t\t\t 0 || index > 0}\n\t\t\t\t\t\t\t\tonPathChange={onPathChange}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t))}\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{segments.map((segment, index) => (\n\t\t\t\t\t\t\t {\n\t\t\t\t\t\t\t\t\tsegmentMeasureRefs.current[index] = node;\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tclassName={cn(segmentClassName, 'shrink-0')}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{segment.label}\n\t\t\t\t\t\t\t\t{segment.isCurrent && currentMenu ? (\n\t\t\t\t\t\t\t\t\t