{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "three-d-carousel", "type": "registry:ui", "description": "3D carousel component with perspective effects and smooth item transitions", "files": [ { "path": "registry/default/ui/three-d-carousel.tsx", "content": "\"use client\"\n\nimport { memo, useEffect, useLayoutEffect, useMemo, useState } from \"react\"\nimport {\n AnimatePresence,\n motion,\n useAnimation,\n useMotionValue,\n useTransform,\n} from \"motion/react\"\n\nexport const useIsomorphicLayoutEffect =\n typeof window !== \"undefined\" ? useLayoutEffect : useEffect\n\ntype UseMediaQueryOptions = {\n defaultValue?: boolean\n initializeWithValue?: boolean\n}\n\nconst IS_SERVER = typeof window === \"undefined\"\n\nexport function useMediaQuery(\n query: string,\n {\n defaultValue = false,\n initializeWithValue = true,\n }: UseMediaQueryOptions = {}\n): boolean {\n const getMatches = (query: string): boolean => {\n if (IS_SERVER) {\n return defaultValue\n }\n return window.matchMedia(query).matches\n }\n\n const [matches, setMatches] = useState(() => {\n if (initializeWithValue) {\n return getMatches(query)\n }\n return defaultValue\n })\n\n const handleChange = () => {\n setMatches(getMatches(query))\n }\n\n useIsomorphicLayoutEffect(() => {\n const matchMedia = window.matchMedia(query)\n handleChange()\n\n matchMedia.addEventListener(\"change\", handleChange)\n\n return () => {\n matchMedia.removeEventListener(\"change\", handleChange)\n }\n }, [query])\n\n return matches\n}\n\nconst keywords = [\n \"night\",\n \"city\",\n \"sky\",\n \"sunset\",\n \"sunrise\",\n \"winter\",\n \"skyscraper\",\n \"building\",\n \"cityscape\",\n \"architecture\",\n \"street\",\n \"lights\",\n \"downtown\",\n \"bridge\",\n]\n\nconst duration = 0.15\nconst transition = {\n duration,\n ease: [0.32, 0.72, 0, 1] as const,\n filter: \"blur(4px)\",\n}\nconst transitionOverlay = { duration: 0.5, ease: [0.32, 0.72, 0, 1] as const }\n\nconst Carousel = memo(\n ({\n handleClick,\n controls,\n cards,\n isCarouselActive,\n }: {\n handleClick: (imgUrl: string, index: number) => void\n controls: any\n cards: string[]\n isCarouselActive: boolean\n }) => {\n const isScreenSizeSm = useMediaQuery(\"(max-width: 640px)\")\n const cylinderWidth = isScreenSizeSm ? 1100 : 1800\n const faceCount = cards.length\n const faceWidth = cylinderWidth / faceCount\n const radius = cylinderWidth / (2 * Math.PI)\n const rotation = useMotionValue(0)\n const transform = useTransform(\n rotation,\n (value) => `rotate3d(0, 1, 0, ${value}deg)`\n )\n\n return (\n \n \n isCarouselActive &&\n rotation.set(rotation.get() + info.offset.x * 0.05)\n }\n onDragEnd={(_, info) =>\n isCarouselActive &&\n controls.start({\n rotateY: rotation.get() + info.velocity.x * 0.05,\n transition: {\n type: \"spring\" as const,\n stiffness: 100,\n damping: 30,\n mass: 0.1,\n },\n })\n }\n animate={controls}\n >\n {cards.map((imgUrl, i) => (\n handleClick(imgUrl, i)}\n >\n \n \n ))}\n \n \n )\n }\n)\n\nconst hiddenMask = `repeating-linear-gradient(to right, rgba(0,0,0,0) 0px, rgba(0,0,0,0) 30px, rgba(0,0,0,1) 30px, rgba(0,0,0,1) 30px)`\nconst visibleMask = `repeating-linear-gradient(to right, rgba(0,0,0,0) 0px, rgba(0,0,0,0) 0px, rgba(0,0,0,1) 0px, rgba(0,0,0,1) 30px)`\nfunction ThreeDPhotoCarousel() {\n const [activeImg, setActiveImg] = useState(null)\n const [isCarouselActive, setIsCarouselActive] = useState(true)\n const controls = useAnimation()\n const cards = useMemo(\n () => keywords.map((keyword) => `https://picsum.photos/200/300?${keyword}`),\n []\n )\n\n useEffect(() => {\n console.log(\"Cards loaded:\", cards)\n }, [cards])\n\n const handleClick = (imgUrl: string) => {\n setActiveImg(imgUrl)\n setIsCarouselActive(false)\n controls.stop()\n }\n\n const handleClose = () => {\n setActiveImg(null)\n setIsCarouselActive(true)\n }\n\n return (\n \n \n {activeImg && (\n \n \n \n )}\n \n
\n \n
\n \n )\n}\n\nexport default ThreeDPhotoCarousel\n", "type": "registry:ui" } ] }