{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "author": "DesignX ", "name": "project-showcase", "type": "registry:component", "title": "Project Showcase", "description": "A hover-preview project list that morphs into a detail page.", "dependencies": [ "lucide-react@^0.577.0", "motion@^14.0.0" ], "registryDependencies": [ "@dx/utils" ], "files": [ { "path": "dx/project-showcase.tsx", "type": "registry:component", "target": "components/dx/project-showcase.tsx", "content": "import * as React from \"react\";\nimport { motion } from \"motion/react\";\nimport { ArrowLeftIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type ShowcaseProject = {\n id: string;\n title: string;\n /** Image URL. */\n image: string;\n /** Heading shown above the description in the detail view. */\n tagline?: string;\n description?: React.ReactNode;\n /** Buttons or links shown at the end of the detail view. */\n actions?: React.ReactNode;\n};\n\nexport type ProjectShowcaseProps = Omit, \"onChange\" | \"defaultValue\"> & {\n projects: ShowcaseProject[];\n /** Label above the list. */\n label?: React.ReactNode;\n /** Id of the open project, or null for the list. */\n value?: string | null;\n defaultValue?: string | null;\n onValueChange?: (id: string | null) => void;\n /** Let the floating preview be dragged around. */\n draggable?: boolean;\n};\n\nconst radius = { borderRadius: 24 };\n\nfunction ProjectShowcase({\n projects,\n label = \"Selected work\",\n value,\n defaultValue = null,\n onValueChange,\n draggable = true,\n className,\n ...props\n}: ProjectShowcaseProps) {\n const uid = React.useId();\n const area = React.useRef(null);\n const [inner, setInner] = React.useState(defaultValue);\n const openId = value !== undefined ? value : inner;\n const setOpen = React.useCallback(\n (id: string | null) => {\n if (value === undefined) setInner(id);\n onValueChange?.(id);\n },\n [value, onValueChange],\n );\n const [hover, setHover] = React.useState(0);\n\n const openIndex = projects.findIndex((p) => p.id === openId);\n const open = openIndex >= 0 ? projects[openIndex] : null;\n const shown = projects[open ? openIndex : hover] ?? projects[0];\n\n React.useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setOpen(null);\n };\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [open, setOpen]);\n\n return (\n \n {!open ? (\n
\n \n
    \n
  • \n {label}\n \n
  • \n {projects.map((p, i) => (\n
  • \n setHover(i)}\n onFocus={() => setHover(i)}\n onClick={() => setOpen(p.id)}\n className={cn(\n \"relative flex w-fit items-center text-3xl font-medium tracking-tighter outline-none transition-opacity sm:text-4xl\",\n \"focus-visible:underline focus-visible:decoration-2 focus-visible:underline-offset-4\",\n hover === i ? \"opacity-100\" : \"opacity-45\",\n )}\n >\n {p.title}\n {hover === i && (\n \n )}\n \n
  • \n ))}\n
\n
\n ) : (\n
\n setOpen(null)}\n className=\"flex w-fit items-center gap-1.5 text-sm text-muted-foreground outline-none hover:text-foreground focus-visible:text-foreground\"\n >\n All projects\n \n
\n \n {open.title}\n \n
\n \n \n {open.tagline && (\n \n

{open.tagline}

\n \n
\n )}\n {open.description && (\n \n {open.description}\n \n )}\n {open.actions && (\n \n {open.actions}\n \n )}\n \n
\n )}\n \n );\n}\n\nexport { ProjectShowcase };\n" } ], "categories": [ "motion" ], "meta": { "group": "dx", "docs": "https://dxuireact.com/docs/components/project-showcase" } }