{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "3d-folder",
  "title": "3D Folder",
  "description": "A 3D animated folder component that reveals project cards on hover and expands into a smooth image lightbox with keyboard navigation.",
  "dependencies": [
    "react",
    "@tabler/icons-react"
  ],
  "files": [
    {
      "path": "registry/gammaui/3d-folder.tsx",
      "content": "\"use client\"\n\nimport {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\"\nimport type React from \"react\"\nimport {\n  IconChevronLeft,\n  IconChevronRight,\n  IconExternalLink,\n  IconX,\n} from \"@tabler/icons-react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface Project {\n  id: string\n  image: string\n  title: string\n}\n\ninterface AnimatedFolderProps {\n  title: string\n  projects: Project[]\n  className?: string\n}\n\nexport function AnimatedFolder({\n  title,\n  projects,\n  className,\n}: AnimatedFolderProps) {\n  const [isOpened, setIsOpened] = useState(false)\n  const [selectedIndex, setSelectedIndex] = useState<number | null>(null)\n  const [sourceRect, setSourceRect] = useState<DOMRect | null>(null)\n  const [hiddenCardId, setHiddenCardId] = useState<string | null>(null)\n  const cardRefs = useRef<(HTMLDivElement | null)[]>([])\n\n  const handleProjectClick = (project: Project, index: number) => {\n    const cardEl = cardRefs.current[index]\n    if (cardEl) {\n      setSourceRect(cardEl.getBoundingClientRect())\n    }\n    setSelectedIndex(index)\n    setHiddenCardId(project.id)\n  }\n\n  const handleCloseLightbox = () => {\n    setSelectedIndex(null)\n    setSourceRect(null)\n  }\n\n  const handleCloseComplete = () => {\n    setHiddenCardId(null)\n  }\n\n  const handleNavigate = (newIndex: number) => {\n    setSelectedIndex(newIndex)\n    setHiddenCardId(projects[newIndex]?.id || null)\n  }\n\n  return (\n    <>\n      <div\n        className={cn(\n          \"relative flex flex-col items-center justify-center\",\n          \"cursor-pointer rounded-2xl p-8\",\n          \"transition-all duration-500 ease-out\",\n          \"group\",\n          className\n        )}\n        style={{\n          minWidth: \"280px\",\n          minHeight: \"320px\",\n          perspective: \"1000px\",\n        }}\n      >\n        {/* Subtle background glow on hover */}\n        <div\n          className=\"absolute inset-0 rounded-2xl transition-opacity duration-500\"\n          style={{\n            background:\n              \"radial-gradient(circle at 50% 70%, var(--accent) 0%, transparent 70%)\",\n            opacity: isOpened ? 0.08 : 0,\n          }}\n        />\n\n        <div\n          className=\"relative mb-4 flex items-center justify-center\"\n          style={{ height: \"160px\", width: \"200px\" }}\n          onClick={() => setIsOpened((prev) => !prev)}\n        >\n          {/* Folder back layer - z-index 10 */}\n          <div\n            className=\"absolute h-24 w-32 rounded-[10px] bg-[#437cb2] shadow-md\"\n            style={{\n              transformOrigin: \"bottom center\",\n              transform: isOpened ? \"rotateX(-15deg)\" : \"rotateX(0deg)\",\n              transition: \"transform 500ms cubic-bezier(0.34, 1.56, 0.64, 1)\",\n              zIndex: 10,\n            }}\n          />\n\n          {/* Folder tab - z-index 10 */}\n          <div\n            className=\"absolute h-4 w-12 rounded-t-[7px] bg-[#5184b3]\"\n            style={{\n              top: \"calc(50% - 48px - 12px)\",\n              left: \"calc(50% - 64px + 2px)\",\n              transformOrigin: \"bottom center\",\n              transform: isOpened\n                ? \"rotateX(-25deg) translateY(2px)\"\n                : \"rotateX(0deg)\",\n              transition: \"transform 500ms cubic-bezier(0.34, 1.56, 0.64, 1)\",\n              zIndex: 10,\n            }}\n          />\n\n          {/* Project cards - z-index 20, between back and front */}\n          <div\n            className=\"absolute\"\n            style={{\n              top: \"50%\",\n              left: \"50%\",\n              transform: \"translate(-50%, -50%)\",\n              zIndex: 20,\n            }}\n          >\n            {projects.slice(0, 3).map((project, index) => (\n              <ProjectCard\n                key={project.id}\n                ref={(el) => {\n                  cardRefs.current[index] = el\n                }}\n                image={project.image}\n                title={project.title}\n                delay={index * 80}\n                isVisible={isOpened}\n                index={index}\n                onClick={() => handleProjectClick(project, index)}\n                isSelected={hiddenCardId === project.id}\n              />\n            ))}\n          </div>\n\n          {/* Folder front layer - z-index 30 */}\n          <div\n            className=\"absolute h-24 w-32 rounded-[10px] bg-[#5184b3] shadow-lg\"\n            style={{\n              top: \"calc(50% - 48px + 4px)\",\n              transformOrigin: \"bottom center\",\n              transform: isOpened\n                ? \"rotateX(25deg) translateY(8px)\"\n                : \"rotateX(0deg)\",\n              transition: \"transform 500ms cubic-bezier(0.34, 1.56, 0.64, 1)\",\n              zIndex: 30,\n            }}\n          />\n\n          {/* Folder shine effect - z-index 31 */}\n          <div\n            className=\"pointer-events-none absolute h-24 w-32 overflow-hidden rounded-[10px]\"\n            style={{\n              top: \"calc(50% - 48px + 4px)\",\n              background:\n                \"linear-gradient(135deg, rgba(255,255,255,0.3) 0%, transparent 50%)\",\n              transformOrigin: \"bottom center\",\n              transform: isOpened\n                ? \"rotateX(25deg) translateY(8px)\"\n                : \"rotateX(0deg)\",\n              transition: \"transform 500ms cubic-bezier(0.34, 1.56, 0.64, 1)\",\n              zIndex: 31,\n            }}\n          />\n        </div>\n\n        {/* Folder title */}\n        <h3\n          className=\"text-foreground text-lg font-semibold transition-all duration-300\"\n          style={{\n            transform: isOpened ? \"translateY(4px)\" : \"translateY(0)\",\n          }}\n        >\n          {title}\n        </h3>\n\n        {/* Project count */}\n        <p\n          className=\"text-muted-foreground text-sm transition-all duration-300\"\n          style={{\n            opacity: isOpened ? 0.7 : 1,\n          }}\n        >\n          {projects.length} Documents\n        </p>\n\n        {/* Hover hint */}\n        <div\n          className=\"text-muted-foreground absolute bottom-4 left-1/2 flex -translate-x-1/2 items-center gap-1.5 text-xs transition-all duration-300\"\n          style={{\n            opacity: isOpened ? 0 : 0.6,\n            transform: isOpened ? \"translateY(10px)\" : \"translateY(0)\",\n          }}\n        >\n          <span>Click to explore</span>\n        </div>\n      </div>\n\n      <ImageLightbox\n        projects={projects.slice(0, 3)}\n        currentIndex={selectedIndex ?? 0}\n        isOpen={selectedIndex !== null}\n        onClose={handleCloseLightbox}\n        sourceRect={sourceRect}\n        onCloseComplete={handleCloseComplete}\n        onNavigate={handleNavigate}\n      />\n    </>\n  )\n}\n\ninterface Project {\n  id: string\n  image: string\n  title: string\n}\n\ninterface ImageLightboxProps {\n  projects: Project[]\n  currentIndex: number\n  isOpen: boolean\n  onClose: () => void\n  sourceRect: DOMRect | null\n  onCloseComplete?: () => void\n  onNavigate: (index: number) => void\n}\n\nexport function ImageLightbox({\n  projects,\n  currentIndex,\n  isOpen,\n  onClose,\n  sourceRect,\n  onCloseComplete,\n  onNavigate,\n}: ImageLightboxProps) {\n  const [animationPhase, setAnimationPhase] = useState<\n    \"initial\" | \"animating\" | \"complete\"\n  >(\"initial\")\n  const [isClosing, setIsClosing] = useState(false)\n  const [shouldRender, setShouldRender] = useState(false)\n  const [internalIndex, setInternalIndex] = useState(currentIndex)\n  const [prevIndex, setPrevIndex] = useState(currentIndex)\n  const [isSliding, setIsSliding] = useState(false)\n  const [slideDirection, setSlideDirection] = useState<\"left\" | \"right\">(\n    \"right\"\n  )\n  const containerRef = useRef<HTMLDivElement>(null)\n\n  const totalProjects = projects.length\n  const hasNext = internalIndex < totalProjects - 1\n  const hasPrev = internalIndex > 0\n\n  const currentProject = projects[internalIndex]\n\n  useEffect(() => {\n    if (isOpen && currentIndex !== internalIndex && !isSliding) {\n      const direction = currentIndex > internalIndex ? \"left\" : \"right\"\n      setSlideDirection(direction)\n      setPrevIndex(internalIndex)\n      setIsSliding(true)\n\n      const timer = setTimeout(() => {\n        setInternalIndex(currentIndex)\n        setIsSliding(false)\n      }, 400)\n\n      return () => clearTimeout(timer)\n    }\n  }, [currentIndex, isOpen])\n\n  useEffect(() => {\n    if (isOpen) {\n      setInternalIndex(currentIndex)\n      setPrevIndex(currentIndex)\n      setIsSliding(false)\n    }\n  }, [isOpen])\n\n  const navigateNext = useCallback(() => {\n    if (internalIndex >= totalProjects - 1 || isSliding) return\n    onNavigate(internalIndex + 1)\n  }, [internalIndex, totalProjects, isSliding, onNavigate])\n\n  const navigatePrev = useCallback(() => {\n    if (internalIndex <= 0 || isSliding) return\n    onNavigate(internalIndex - 1)\n  }, [internalIndex, isSliding, onNavigate])\n\n  const handleClose = useCallback(() => {\n    setIsClosing(true)\n    onClose()\n    setTimeout(() => {\n      setIsClosing(false)\n      setShouldRender(false)\n      setAnimationPhase(\"initial\")\n      onCloseComplete?.()\n    }, 400)\n  }, [onClose, onCloseComplete])\n\n  useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (!isOpen) return\n      if (e.key === \"Escape\") handleClose()\n      if (e.key === \"ArrowRight\") navigateNext()\n      if (e.key === \"ArrowLeft\") navigatePrev()\n    }\n\n    window.addEventListener(\"keydown\", handleKeyDown)\n    if (isOpen) {\n      document.body.style.overflow = \"hidden\"\n    }\n\n    return () => {\n      window.removeEventListener(\"keydown\", handleKeyDown)\n      document.body.style.overflow = \"\"\n    }\n  }, [isOpen, handleClose, navigateNext, navigatePrev])\n\n  useLayoutEffect(() => {\n    if (isOpen && sourceRect) {\n      setShouldRender(true)\n      setAnimationPhase(\"initial\")\n      setIsClosing(false)\n      requestAnimationFrame(() => {\n        requestAnimationFrame(() => {\n          setAnimationPhase(\"animating\")\n        })\n      })\n      const timer = setTimeout(() => {\n        setAnimationPhase(\"complete\")\n      }, 500)\n      return () => clearTimeout(timer)\n    }\n  }, [isOpen, sourceRect])\n\n  const handleDotClick = (idx: number) => {\n    if (isSliding || idx === internalIndex) return\n    onNavigate(idx)\n  }\n\n  if (!shouldRender || !currentProject) return null\n\n  const getInitialStyles = (): React.CSSProperties => {\n    if (!sourceRect) return {}\n\n    const viewportWidth = window.innerWidth\n    const viewportHeight = window.innerHeight\n    const targetWidth = Math.min(768, viewportWidth - 64)\n    const targetHeight = Math.min(viewportHeight * 0.85, 600)\n\n    const targetX = (viewportWidth - targetWidth) / 2\n    const targetY = (viewportHeight - targetHeight) / 2\n\n    const scaleX = sourceRect.width / targetWidth\n    const scaleY = sourceRect.height / targetHeight\n    const scale = Math.max(scaleX, scaleY)\n\n    const translateX =\n      sourceRect.left + sourceRect.width / 2 - (targetX + targetWidth / 2)\n    const translateY =\n      sourceRect.top + sourceRect.height / 2 - (targetY + targetHeight / 2)\n\n    return {\n      transform: `translate(${translateX}px, ${translateY}px) scale(${scale})`,\n      opacity: 1,\n    }\n  }\n\n  const getFinalStyles = (): React.CSSProperties => {\n    return {\n      transform: \"translate(0, 0) scale(1)\",\n      opacity: 1,\n    }\n  }\n\n  const currentStyles =\n    animationPhase === \"initial\" && !isClosing\n      ? getInitialStyles()\n      : getFinalStyles()\n\n  return (\n    <div\n      className={cn(\n        \"fixed inset-0 z-50 flex items-center justify-center p-4 md:p-8\"\n      )}\n      onClick={handleClose}\n      style={{\n        opacity: isClosing ? 0 : 1,\n        transition: \"opacity 400ms cubic-bezier(0.16, 1, 0.3, 1)\",\n      }}\n    >\n      <div\n        className=\"bg-background/80 absolute inset-0 backdrop-blur-xl\"\n        style={{\n          opacity: animationPhase === \"initial\" && !isClosing ? 0 : 1,\n          transition: \"opacity 400ms cubic-bezier(0.16, 1, 0.3, 1)\",\n        }}\n      />\n\n      {/* Close button */}\n      <button\n        onClick={(e) => {\n          e.stopPropagation()\n          handleClose()\n        }}\n        className={cn(\n          \"absolute top-5 right-5 z-50\",\n          \"flex h-10 w-10 items-center justify-center\",\n          \"bg-muted/50 rounded-full backdrop-blur-md\",\n          \"border-border border\",\n          \"text-muted-foreground hover:text-foreground hover:bg-muted\",\n          \"transition-all duration-300 ease-out hover:scale-105 active:scale-95\"\n        )}\n        style={{\n          opacity: animationPhase === \"complete\" && !isClosing ? 1 : 0,\n          transform:\n            animationPhase === \"complete\" && !isClosing\n              ? \"translateY(0)\"\n              : \"translateY(-10px)\",\n          transition: \"opacity 300ms ease-out, transform 300ms ease-out\",\n        }}\n      >\n        <IconX className=\"h-4 w-4\" strokeWidth={2.5} />\n      </button>\n\n      <button\n        onClick={(e) => {\n          e.stopPropagation()\n          navigatePrev()\n        }}\n        disabled={!hasPrev || isSliding}\n        className={cn(\n          \"absolute left-4 z-50 md:left-8\",\n          \"flex h-12 w-12 items-center justify-center\",\n          \"bg-muted/50 rounded-full backdrop-blur-md\",\n          \"border-border border\",\n          \"text-muted-foreground hover:text-foreground hover:bg-muted\",\n          \"transition-all duration-300 ease-out hover:scale-110 active:scale-95\",\n          \"disabled:pointer-events-none disabled:opacity-0\"\n        )}\n        style={{\n          opacity:\n            animationPhase === \"complete\" && !isClosing && hasPrev ? 1 : 0,\n          transform:\n            animationPhase === \"complete\" && !isClosing\n              ? \"translateX(0)\"\n              : \"translateX(-20px)\",\n          transition:\n            \"opacity 300ms ease-out 150ms, transform 300ms ease-out 150ms\",\n        }}\n      >\n        <IconChevronLeft className=\"h-5 w-5\" strokeWidth={2.5} />\n      </button>\n\n      <button\n        onClick={(e) => {\n          e.stopPropagation()\n          navigateNext()\n        }}\n        disabled={!hasNext || isSliding}\n        className={cn(\n          \"absolute right-4 z-50 md:right-8\",\n          \"flex h-12 w-12 items-center justify-center\",\n          \"bg-muted/50 rounded-full backdrop-blur-md\",\n          \"border-border border\",\n          \"text-muted-foreground hover:text-foreground hover:bg-muted\",\n          \"transition-all duration-300 ease-out hover:scale-110 active:scale-95\",\n          \"disabled:pointer-events-none disabled:opacity-0\"\n        )}\n        style={{\n          opacity:\n            animationPhase === \"complete\" && !isClosing && hasNext ? 1 : 0,\n          transform:\n            animationPhase === \"complete\" && !isClosing\n              ? \"translateX(0)\"\n              : \"translateX(20px)\",\n          transition:\n            \"opacity 300ms ease-out 150ms, transform 300ms ease-out 150ms\",\n        }}\n      >\n        <IconChevronRight className=\"h-5 w-5\" strokeWidth={2.5} />\n      </button>\n\n      <div\n        ref={containerRef}\n        className=\"relative z-10 w-full max-w-3xl\"\n        onClick={(e) => e.stopPropagation()}\n        style={{\n          ...currentStyles,\n          transform: isClosing\n            ? \"translate(0, 0) scale(0.95)\"\n            : currentStyles.transform,\n          transition:\n            animationPhase === \"initial\" && !isClosing\n              ? \"none\"\n              : \"transform 400ms cubic-bezier(0.16, 1, 0.3, 1), opacity 400ms ease-out\",\n          transformOrigin: \"center center\",\n        }}\n      >\n        <div\n          className={cn(\n            \"relative overflow-hidden\",\n            \"rounded-2xl\",\n            \"bg-card\",\n            \"ring-border ring-1\",\n            \"shadow-2xl\"\n          )}\n          style={{\n            borderRadius:\n              animationPhase === \"initial\" && !isClosing ? \"8px\" : \"16px\",\n            transition: \"border-radius 500ms cubic-bezier(0.16, 1, 0.3, 1)\",\n          }}\n        >\n          <div className=\"relative overflow-hidden\">\n            <div\n              className=\"flex transition-transform duration-400 ease-out\"\n              style={{\n                transform: `translateX(-${internalIndex * 100}%)`,\n                transition: isSliding\n                  ? \"transform 400ms cubic-bezier(0.32, 0.72, 0, 1)\"\n                  : \"none\",\n              }}\n            >\n              {projects.map((project, idx) => (\n                <img\n                  key={project.id}\n                  src={project.image || \"/placeholder.svg\"}\n                  alt={project.title}\n                  className=\"bg-background h-auto max-h-[70vh] w-full shrink-0 object-contain\"\n                  style={{ minWidth: \"100%\" }}\n                />\n              ))}\n            </div>\n\n            {/* Subtle vignette effect */}\n            <div className=\"from-card/20 to-card/10 pointer-events-none absolute inset-0 bg-linear-to-t via-transparent\" />\n          </div>\n\n          <div\n            className={cn(\"px-6 py-5\", \"bg-card\", \"border-border border-t\")}\n            style={{\n              opacity: animationPhase === \"complete\" && !isClosing ? 1 : 0,\n              transform:\n                animationPhase === \"complete\" && !isClosing\n                  ? \"translateY(0)\"\n                  : \"translateY(20px)\",\n              transition:\n                \"opacity 300ms ease-out 100ms, transform 300ms ease-out 100ms\",\n            }}\n          >\n            <div className=\"flex items-start justify-between gap-4\">\n              <div className=\"min-w-0 flex-1\">\n                <h3 className=\"text-foreground h-7 truncate text-lg font-medium tracking-tight\">\n                  {currentProject?.title}\n                </h3>\n                <div className=\"mt-1 flex items-center gap-3\">\n                  <p className=\"text-muted-foreground text-sm\">\n                    <kbd className=\"bg-muted text-muted-foreground border-border mx-0.5 rounded border px-1.5 py-0.5 text-xs font-medium\">\n                      ←\n                    </kbd>\n                    <kbd className=\"bg-muted text-muted-foreground border-border mx-0.5 rounded border px-1.5 py-0.5 text-xs font-medium\">\n                      →\n                    </kbd>{\" \"}\n                    to navigate\n                  </p>\n                  <div className=\"flex items-center gap-1.5\">\n                    {projects.map((_, idx) => (\n                      <button\n                        key={idx}\n                        onClick={() => handleDotClick(idx)}\n                        className={cn(\n                          \"h-2 w-2 rounded-full transition-all duration-300\",\n                          idx === internalIndex\n                            ? \"bg-foreground scale-110\"\n                            : \"bg-muted-foreground/40 hover:bg-muted-foreground/60\"\n                        )}\n                      />\n                    ))}\n                  </div>\n                </div>\n              </div>\n\n              <button\n                className={cn(\n                  \"flex items-center gap-2 px-4 py-2\",\n                  \"text-muted-foreground text-sm font-medium\",\n                  \"bg-muted/50 hover:bg-muted\",\n                  \"border-border rounded-lg border\",\n                  \"transition-all duration-200 ease-out\",\n                  \"hover:text-foreground\"\n                )}\n              >\n                <span>View</span>\n                <IconExternalLink className=\"h-3.5 w-3.5\" />\n              </button>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\ninterface ProjectCardProps {\n  image: string\n  title: string\n  delay: number\n  isVisible: boolean\n  index: number\n  onClick: () => void\n  isSelected: boolean\n}\n\nexport const ProjectCard = forwardRef<HTMLDivElement, ProjectCardProps>(\n  ({ image, title, delay, isVisible, index, onClick, isSelected }, ref) => {\n    const rotations = [-12, 0, 12]\n    const translations = [-55, 0, 55]\n\n    return (\n      <div\n        ref={ref}\n        className={cn(\n          \"absolute h-28 w-20 overflow-hidden rounded-[8px] shadow-xl\",\n          \"bg-card border-border border\",\n          \"cursor-pointer hover:ring-2 hover:ring-orange-400/50\",\n          isSelected && \"opacity-0\"\n        )}\n        style={{\n          transform: isVisible\n            ? `translateY(-90px) translateX(${translations[index]}px) rotate(${rotations[index]}deg) scale(1)`\n            : \"translateY(0px) translateX(0px) rotate(0deg) scale(0.5)\",\n          opacity: isSelected ? 0 : isVisible ? 1 : 0,\n          transition: `all 600ms cubic-bezier(0.34, 1.56, 0.64, 1) ${delay}ms`,\n          zIndex: 10 - index,\n          left: \"-40px\",\n          top: \"-56px\",\n        }}\n        onClick={(e) => {\n          e.stopPropagation()\n          onClick()\n        }}\n      >\n        <img\n          src={image || \"/placeholder.svg\"}\n          alt={title}\n          className=\"h-full w-full object-cover\"\n        />\n        <div className=\"from-foreground/60 absolute inset-0 bg-linear-to-t to-transparent\" />\n        <p className=\"text-primary-foreground absolute right-1.5 bottom-1.5 left-1.5 truncate text-[10px] font-medium\">\n          {title}\n        </p>\n      </div>\n    )\n  }\n)\n\nProjectCard.displayName = \"ProjectCard\"\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}