{ "name": "enlarge-gallery", "type": "registry:ui", "registryDependencies": [], "dependencies": [ "lucide-react" ], "devDependencies": [], "tailwind": {}, "cssVars": { "light": {}, "dark": {} }, "files": [ { "path": "enlarge-gallery.tsx", "content": "'use client';\n\nimport React, { useState, useCallback } from 'react';\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport './enlarge-gallery.css';\n\nexport interface EnlargeGalleryItem {\n id: string | number;\n src: string;\n alt?: string;\n title: string;\n description?: string;\n}\n\nexport interface EnlargeGalleryProps extends React.HTMLAttributes {\n items: EnlargeGalleryItem[];\n}\n\nexport const EnlargeGallery = React.forwardRef<\n HTMLDivElement,\n EnlargeGalleryProps\n>(({ className, items, ...props }, ref) => {\n const [activeIndex, setActiveIndex] = useState(0);\n\n // The thumbnail strip as a queue of item ids, left to right; the active\n // item is excluded. Every change removes the incoming item from the queue\n // and pushes the outgoing item onto the end.\n const [thumbOrder, setThumbOrder] = useState<(string | number)[]>(() =>\n items.slice(1).map((item) => item.id)\n );\n\n const goToIndex = useCallback(\n (newIndex: number) => {\n setActiveIndex((oldIndex) => {\n if (oldIndex === newIndex) return oldIndex;\n\n setThumbOrder((order) => [\n ...order.filter((id) => id !== items[newIndex].id),\n items[oldIndex].id,\n ]);\n\n return newIndex;\n });\n },\n [items]\n );\n\n if (!items || items.length === 0) return null;\n\n const activeItem = items[activeIndex];\n const total = items.length;\n\n const indexOfId = (id: string | number) =>\n items.findIndex((i) => i.id === id);\n\n return (\n \n
\n {/* Slides / Track */}\n
\n {items.map((item, index) => {\n const active = index === activeIndex;\n const offset = active\n ? 0\n : thumbOrder.indexOf(item.id) - (thumbOrder.length - 1) / 2;\n\n return (\n \n \n {/* eslint-disable-next-line @next/next/no-img-element */}\n \n {!active && (\n goToIndex(index)}\n >\n \n Show {item.title}\n \n \n )}\n
\n
\n );\n })}\n \n\n {/* Active Content & Navigation */}\n
\n
\n

\n {String(activeIndex + 1).padStart(2, '0')} /{' '}\n {String(total).padStart(2, '0')}\n

\n

{activeItem.title}

\n {activeItem.description && (\n

\n {activeItem.description}\n

\n )}\n
\n\n
\n \n goToIndex(indexOfId(thumbOrder[thumbOrder.length - 1]))\n }\n >\n \n Previous\n \n goToIndex(indexOfId(thumbOrder[0]))}\n >\n \n Next\n \n
\n
\n \n \n );\n});\nEnlargeGallery.displayName = 'EnlargeGallery';\n", "type": "registry:ui" }, { "path": "enlarge-gallery.tsx", "content": "'use client';\n\nimport React, { useState, useCallback } from 'react';\nimport { ChevronLeft, ChevronRight } from 'lucide-react';\nimport { cn } from '@/lib/utils';\nimport './enlarge-gallery.css';\n\nexport interface EnlargeGalleryItem {\n id: string | number;\n src: string;\n alt?: string;\n title: string;\n description?: string;\n}\n\nexport interface EnlargeGalleryProps extends React.HTMLAttributes {\n items: EnlargeGalleryItem[];\n}\n\nexport const EnlargeGallery = React.forwardRef<\n HTMLDivElement,\n EnlargeGalleryProps\n>(({ className, items, ...props }, ref) => {\n const [activeIndex, setActiveIndex] = useState(0);\n\n // The thumbnail strip as a queue of item ids, left to right; the active\n // item is excluded. Every change removes the incoming item from the queue\n // and pushes the outgoing item onto the end.\n const [thumbOrder, setThumbOrder] = useState<(string | number)[]>(() =>\n items.slice(1).map((item) => item.id)\n );\n\n const goToIndex = useCallback(\n (newIndex: number) => {\n setActiveIndex((oldIndex) => {\n if (oldIndex === newIndex) return oldIndex;\n\n setThumbOrder((order) => [\n ...order.filter((id) => id !== items[newIndex].id),\n items[oldIndex].id,\n ]);\n\n return newIndex;\n });\n },\n [items]\n );\n\n if (!items || items.length === 0) return null;\n\n const activeItem = items[activeIndex];\n const total = items.length;\n\n const indexOfId = (id: string | number) =>\n items.findIndex((i) => i.id === id);\n\n return (\n \n
\n {/* Slides / Track */}\n
\n {items.map((item, index) => {\n const active = index === activeIndex;\n const offset = active\n ? 0\n : thumbOrder.indexOf(item.id) - (thumbOrder.length - 1) / 2;\n\n return (\n \n \n {/* eslint-disable-next-line @next/next/no-img-element */}\n \n {!active && (\n goToIndex(index)}\n >\n \n Show {item.title}\n \n \n )}\n
\n
\n );\n })}\n \n\n {/* Active Content & Navigation */}\n
\n
\n

\n {String(activeIndex + 1).padStart(2, '0')} /{' '}\n {String(total).padStart(2, '0')}\n

\n

{activeItem.title}

\n {activeItem.description && (\n

\n {activeItem.description}\n

\n )}\n
\n\n
\n \n goToIndex(indexOfId(thumbOrder[thumbOrder.length - 1]))\n }\n >\n \n Previous\n \n goToIndex(indexOfId(thumbOrder[0]))}\n >\n \n Next\n \n
\n
\n \n \n );\n});\nEnlargeGallery.displayName = 'EnlargeGallery';\n", "type": "registry:ui" }, { "path": "enlarge-gallery.css", "content": "@reference \"../../app/globals.css\";\n\n.enlarge-gallery-root {\n --thumb-scale: 0.11;\n --thumb-gap: 14px;\n --thumb-bottom: 28px;\n --zoom-duration: 0.8s;\n --zoom-ease: cubic-bezier(0.22, 1, 0.36, 1);\n\n background: #0b0b0f;\n color: #fff;\n height: 100vh;\n height: 100dvh;\n overflow: hidden;\n position: relative;\n width: 100%;\n}\n\n@media (width <= 640px) {\n .enlarge-gallery-root {\n --thumb-scale: 0.16;\n --thumb-gap: 10px;\n --thumb-bottom: 20px;\n }\n}\n\n.enlarge-gallery-gallery {\n height: 100%;\n}\n\n.enlarge-gallery-track {\n height: 100%;\n overflow: visible !important;\n}\n\n.enlarge-gallery-item {\n inset: 0 !important;\n margin: 0 !important;\n pointer-events: none !important;\n position: absolute !important;\n transform: none !important;\n width: auto !important;\n}\n\n.enlarge-gallery-item[data-active='true'] {\n z-index: 1 !important;\n}\n\n.enlarge-gallery-item[data-active='false'] {\n z-index: 10 !important;\n}\n\n.enlarge-gallery-slide {\n border-radius: 0;\n inset: 0;\n overflow: hidden;\n pointer-events: auto;\n position: absolute;\n transform: translate(0, 0) scale(1);\n transition:\n transform var(--zoom-duration) var(--zoom-ease),\n border-radius var(--zoom-duration) var(--zoom-ease),\n filter 0.25s ease;\n will-change: transform;\n}\n\n.enlarge-gallery-item[data-active='false'] .enlarge-gallery-slide {\n border-radius: 48px;\n filter: brightness(0.7);\n transform: translate(\n calc(var(--offset) * (var(--thumb-scale) * 100vw + var(--thumb-gap))),\n calc((50vh - var(--thumb-scale) * 50vh) - var(--thumb-bottom))\n )\n scale(var(--thumb-scale));\n}\n\n.enlarge-gallery-item[data-active='false'] .enlarge-gallery-slide:hover {\n filter: brightness(1);\n}\n\n.enlarge-gallery-image {\n display: block;\n height: 100%;\n object-fit: cover;\n width: 100%;\n}\n\n.enlarge-gallery-thumbButton {\n appearance: none;\n background: none;\n border: 0;\n cursor: pointer;\n inset: 0;\n padding: 0;\n position: absolute;\n}\n\n.enlarge-gallery-rail {\n align-items: flex-start;\n background: linear-gradient(90deg, rgb(0 0 0 / 60%), transparent);\n display: flex;\n flex-direction: column;\n gap: 2rem;\n inset: 0 auto 0 0;\n justify-content: center;\n max-width: min(520px, 70%);\n padding: 0 3.5rem 0 3rem;\n pointer-events: none;\n position: absolute;\n z-index: 5;\n}\n\n@media (width <= 640px) {\n .enlarge-gallery-rail {\n max-width: 90%;\n padding: 0 1.5rem;\n }\n}\n\n.enlarge-gallery-content {\n animation: content-in 0.6s var(--zoom-ease) calc(var(--zoom-duration) * 0.4)\n backwards;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n@keyframes content-in {\n from {\n opacity: 0;\n transform: translateY(24px);\n }\n}\n\n.enlarge-gallery-kicker {\n font-size: 0.8rem;\n margin: 0;\n opacity: 0.7;\n}\n\n.enlarge-gallery-title {\n font-size: clamp(2rem, 5vw, 3.5rem);\n line-height: 1.05;\n margin: 0;\n}\n\n.enlarge-gallery-description {\n color: rgb(255 255 255 / 85%);\n font-size: clamp(0.9rem, 1.4vw, 1.05rem);\n line-height: 1.6;\n margin: 0;\n}\n\n.enlarge-gallery-controls {\n display: flex;\n gap: 0.75rem;\n pointer-events: auto;\n}\n\n.enlarge-gallery-navBtn {\n align-items: center;\n appearance: none;\n backdrop-filter: blur(8px);\n background: rgb(255 255 255 / 10%);\n border: 1px solid rgb(255 255 255 / 20%);\n border-radius: 50%;\n color: #fff;\n cursor: pointer;\n display: flex;\n height: 3rem;\n justify-content: center;\n transition:\n background 0.2s,\n border-color 0.2s,\n opacity 0.2s;\n width: 3rem;\n}\n\n.enlarge-gallery-navBtn:hover:not(:disabled) {\n background: rgb(255 255 255 / 22%);\n border-color: rgb(255 255 255 / 40%);\n}\n\n.enlarge-gallery-navBtn:disabled {\n cursor: default;\n opacity: 0.35;\n}\n\n.enlarge-gallery-visuallyHidden {\n border: 0;\n clip: rect(0 0 0 0);\n height: 1px;\n margin: -1px;\n overflow: hidden;\n padding: 0;\n position: absolute;\n width: 1px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .enlarge-gallery-slide {\n transition: none;\n }\n\n .enlarge-gallery-content {\n animation: none;\n }\n}\n", "type": "registry:ui" } ] }