{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "coverflow-gallery",
  "title": "Coverflow Gallery",
  "description": "3D coverflow carousel with keyboard, dot nav, and optional autoplay that pauses on hover.",
  "files": [
    {
      "path": "components/gallery/CoverflowGallery.tsx",
      "content": "'use client';\n\nimport {\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type KeyboardEvent,\n} from 'react';\n\n// ---------------------------------------------------------------------------\n// Props\n// ---------------------------------------------------------------------------\n//\n// items         Gallery entries — image src, alt, and optional caption\n// cardWidth     Card width in px\n// cardHeight    Card height in px\n// cornerRadius  Border radius in px\n// autoplay      Auto-advance slides (pauses on hover/focus)\n// showCaption   Show caption on the focused card\n// className     Styles for the outer shell\n//\ntype GalleryItem = {\n  image: { src: string; alt?: string };\n  caption?: string;\n};\n\ntype CoverflowGalleryProps = {\n  items?: GalleryItem[];\n  cardWidth?: number;\n  cardHeight?: number;\n  cornerRadius?: number;\n  autoplay?: boolean;\n  showCaption?: boolean;\n  className?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Defaults\n// ---------------------------------------------------------------------------\nconst DEFAULT_ITEMS: GalleryItem[] = [\n  {\n    image: {\n      src: 'https://assets.uniquel.io/components/stock/photo-1497366216548-37526070297c.jpg',\n      alt: 'Minimal office workspace',\n    },\n    caption: 'Studio\\nLight\\nCalm',\n  },\n  {\n    image: {\n      src: 'https://assets.uniquel.io/components/stock/photo-1487958449943-2429e8be8625.jpg',\n      alt: 'Modern concrete building',\n    },\n    caption: 'Form\\nConcrete\\nLine',\n  },\n  {\n    image: {\n      src: 'https://assets.uniquel.io/components/stock/photo-1503387762-592deb58ef4e.jpg',\n      alt: 'Architectural interior detail',\n    },\n    caption: 'Space\\nSteel\\nFocus',\n  },\n  {\n    image: {\n      src: 'https://assets.uniquel.io/components/stock/photo-1600607687939-ce8a6c25118c.jpg',\n      alt: 'Bright living room interior',\n    },\n    caption: 'Home\\nWarm\\nOpen',\n  },\n  {\n    image: {\n      src: 'https://assets.uniquel.io/components/stock/photo-1486325212027-8081e485255e.jpg',\n      alt: 'Curved glass tower',\n    },\n    caption: 'Curve\\nMotion\\nDepth',\n  },\n];\n\nconst PERSPECTIVE = 1600;\nconst SCALE_STEP = 0.16;\nconst MAX_VISIBLE = 2;\nconst DEPTH = 240;\nconst MOVE_MS = 600;\nconst AUTOPLAY_MS = 2500;\nconst INACTIVE_DIM = 0.4;\nconst YAW_TILT = 12;\nconst ROLL_TILT = 8;\nconst SPREAD = 8;\n\nexport default function CoverflowGallery({\n  items = DEFAULT_ITEMS,\n  cardWidth = 400,\n  cardHeight = 400,\n  cornerRadius = 12,\n  autoplay = false,\n  showCaption = true,\n  className = '',\n}: CoverflowGalleryProps) {\n  const deck = items.length ? items : DEFAULT_ITEMS;\n  const total = deck.length;\n\n  const [focusedIndex, setFocusedIndex] = useState(0);\n  const [isPaused, setIsPaused] = useState(false);\n  const inputLockedRef = useRef(false);\n\n  const releaseLockLater = useCallback(() => {\n    inputLockedRef.current = true;\n    window.setTimeout(() => {\n      inputLockedRef.current = false;\n    }, Math.max(50, MOVE_MS));\n  }, []);\n\n  const advance = useCallback(\n    (direction: number) => {\n      if (inputLockedRef.current || total < 2) return;\n      releaseLockLater();\n      setFocusedIndex((current) => (((current + direction) % total) + total) % total);\n    },\n    [releaseLockLater, total]\n  );\n\n  const jumpToIndex = useCallback(\n    (index: number) => {\n      if (inputLockedRef.current) return;\n      releaseLockLater();\n      setFocusedIndex(index);\n    },\n    [releaseLockLater]\n  );\n\n  const focusCard = useCallback(\n    (index: number) => {\n      if (inputLockedRef.current || autoplay) return;\n      releaseLockLater();\n      setFocusedIndex((current) =>\n        index === current ? (current + 1) % total : index\n      );\n    },\n    [autoplay, releaseLockLater, total]\n  );\n\n  useEffect(() => {\n    setFocusedIndex((current) => Math.max(0, Math.min(total - 1, current)));\n  }, [total]);\n\n  useEffect(() => {\n    if (!autoplay || isPaused || total < 2) return;\n    const id = window.setInterval(() => advance(1), AUTOPLAY_MS);\n    return () => window.clearInterval(id);\n  }, [advance, autoplay, isPaused, total]);\n\n  const onKeyDown = useCallback(\n    (event: KeyboardEvent<HTMLDivElement>) => {\n      if (event.key === 'ArrowRight') {\n        event.preventDefault();\n        advance(1);\n      } else if (event.key === 'ArrowLeft') {\n        event.preventDefault();\n        advance(-1);\n      }\n    },\n    [advance]\n  );\n\n  const transitionCss = `transform ${MOVE_MS}ms cubic-bezier(0.22, 1, 0.36, 1), opacity ${MOVE_MS}ms cubic-bezier(0.22, 1, 0.36, 1)`;\n\n  const rootStyle: CSSProperties = {\n    position: 'relative',\n    width: '100%',\n    minWidth: 320,\n    minHeight: 360,\n    display: 'flex',\n    flexDirection: 'column',\n    alignItems: 'center',\n    justifyContent: 'center',\n    perspective: `${PERSPECTIVE}px`,\n    overflow: 'hidden',\n    outline: 'none',\n  };\n\n  return (\n    <div\n      className={['w-full py-8', className].filter(Boolean).join(' ')}\n      tabIndex={0}\n      role='group'\n      aria-roledescription='carousel'\n      aria-label='Image gallery'\n      onKeyDown={onKeyDown}\n      onMouseEnter={() => setIsPaused(true)}\n      onMouseLeave={() => setIsPaused(false)}\n      onFocus={() => setIsPaused(true)}\n      onBlur={() => setIsPaused(false)}\n    >\n      <div style={rootStyle}>\n        <div\n          style={{\n            position: 'relative',\n            width: cardWidth,\n            height: cardHeight,\n            transformStyle: 'preserve-3d',\n          }}\n        >\n          {deck.map((entry, index) => {\n            let offset = index - focusedIndex;\n            if (offset > total / 2) offset -= total;\n            if (offset < -total / 2) offset += total;\n\n            const distance = Math.abs(offset);\n            const isVisible = distance <= MAX_VISIBLE;\n            const isFocused = offset === 0;\n            const scale = Math.max(0.4, 1 - distance * SCALE_STEP);\n            const translateX = offset * SPREAD * 30;\n            const translateZ = -distance * DEPTH;\n            const rotateY = -offset * YAW_TILT;\n            const rotateZ = offset * ROLL_TILT;\n\n            const cardStyle: CSSProperties = {\n              position: 'absolute',\n              left: '50%',\n              top: '50%',\n              width: cardWidth,\n              height: cardHeight,\n              borderRadius: cornerRadius,\n              overflow: 'hidden',\n              transformStyle: 'preserve-3d',\n              transformOrigin: 'center center',\n              transform: `translate(-50%, -50%) translateX(${translateX}px) translateZ(${translateZ}px) rotateY(${rotateY}deg) rotateZ(${rotateZ}deg) scale(${scale})`,\n              transition: transitionCss,\n              opacity: isVisible ? 1 : 0,\n              cursor: autoplay || isFocused ? 'default' : 'pointer',\n              pointerEvents: isVisible && !autoplay ? 'auto' : 'none',\n              backgroundColor: '#171717',\n            };\n\n            return (\n              <div\n                key={`${entry.image.src}-${index}`}\n                style={cardStyle}\n                onClick={() => focusCard(index)}\n                aria-label={entry.caption || entry.image.alt}\n                aria-hidden={!isVisible}\n              >\n                <img\n                  src={entry.image.src}\n                  alt={entry.image.alt || entry.caption || ''}\n                  draggable={false}\n                  style={{\n                    position: 'absolute',\n                    inset: 0,\n                    width: '100%',\n                    height: '100%',\n                    objectFit: 'cover',\n                    display: 'block',\n                    userSelect: 'none',\n                  }}\n                />\n\n                {showCaption && entry.caption && isFocused && (\n                  <>\n                    <div\n                      style={{\n                        position: 'absolute',\n                        inset: 0,\n                        background:\n                          'linear-gradient(180deg, rgba(0,0,0,0) 35%, rgba(0,0,0,0.72) 100%)',\n                        pointerEvents: 'none',\n                      }}\n                    />\n                    <div className='pointer-events-none absolute bottom-0 left-0 p-6 text-white'>\n                      <span className='whitespace-pre-line text-2xl font-bold leading-tight tracking-tight drop-shadow-md'>\n                        {entry.caption}\n                      </span>\n                    </div>\n                  </>\n                )}\n\n                <div\n                  style={{\n                    position: 'absolute',\n                    inset: 0,\n                    background: '#000000',\n                    opacity: isFocused ? 0 : INACTIVE_DIM,\n                    transition: `opacity ${MOVE_MS}ms cubic-bezier(0.22, 1, 0.36, 1)`,\n                    pointerEvents: 'none',\n                  }}\n                />\n              </div>\n            );\n          })}\n        </div>\n      </div>\n\n      <div className='mt-6 flex items-center justify-center gap-2'>\n        {deck.map((entry, index) => (\n          <button\n            key={`dot-${entry.image.src}-${index}`}\n            type='button'\n            aria-label={`Go to slide ${index + 1}`}\n            onClick={() => jumpToIndex(index)}\n            className={[\n              'h-2 rounded-full transition-all duration-300',\n              index === focusedIndex\n                ? 'w-6 bg-[var(--color-primary,#111827)]'\n                : 'w-2 bg-[var(--color-muted,#d1d5db)] hover:opacity-75',\n            ].join(' ')}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/gallery/CoverflowGallery.tsx"
    }
  ],
  "categories": [
    "gallery"
  ],
  "type": "registry:component"
}