{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "folder-preview", "type": "registry:ui", "dependencies": [ "framer-motion" ], "files": [ { "path": "components/ui/folder-preview.tsx", "content": "\"use client\";\r\n\r\nimport * as React from \"react\";\r\nimport { motion, AnimatePresence, useAnimation, Variants } from \"framer-motion\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\n// ============================================\r\n// SVG Icon Components\r\n// ============================================\r\n\r\nconst FolderBackIcon = ({ className }: { className?: string }) => (\r\n \r\n \r\n \r\n);\r\n\r\nconst FolderCoverIcon = ({ className }: { className?: string }) => (\r\n \r\n \r\n \r\n);\r\n\r\nconst UsersIcon = ({ className }: { className?: string }) => (\r\n \r\n \r\n \r\n \r\n);\r\n\r\nconst GlobeIcon = ({ className }: { className?: string }) => (\r\n \r\n \r\n \r\n \r\n);\r\n\r\nconst PadlockIcon = ({ className }: { className?: string }) => (\r\n \r\n \r\n \r\n);\r\n\r\nconst CloudIcon = ({ className }: { className?: string }) => (\r\n \r\n \r\n \r\n);\r\n\r\nconst FileIcon = ({ className }: { className?: string }) => (\r\n \r\n \r\n \r\n \r\n);\r\n\r\n// ============================================\r\n// Types & Interfaces\r\n// ============================================\r\n\r\nexport type FolderVariant =\r\n | \"devi\"\r\n | \"rudras\"\r\n | \"ardra\"\r\n | \"shakti\"\r\n | \"kubera\"\r\n | \"hari\"\r\n | \"ravi\"\r\n | \"durga\"\r\n | \"nandi\";\r\n\r\nexport interface FolderPreviewProps {\r\n variant?: FolderVariant;\r\n images?: string[];\r\n files?: { name: string; type?: \"txt\" | \"gif\" | \"mp3\" | \"default\" }[];\r\n label?: string;\r\n size?: \"sm\" | \"md\" | \"lg\";\r\n className?: string;\r\n onClick?: () => void;\r\n}\r\n\r\n// ============================================\r\n// Color Schemes for Each Variant\r\n// ============================================\r\n\r\nconst variantColors: Record<\r\n FolderVariant,\r\n {\r\n back: string;\r\n cover: string;\r\n deco: string;\r\n caption: string;\r\n bg: string;\r\n }\r\n> = {\r\n devi: {\r\n back: \"text-gray-500\",\r\n cover: \"text-gray-400\",\r\n deco: \"text-gray-400 brightness-125\",\r\n caption: \"text-gray-800 dark:text-gray-200\",\r\n bg: \"bg-gray-100 dark:bg-gray-900\",\r\n },\r\n rudras: {\r\n back: \"text-gray-700 dark:text-gray-600\",\r\n cover: \"text-gray-600 dark:text-gray-500\",\r\n deco: \"text-gray-400\",\r\n caption: \"text-blue-600 dark:text-blue-400\",\r\n bg: \"bg-slate-200 dark:bg-slate-800\",\r\n },\r\n ardra: {\r\n back: \"text-blue-800 dark:text-blue-700\",\r\n cover: \"text-blue-600 dark:text-blue-500\",\r\n deco: \"text-blue-700 dark:text-blue-600\",\r\n caption: \"text-blue-500 dark:text-blue-400\",\r\n bg: \"bg-gray-800 dark:bg-gray-950\",\r\n },\r\n shakti: {\r\n back: \"text-indigo-800\",\r\n cover: \"text-indigo-700\",\r\n deco: \"text-indigo-800\",\r\n caption: \"text-green-400\",\r\n bg: \"bg-blue-600 dark:bg-blue-800\",\r\n },\r\n kubera: {\r\n back: \"text-gray-900\",\r\n cover: \"text-gray-700\",\r\n deco: \"text-gray-600\",\r\n caption: \"text-gray-900 dark:text-gray-100\",\r\n bg: \"bg-emerald-400 dark:bg-emerald-600\",\r\n },\r\n hari: {\r\n back: \"text-blue-800\",\r\n cover: \"text-blue-700\",\r\n deco: \"text-blue-800\",\r\n caption: \"text-yellow-400\",\r\n bg: \"bg-sky-500 dark:bg-sky-700\",\r\n },\r\n ravi: {\r\n back: \"text-gray-900\",\r\n cover: \"text-gray-700\",\r\n deco: \"text-black dark:text-white\",\r\n caption: \"text-gray-900 dark:text-gray-100\",\r\n bg: \"bg-gray-200 dark:bg-gray-800\",\r\n },\r\n durga: {\r\n back: \"text-green-600\",\r\n cover: \"text-green-500\",\r\n deco: \"text-green-600\",\r\n caption: \"text-green-400 font-mono\",\r\n bg: \"bg-gray-900 dark:bg-black\",\r\n },\r\n nandi: {\r\n back: \"text-amber-500\",\r\n cover: \"text-amber-400\",\r\n deco: \"text-amber-500\",\r\n caption: \"text-gray-900 dark:text-gray-100\",\r\n bg: \"bg-green-100 dark:bg-green-950\",\r\n },\r\n};\r\n\r\n// ============================================\r\n// Size Configuration\r\n// ============================================\r\n\r\nconst sizeConfig = {\r\n sm: {\r\n folder: \"w-16\",\r\n thumb: \"w-10 h-10\",\r\n deco: \"w-4 h-4\",\r\n caption: \"text-xs\",\r\n },\r\n md: {\r\n folder: \"w-24\",\r\n thumb: \"w-14 h-14\",\r\n deco: \"w-6 h-6\",\r\n caption: \"text-sm\",\r\n },\r\n lg: {\r\n folder: \"w-32\",\r\n thumb: \"w-20 h-20\",\r\n deco: \"w-8 h-8\",\r\n caption: \"text-base\",\r\n },\r\n};\r\n\r\n// ============================================\r\n// Animation Variants\r\n// ============================================\r\n\r\nconst createCircularPositions = (count: number, radius: number = 120) => {\r\n return Array.from({ length: count }, (_, i) => {\r\n const startAngle = Math.PI / count;\r\n const angle = startAngle / 2 + startAngle * i;\r\n return {\r\n x: Math.round(radius * Math.cos(angle)),\r\n y: Math.round(-radius * Math.sin(angle)),\r\n };\r\n });\r\n};\r\n\r\n// ============================================\r\n// Individual Folder Components for each Variant\r\n// ============================================\r\n\r\nconst DeviFolder: React.FC<{\r\n images: string[];\r\n isHovered: boolean;\r\n colors: typeof variantColors.devi;\r\n sizes: typeof sizeConfig.md;\r\n label?: string;\r\n}> = ({ images, isHovered, colors, sizes, label }) => {\r\n const positions = createCircularPositions(images.length, 80);\r\n\r\n return (\r\n
\r\n {/* Previews - positioned from folder center */}\r\n
\r\n {images.map((img, i) => (\r\n \r\n ))}\r\n
\r\n\r\n {/* Folder */}\r\n
\r\n {/* Back */}\r\n
\r\n \r\n
\r\n\r\n {/* Cover */}\r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n\r\n {label && (\r\n

\r\n {label}\r\n

\r\n )}\r\n \r\n );\r\n};\r\n\r\n\r\nconst RudrasFolder: React.FC<{\r\n images: string[];\r\n isHovered: boolean;\r\n colors: typeof variantColors.rudras;\r\n sizes: typeof sizeConfig.md;\r\n label?: string;\r\n}> = ({ images, isHovered, colors, sizes, label }) => {\r\n const positions = createCircularPositions(images.length, 80);\r\n\r\n return (\r\n
\r\n {/* Previews - positioned from folder center */}\r\n
\r\n {images.map((img, i) => (\r\n \r\n ))}\r\n
\r\n\r\n {/* Folder */}\r\n
\r\n {/* Back */}\r\n
\r\n \r\n
\r\n\r\n {/* Paper Sheet Deco */}\r\n
\r\n\r\n {/* Cover */}\r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n\r\n {label && (\r\n

\r\n {label}\r\n

\r\n )}\r\n
\r\n );\r\n};\r\n\r\n\r\nconst ArdraFolder: React.FC<{\r\n images: string[];\r\n isHovered: boolean;\r\n colors: typeof variantColors.ardra;\r\n sizes: typeof sizeConfig.md;\r\n label?: string;\r\n}> = ({ images, isHovered, colors, sizes, label }) => {\r\n const [randomPositions] = React.useState(() =>\r\n images.map((_, i) => {\r\n const radius = 60 + Math.random() * 20;\r\n const angle = (2 * (i + 1) * Math.PI) / images.length;\r\n return {\r\n x: Math.round(radius * Math.cos(angle)),\r\n y: Math.round(radius * Math.sin(angle)),\r\n rotate: Math.random() * 6 - 3,\r\n };\r\n })\r\n );\r\n\r\n return (\r\n
\r\n {/* Feedback Circle */}\r\n \r\n \r\n \r\n\r\n {/* Previews - positioned from folder center */}\r\n
\r\n {images.map((img, i) => (\r\n \r\n ))}\r\n
\r\n\r\n {/* Folder */}\r\n \r\n {/* Back */}\r\n
\r\n \r\n
\r\n\r\n {/* Cover */}\r\n
\r\n \r\n \r\n \r\n
\r\n
\r\n \r\n\r\n {label && (\r\n

\r\n {label}\r\n

\r\n )}\r\n \r\n );\r\n};\r\n\r\nconst ShaktiFolder: React.FC<{\r\n images: string[];\r\n isHovered: boolean;\r\n colors: typeof variantColors.shakti;\r\n sizes: typeof sizeConfig.md;\r\n label?: string;\r\n}> = ({ images, isHovered, colors, sizes, label }) => {\r\n return (\r\n \r\n
\r\n {/* Back */}\r\n
\r\n \r\n
\r\n\r\n {/* Previews - Fan animation */}\r\n
\r\n {images.map((img, i) => (\r\n \r\n ))}\r\n
\r\n\r\n {/* Cover */}\r\n
\r\n \r\n \r\n \r\n
\r\n
\r\n \r\n\r\n {label && (\r\n

\r\n {label}\r\n

\r\n )}\r\n \r\n );\r\n};\r\n\r\nconst KuberaFolder: React.FC<{\r\n images: string[];\r\n isHovered: boolean;\r\n colors: typeof variantColors.kubera;\r\n sizes: typeof sizeConfig.md;\r\n label?: string;\r\n}> = ({ images, isHovered, colors, sizes, label }) => {\r\n return (\r\n
\r\n
\r\n {/* Back */}\r\n
\r\n \r\n
\r\n\r\n {/* Paper Sheet Deco */}\r\n
\r\n\r\n {/* Floating Previews */}\r\n
\r\n {images.map((img, i) => (\r\n \r\n ))}\r\n
\r\n\r\n {/* Cover */}\r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n\r\n {label && (\r\n

\r\n {label}\r\n

\r\n )}\r\n
\r\n );\r\n};\r\n\r\nconst HariFolder: React.FC<{\r\n images: string[];\r\n isHovered: boolean;\r\n colors: typeof variantColors.hari;\r\n sizes: typeof sizeConfig.md;\r\n label?: string;\r\n}> = ({ images, isHovered, colors, sizes, label }) => {\r\n const positions = createCircularPositions(images.length, 120);\r\n\r\n return (\r\n
\r\n {/* Feedback Circle */}\r\n \r\n\r\n {/* Jumping Previews */}\r\n
\r\n {images.map((img, i) => (\r\n \r\n ))}\r\n
\r\n\r\n \r\n {/* Back */}\r\n
\r\n \r\n
\r\n\r\n {/* Paper Sheet Deco */}\r\n
\r\n\r\n {/* Cover */}\r\n \r\n \r\n \r\n \r\n
\r\n \r\n \r\n\r\n \r\n {label}\r\n \r\n
\r\n );\r\n};\r\n\r\nconst RaviFolder: React.FC<{\r\n images: string[];\r\n isHovered: boolean;\r\n colors: typeof variantColors.ravi;\r\n sizes: typeof sizeConfig.md;\r\n label?: string;\r\n}> = ({ images, isHovered, colors, sizes, label }) => {\r\n // Reorder images for card-spread effect\r\n const reorder = (arr: string[]) => {\r\n const result: string[] = [];\r\n let i = Math.ceil(arr.length / 2);\r\n let j = i - 1;\r\n while (j >= 0) {\r\n result.push(arr[j--]);\r\n if (i < arr.length) result.push(arr[i++]);\r\n }\r\n return result;\r\n };\r\n\r\n const orderedImages = React.useMemo(() => reorder(images), [images]);\r\n\r\n return (\r\n
\r\n {/* Feedback Circle */}\r\n \r\n\r\n
\r\n {/* Back */}\r\n
\r\n \r\n
\r\n\r\n {/* Paper Sheet */}\r\n
\r\n\r\n {/* Card Spread Previews */}\r\n
\r\n {orderedImages.map((img, i) => {\r\n const interval = 60;\r\n const c = orderedImages.length;\r\n const x =\r\n -interval * Math.floor(c / 2) + interval * i + (c % 2 === 0 ? interval / 2 : 0);\r\n const rotateInterval = 20;\r\n const rotate =\r\n -rotateInterval * Math.floor(c / 2) +\r\n rotateInterval * i +\r\n (c % 2 === 0 ? rotateInterval / 2 : 0);\r\n\r\n return (\r\n \r\n );\r\n })}\r\n
\r\n\r\n {/* Cover */}\r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n\r\n {label && (\r\n

\r\n {label}\r\n

\r\n )}\r\n
\r\n );\r\n};\r\n\r\nconst DurgaFolder: React.FC<{\r\n files: { name: string; type?: string }[];\r\n isHovered: boolean;\r\n colors: typeof variantColors.durga;\r\n sizes: typeof sizeConfig.md;\r\n label?: string;\r\n}> = ({ files, isHovered, colors, sizes, label }) => {\r\n return (\r\n
\r\n {/* Text Preview - shows file list inside a tooltip bubble */}\r\n \r\n {files.slice(0, 6).map((file, i) => (\r\n \r\n {file.name}\r\n \r\n ))}\r\n \r\n\r\n {/* Folder */}\r\n
\r\n {/* Back */}\r\n
\r\n \r\n
\r\n\r\n {/* Paper Sheet */}\r\n
\r\n\r\n {/* Cover */}\r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n\r\n {label && (\r\n

\r\n {label}\r\n

\r\n )}\r\n
\r\n );\r\n};\r\n\r\nconst NandiFolder: React.FC<{\r\n files: { name: string; type?: string }[];\r\n isHovered: boolean;\r\n colors: typeof variantColors.nandi;\r\n sizes: typeof sizeConfig.md;\r\n label?: string;\r\n}> = ({ files, isHovered, colors, sizes, label }) => {\r\n const fileColorMap: Record = {\r\n txt: \"fill-blue-300\",\r\n gif: \"fill-teal-400\",\r\n mp3: \"fill-amber-400\",\r\n default: \"fill-gray-400\",\r\n };\r\n\r\n return (\r\n
\r\n {/* Magnifier Preview - compact bubble above folder */}\r\n \r\n {files.slice(0, 6).map((file, i) => (\r\n
\r\n \r\n \r\n {file.name}\r\n \r\n
\r\n ))}\r\n \r\n\r\n {/* Folder */}\r\n
\r\n {/* Back */}\r\n
\r\n \r\n
\r\n\r\n {/* Paper Sheet */}\r\n
\r\n\r\n {/* Cover */}\r\n \r\n \r\n \r\n \r\n
\r\n \r\n
\r\n\r\n {label && (\r\n

\r\n {label}\r\n

\r\n )}\r\n
\r\n );\r\n};\r\n\r\n// ============================================\r\n// Main FolderPreview Component\r\n// ============================================\r\n\r\nexport const FolderPreview = React.forwardRef(\r\n (\r\n {\r\n variant = \"devi\",\r\n images = [],\r\n files = [],\r\n label,\r\n size = \"md\",\r\n className,\r\n onClick,\r\n },\r\n ref\r\n ) => {\r\n const [isHovered, setIsHovered] = React.useState(false);\r\n const colors = variantColors[variant];\r\n const sizes = sizeConfig[size];\r\n\r\n const defaultImages = [\r\n \"/folder-preview/user1.svg\",\r\n \"/folder-preview/user2.svg\",\r\n \"/folder-preview/user3.svg\",\r\n \"/folder-preview/user4.svg\",\r\n \"/folder-preview/user5.svg\",\r\n ];\r\n\r\n const defaultFiles = [\r\n { name: \"docs\", type: \"default\" as const },\r\n { name: \"template\", type: \"default\" as const },\r\n { name: \"readme.md\", type: \"txt\" as const },\r\n { name: \"app.js\", type: \"txt\" as const },\r\n { name: \"test.sh\", type: \"txt\" as const },\r\n { name: \"package.json\", type: \"txt\" as const },\r\n { name: \"logo.svg\", type: \"default\" as const },\r\n { name: \"...\", type: \"default\" as const },\r\n ];\r\n\r\n const imageList = images.length > 0 ? images : defaultImages;\r\n const fileList = files.length > 0 ? files : defaultFiles;\r\n\r\n const renderFolder = () => {\r\n const props = { isHovered, colors, sizes, label };\r\n\r\n switch (variant) {\r\n case \"devi\":\r\n return ;\r\n case \"rudras\":\r\n return ;\r\n case \"ardra\":\r\n return ;\r\n case \"shakti\":\r\n return ;\r\n case \"kubera\":\r\n return ;\r\n case \"hari\":\r\n return ;\r\n case \"ravi\":\r\n return ;\r\n case \"durga\":\r\n return ;\r\n case \"nandi\":\r\n return ;\r\n default:\r\n return ;\r\n }\r\n };\r\n\r\n return (\r\n setIsHovered(true)}\r\n onMouseLeave={() => setIsHovered(false)}\r\n onClick={onClick}\r\n >\r\n {renderFolder()}\r\n \r\n );\r\n }\r\n);\r\n\r\nFolderPreview.displayName = \"FolderPreview\";\r\n\r\nexport default FolderPreview;\r\n", "type": "registry:ui", "target": "components/ui/folder-preview.tsx" } ] }