{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tilted-carousel",
  "title": "Tilted Carousel",
  "description": "A 3D book-flow carousel where cards tilt around the active slide, with paper/glass themes and 3 title alignment styles.",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "registry/new-york/tilted-carousel/tilted-carousel.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { motion } from \"motion/react\";\nimport { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nfunction cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport interface TiltedCarouselSlide {\n  src: string;\n  title: string;\n}\n\nexport type TiltedCarouselTitleAlign = \"bottom-left\" | \"bottom-center\" | \"center\";\nexport type TiltedCarouselTheme = \"paper\" | \"glass\";\n\nexport interface TiltedCarouselProps extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"children\"> {\n  slides?: TiltedCarouselSlide[];\n  cardWidth?: number;\n  cardBorderRadius?: number;\n  tiltAngle?: number;\n  theme?: TiltedCarouselTheme;\n  titleAlign?: TiltedCarouselTitleAlign;\n  titleSize?: number;\n  initialIndex?: number;\n}\n\nconst DEFAULT_SLIDES: TiltedCarouselSlide[] = [\n  { src: \"https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=500&q=80\", title: \"mountain ridge\" },\n  { src: \"https://images.unsplash.com/photo-1449034446853-66c86144b0ad?w=500&q=80\", title: \"night scene\" },\n  { src: \"https://images.unsplash.com/photo-1490750967868-88aa4486c946?w=500&q=80\", title: \"yellow wildflowers\" },\n  { src: \"https://images.unsplash.com/photo-1478436127897-769e1b3f0f36?w=500&q=80\", title: \"street scene\" },\n  { src: \"https://images.unsplash.com/photo-1523906834658-6e24ef2386f9?w=500&q=80\", title: \"bicycle shop\" },\n  { src: \"https://images.unsplash.com/photo-1544620347-c4fd4a3d5957?w=500&q=80\", title: \"train window view\" },\n  { src: \"https://images.unsplash.com/photo-1517594422361-5eeb8ae275a9?w=500&q=80\", title: \"train tracks\" },\n  { src: \"https://images.unsplash.com/photo-1555529771-7888783a18d3?w=500&q=80\", title: \"convenience store\" },\n  { src: \"https://images.unsplash.com/photo-1480796927426-f609979314bd?w=500&q=80\", title: \"city street\" },\n  { src: \"https://images.unsplash.com/photo-1493976040374-85c8e12f0c0e?w=500&q=80\", title: \"japanese culture\" },\n];\n\nconst TOKENS = {\n  paper: \"#f5f5f5\",\n  ink: \"#221e1b\",\n  inkFaint: \"rgba(34,30,27,0.45)\",\n  glassBg: \"rgba(20,18,17,0.88)\",\n  glassFg: \"#ffffff\",\n  glassFgFaint: \"rgba(255,255,255,0.45)\",\n  glassBorder: \"rgba(255,255,255,0.12)\",\n  glassPillBg: \"rgba(255,255,255,0.10)\",\n  paperPillBg: \"#ffffff\",\n  paperBorder: \"rgba(34,30,27,0.10)\",\n  easeSignature: \"cubic-bezier(0.16, 1, 0.3, 1)\",\n} as const;\n\nexport function TiltedCarousel({\n  slides = DEFAULT_SLIDES,\n  cardWidth = 200,\n  cardBorderRadius = 12,\n  tiltAngle = 60,\n  theme = \"paper\",\n  titleAlign = \"bottom-left\",\n  titleSize = 14,\n  initialIndex = 3,\n  className,\n  style,\n  ...props\n}: TiltedCarouselProps) {\n  const effectiveSlides = slides.length > 0 ? slides : DEFAULT_SLIDES;\n\n  const [activeIndex, setActiveIndex] = React.useState(() => Math.min(initialIndex, effectiveSlides.length - 1));\n\n  const toPrev = () => setActiveIndex((prev) => Math.max(0, prev - 1));\n  const toNext = () => setActiveIndex((prev) => Math.min(effectiveSlides.length - 1, prev + 1));\n  const toSlide = (index: number) => setActiveIndex(index);\n\n  const isGlass = theme === \"glass\";\n  const bg = isGlass ? TOKENS.glassBg : TOKENS.paper;\n  const fg = isGlass ? TOKENS.glassFg : TOKENS.ink;\n  const fgFaint = isGlass ? TOKENS.glassFgFaint : TOKENS.inkFaint;\n  const pillBg = isGlass ? TOKENS.glassPillBg : TOKENS.paperPillBg;\n  const pillBorder = isGlass ? TOKENS.glassBorder : TOKENS.paperBorder;\n\n  const cardHeight = cardWidth * (4 / 3);\n  const totalWidth = cardWidth * effectiveSlides.length;\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === \"ArrowLeft\") {\n      e.preventDefault();\n      toPrev();\n    }\n    if (e.key === \"ArrowRight\") {\n      e.preventDefault();\n      toNext();\n    }\n  };\n\n  return (\n    <div\n      tabIndex={0}\n      onKeyDown={handleKeyDown}\n      className={cn(\"relative flex h-full w-full flex-col items-center justify-center overflow-hidden select-none outline-none\", className)}\n      style={{ backgroundColor: bg, color: fg, fontFamily: \"'Helvetica Neue', Arial, sans-serif\", ...style }}\n      {...props}\n    >\n      <div className=\"mb-10\" style={{ width: cardWidth, overflow: \"visible\" }}>\n        <motion.div\n          className=\"flex\"\n          style={{ width: totalWidth }}\n          animate={{ x: -activeIndex * cardWidth }}\n          transition={{ type: \"spring\", bounce: 0.2, duration: 0.8 }}\n        >\n          {effectiveSlides.map((item, i) => {\n            const isActive = activeIndex === i;\n            return (\n              <div key={i} className=\"shrink-0\" style={{ width: cardWidth, perspective: 800 }}>\n                <motion.div\n                  className=\"flex flex-col items-center gap-2 [will-change:transform]\"\n                  style={{ width: cardWidth, height: cardHeight }}\n                  animate={{ rotateY: (activeIndex - i) * tiltAngle, scale: isActive ? 1 : 0.85 }}\n                  transition={{ type: \"spring\", bounce: 0.1, duration: 1 }}\n                >\n                  <div\n                    onClick={() => toSlide(i)}\n                    className=\"relative shrink-0 cursor-pointer overflow-hidden\"\n                    style={{ width: \"100%\", height: cardHeight, borderRadius: cardBorderRadius }}\n                  >\n                    <img src={item.src} alt={item.title} className=\"block h-full w-full object-cover\" />\n\n                    <div\n                      className=\"pointer-events-none absolute inset-0\"\n                      style={{\n                        background:\n                          titleAlign === \"center\"\n                            ? \"rgba(0,0,0,0.28)\"\n                            : \"linear-gradient(to top, rgba(0,0,0,0.62) 0%, rgba(0,0,0,0.10) 45%, transparent 100%)\",\n                        opacity: isActive ? 1 : 0.5,\n                        transition: \"opacity 400ms ease\",\n                      }}\n                    />\n\n                    <motion.div\n                      className=\"pointer-events-none absolute font-semibold text-white [will-change:opacity,filter]\"\n                      style={{\n                        ...(titleAlign === \"center\"\n                          ? { top: \"50%\", left: 0, right: 0, textAlign: \"center\" as const, padding: \"0 14px\" }\n                          : titleAlign === \"bottom-center\"\n                            ? { bottom: 14, left: 0, right: 0, textAlign: \"center\" as const, padding: \"0 14px\" }\n                            : { bottom: 14, left: 14, right: 14, textAlign: \"left\" as const }),\n                        fontSize: titleSize,\n                        letterSpacing: \"-0.01em\",\n                        lineHeight: 1.2,\n                      }}\n                      animate={{\n                        opacity: isActive ? 1 : 0.65,\n                        filter: isActive ? \"blur(0px)\" : \"blur(0.5px)\",\n                        y: titleAlign === \"center\" ? (isActive ? \"-50%\" : \"-44%\") : isActive ? 0 : 4,\n                      }}\n                      transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}\n                    >\n                      {item.title}\n                    </motion.div>\n                  </div>\n                </motion.div>\n              </div>\n            );\n          })}\n        </motion.div>\n      </div>\n\n      <div\n        className=\"flex items-center gap-2 rounded-full px-2.5 py-1.5\"\n        style={{\n          backgroundColor: pillBg,\n          backdropFilter: isGlass ? \"blur(24px) saturate(180%)\" : undefined,\n          WebkitBackdropFilter: isGlass ? \"blur(24px) saturate(180%)\" : undefined,\n          border: `1px solid ${pillBorder}`,\n          boxShadow: isGlass ? \"0 4px 24px rgba(0,0,0,0.32)\" : \"0 1px 4px rgba(34,30,27,0.08)\",\n        }}\n      >\n        <button\n          type=\"button\"\n          aria-label=\"Previous slide\"\n          disabled={activeIndex === 0}\n          onClick={toPrev}\n          className=\"flex items-center justify-center rounded-md border-none bg-transparent px-2.5 py-1.5\"\n          style={{ color: fg, opacity: activeIndex === 0 ? 0.3 : 1, transition: \"opacity 200ms ease\" }}\n        >\n          <svg width={16} height={16} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.2} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n            <polyline points=\"15 18 9 12 15 6\" />\n          </svg>\n        </button>\n\n        <div className=\"flex items-center justify-center px-1\" style={{ gap: 5 }}>\n          {effectiveSlides.map((_, i) => (\n            <button\n              key={i}\n              type=\"button\"\n              aria-label={`Go to slide ${i + 1}`}\n              aria-current={activeIndex === i}\n              onClick={() => toSlide(i)}\n              className=\"h-1.75 shrink-0 cursor-pointer rounded-full border-none p-0\"\n              style={{\n                width: activeIndex === i ? 24 : 7,\n                backgroundColor: activeIndex === i ? fg : fgFaint,\n                transition: `width 300ms ${TOKENS.easeSignature}, background-color 300ms ease`,\n              }}\n            />\n          ))}\n        </div>\n\n        <button\n          type=\"button\"\n          aria-label=\"Next slide\"\n          disabled={activeIndex === effectiveSlides.length - 1}\n          onClick={toNext}\n          className=\"flex items-center justify-center rounded-md border-none bg-transparent px-2.5 py-1.5\"\n          style={{ color: fg, opacity: activeIndex === effectiveSlides.length - 1 ? 0.3 : 1, transition: \"opacity 200ms ease\" }}\n        >\n          <svg width={16} height={16} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.2} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n            <polyline points=\"9 18 15 12 9 6\" />\n          </svg>\n        </button>\n      </div>\n\n          </div>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}