{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tilt-slide-transition",
  "title": "Tilt Slide Transition",
  "description": "Multi-page editorial portfolio transition with tilted scaling exit and diagonal clip-path entrance, powered by View Transition API, GSAP SplitText, and Lenis.",
  "dependencies": ["gsap", "@gsap/react", "lenis"],
  "files": [
    {
      "path": "registry/catalog/tilt-slide-transition/index.tsx",
      "content": "// biome-ignore-all lint: GSAP + View Transition API editorial layout showcase\n\"use client\";\n\nimport { useGSAP } from \"@gsap/react\";\nimport { cn } from \"@/lib/utils\";\nimport gsap from \"gsap\";\nimport { SplitText } from \"gsap/SplitText\";\nimport { ReactLenis } from \"lenis/react\";\nimport {\n  type ComponentPropsWithoutRef,\n  type CSSProperties,\n  type ReactNode,\n  type Ref,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { flushSync } from \"react-dom\";\n\ngsap.registerPlugin(useGSAP, SplitText);\n\nexport type TiltSlidePage = \"home\" | \"projects\" | \"info\";\n/** @deprecated Use TiltSlidePage */\nexport type ViewPage = TiltSlidePage;\n\nexport interface TiltSlideTransitionNavItem {\n  id: TiltSlidePage;\n  label: string;\n}\n/** @deprecated Use TiltSlideTransitionNavItem */\nexport type ViewPageTransitionNavItem = TiltSlideTransitionNavItem;\n\nconst TILT_SLIDE_TRANSITION_STYLES = `\n@import url(\"https://fonts.cdnfonts.com/css/pp-neue-montreal\");\n\n.tst-root {\n  --bg: #fff;\n  --fg: #1a1a1a;\n  position: relative;\n  width: 100%;\n  height: 100svh;\n  overflow: hidden;\n  font-family: \"PP Neue Montreal\", sans-serif;\n  background-color: #000;\n  color: var(--fg);\n}\n\n.tst-root * {\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n}\n\n.tst-root ::-webkit-scrollbar {\n  display: none;\n}\n\n.tst-root img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.tst-root .navbar {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  padding: 1rem;\n  display: flex;\n  justify-content: space-between;\n  align-items: flex-start;\n  z-index: 2;\n}\n\n.tst-root .navbar-items {\n  display: flex;\n  gap: clamp(1rem, 4vw, 2rem);\n}\n\n.tst-root .navbar-item {\n  padding: 1.5rem;\n}\n\n.tst-root .navbar-item a,\n.tst-root .navbar-item button {\n  background: none;\n  border: none;\n  padding: 0;\n  font-family: inherit;\n  font-size: inherit;\n  text-decoration: none;\n  color: var(--fg);\n  font-weight: 500;\n  cursor: pointer;\n}\n\n.tst-root .home {\n  width: 100%;\n  height: 100svh;\n  background-color: var(--bg);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  text-align: center;\n  position: relative;\n}\n\n.tst-root .home h1 {\n  width: 100%;\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  text-transform: uppercase;\n  color: var(--fg);\n  font-size: 20vw;\n  font-weight: bolder;\n  line-height: 1;\n  clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);\n  margin: 0;\n}\n\n.tst-root .projects {\n  width: 100%;\n  height: 100svh;\n  overflow-y: auto;\n}\n\n.tst-root .projects-container {\n  width: 100%;\n  min-height: 100svh;\n  background-color: var(--bg);\n  padding: 15rem 2rem;\n}\n\n.tst-root .images {\n  width: 30%;\n  margin: 0 auto;\n  display: flex;\n  flex-direction: column;\n  gap: 2rem;\n}\n\n.tst-root .info {\n  width: 100%;\n  height: 100svh;\n  overflow-y: auto;\n  background-color: var(--bg);\n}\n\n.tst-root .info-container {\n  width: 100%;\n  min-height: 100svh;\n  display: flex;\n}\n\n.tst-root .col {\n  flex: 1;\n}\n\n.tst-root .col:nth-child(2) {\n  padding: 2rem;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.tst-root .col p {\n  font-weight: 500;\n  font-size: 2rem;\n  color: var(--fg);\n  margin: 0;\n}\n\n.tst-root .char,\n.tst-root .line {\n  position: relative;\n  will-change: transform;\n}\n\n@media (max-width: 1000px) {\n  .tst-root .navbar-logo {\n    display: none;\n  }\n\n  .tst-root .navbar {\n    justify-content: center;\n  }\n\n  .tst-root .images {\n    width: calc(100% - 1rem);\n  }\n\n  .tst-root .info-container {\n    flex-direction: column;\n    min-height: 100svh;\n  }\n\n  .tst-root .col {\n    flex: none;\n    width: 100%;\n  }\n\n  .tst-root .col:first-child {\n    height: 55vh;\n    min-height: 300px;\n  }\n\n  .tst-root .col:nth-child(2) {\n    align-items: flex-start;\n    padding: 2.5rem 1.5rem 5rem;\n  }\n\n  .tst-root .col p {\n    font-size: 1.25rem;\n  }\n}\n\n::view-transition-group(navbar) {\n  animation: none;\n  z-index: 100;\n}\n\n::view-transition-old(.page-exit),\n::view-transition-old(page-content) {\n  animation: 1600ms cubic-bezier(0.65, 0, 0.35, 1) both page-out;\n}\n\n::view-transition-new(.page-enter),\n::view-transition-new(page-content) {\n  animation: 1500ms cubic-bezier(0.65, 0, 0.35, 1) both page-in;\n  animation-delay: 0.25s;\n}\n\n@keyframes page-out {\n  0% {\n    transform: translateX(0%) translateY(0%) rotate(0deg) scale(1);\n    opacity: 1;\n  }\n  40% {\n    transform: translateX(0%) translateY(0%) rotate(0deg) scale(0.8);\n    opacity: 0.5;\n  }\n  100% {\n    transform: translateX(-50%) translateY(20%) rotate(-10deg) scale(0.65);\n    opacity: 0.25;\n  }\n}\n\n@keyframes page-in {\n  from {\n    transform: translateX(100%) translateY(20%);\n    clip-path: polygon(50% 50%, 100% 50%, 100% 100%, 50% 100%);\n  }\n  to {\n    transform: translateX(0%) translateY(0%);\n    clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%);\n  }\n}\n`;\n\nexport interface TiltSlideTransitionHomeProps {\n  className?: string;\n  heroTitle?: string;\n}\n\nexport function TiltSlideTransitionHome({\n  className,\n  heroTitle = \"Sora\",\n}: TiltSlideTransitionHomeProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useGSAP(\n    () => {\n      const heroText = new SplitText(\".home h1\", {\n        mask: \"chars\",\n        type: \"chars\",\n      });\n      gsap.set(heroText.chars, { y: 400 });\n\n      gsap.to(heroText.chars, {\n        y: 0,\n        duration: 1,\n        stagger: 0.075,\n        ease: \"power3.out\",\n        delay: 1.125,\n      });\n    },\n    { scope: containerRef }\n  );\n\n  return (\n    <div className={cn(\"home\", className)} ref={containerRef}>\n      <h1>{heroTitle}</h1>\n    </div>\n  );\n}\n\nconst TILT_SLIDE_MEDIA =\n  \"https://cdn.soralabs.studio/media/demo/tilt-slide-template\" as const;\n\nexport interface TiltSlideTransitionProjectsProps {\n  className?: string;\n  images?: string[];\n}\n\nexport function TiltSlideTransitionProjects({\n  className,\n  images = [\n    `${TILT_SLIDE_MEDIA}/img1.jpg`,\n    `${TILT_SLIDE_MEDIA}/img2.webp`,\n    `${TILT_SLIDE_MEDIA}/img3.jpg`,\n  ],\n}: TiltSlideTransitionProjectsProps) {\n  return (\n    <ReactLenis\n      className={cn(\"projects\", className)}\n      options={{ wrapper: undefined }}\n    >\n      <div className=\"projects-container\">\n        <div className=\"images\">\n          {images.map((src, index) => (\n            <img alt={`Project item ${index + 1}`} key={src} src={src} />\n          ))}\n        </div>\n      </div>\n    </ReactLenis>\n  );\n}\n\nexport interface TiltSlideTransitionInfoProps {\n  className?: string;\n  image?: string;\n  paragraph?: string;\n}\n\nexport function TiltSlideTransitionInfo({\n  className,\n  image = `${TILT_SLIDE_MEDIA}/portrait.webp`,\n  paragraph = \"Sora is an open-source animated component distribution crafted with motion, typography, and tactile interaction design. Built for modern web applications, it bridges the gap between physics and interface with minimal, kinetic aesthetics.\",\n}: TiltSlideTransitionInfoProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useGSAP(\n    () => {\n      const split = new SplitText(\"p\", {\n        type: \"lines\",\n        mask: \"lines\",\n        linesClass: \"line\",\n      });\n\n      gsap.set(\".line\", { y: 400 });\n\n      gsap.to(\".line\", {\n        y: 0,\n        duration: 2,\n        stagger: 0.1,\n        ease: \"power4.out\",\n        delay: 0.5,\n      });\n    },\n    { scope: containerRef }\n  );\n\n  return (\n    <ReactLenis\n      className={cn(\"info\", className)}\n      options={{ wrapper: undefined }}\n    >\n      <div className=\"info-container\" ref={containerRef}>\n        <div className=\"col\">\n          <img alt=\"Portrait\" src={image} />\n        </div>\n        <div className=\"col\">\n          <p>{paragraph}</p>\n        </div>\n      </div>\n    </ReactLenis>\n  );\n}\n\nexport interface TiltSlideTransitionNavbarProps {\n  activePage: TiltSlidePage;\n  brandName?: string;\n  className?: string;\n  navItems?: TiltSlideTransitionNavItem[];\n  onNavigate: (page: TiltSlidePage) => void;\n}\n\nexport function TiltSlideTransitionNavbar({\n  activePage: _activePage,\n  brandName = \"Sora\",\n  className,\n  navItems = [\n    { id: \"home\", label: \"Home\" },\n    { id: \"projects\", label: \"Projects\" },\n    { id: \"info\", label: \"Info\" },\n  ],\n  onNavigate,\n}: TiltSlideTransitionNavbarProps) {\n  return (\n    <nav\n      className={cn(\"navbar\", className)}\n      style={{ viewTransitionName: \"navbar\" }}\n    >\n      <div className=\"navbar-logo\">\n        <div className=\"navbar-item\">\n          <a\n            href=\"#home\"\n            onClick={(e) => {\n              e.preventDefault();\n              onNavigate(\"home\");\n            }}\n          >\n            {brandName}\n          </a>\n        </div>\n      </div>\n      <div className=\"navbar-items\">\n        {navItems.map((item) => (\n          <div className=\"navbar-item\" key={item.id}>\n            <a\n              href={`#${item.id}`}\n              onClick={(e) => {\n                e.preventDefault();\n                onNavigate(item.id);\n              }}\n            >\n              {item.label}\n            </a>\n          </div>\n        ))}\n      </div>\n    </nav>\n  );\n}\n\nexport interface TiltSlideTransitionProps\n  extends ComponentPropsWithoutRef<\"div\"> {\n  /** Logo/brand title displayed in the navbar. @default \"Sora\" */\n  brandName?: string;\n  /** Active page for uncontrolled usage. @default \"home\" */\n  defaultPage?: TiltSlidePage;\n  /** Large heading text revealed on the Home page. @default \"Sora\" */\n  heroTitle?: string;\n  /** Paragraph text revealed line-by-line on the Info page. */\n  infoParagraph?: string;\n  /** Navigation links rendered in the navbar. */\n  navItems?: TiltSlideTransitionNavItem[];\n  /** Callback fired when a page transition starts. */\n  onPageChange?: (page: TiltSlidePage) => void;\n  /** Controlled page state. */\n  page?: TiltSlidePage;\n  /** Portrait image URL shown on the Info page. */\n  portraitImage?: string;\n  /** Image URLs displayed in the Projects smooth scroll feed. */\n  projectImages?: string[];\n  /** Optional forwarded ref for the root container. */\n  ref?: Ref<HTMLDivElement>;\n}\n/** @deprecated Use TiltSlideTransitionProps */\nexport type ViewPageTransitionProps = TiltSlideTransitionProps;\n\nexport function TiltSlideTransition({\n  brandName = \"Sora\",\n  className,\n  defaultPage = \"home\",\n  heroTitle = \"Sora\",\n  infoParagraph,\n  navItems,\n  onPageChange,\n  page: controlledPage,\n  portraitImage,\n  projectImages,\n  ref,\n  style,\n  ...props\n}: TiltSlideTransitionProps) {\n  const [internalPage, setInternalPage] = useState<TiltSlidePage>(defaultPage);\n  const activePage = controlledPage ?? internalPage;\n  const isTransitioningRef = useRef(false);\n\n  useEffect(() => {\n    const styleId = \"sora-tst-styles\";\n    if (document.getElementById(styleId)) {\n      return;\n    }\n    const styleElement = document.createElement(\"style\");\n    styleElement.id = styleId;\n    styleElement.textContent = TILT_SLIDE_TRANSITION_STYLES;\n    document.head.appendChild(styleElement);\n  }, []);\n\n  const handleNavigate = useCallback(\n    (targetPage: TiltSlidePage) => {\n      if (targetPage === activePage || isTransitioningRef.current) {\n        return;\n      }\n\n      onPageChange?.(targetPage);\n\n      if (\n        typeof document !== \"undefined\" &&\n        \"startViewTransition\" in document\n      ) {\n        isTransitioningRef.current = true;\n        try {\n          const transition = document.startViewTransition(() => {\n            flushSync(() => {\n              setInternalPage(targetPage);\n            });\n          });\n\n          transition.finished.finally(() => {\n            isTransitioningRef.current = false;\n          });\n        } catch {\n          setInternalPage(targetPage);\n          isTransitioningRef.current = false;\n        }\n      } else {\n        setInternalPage(targetPage);\n      }\n    },\n    [activePage, onPageChange]\n  );\n\n  const renderActiveView = (): ReactNode => {\n    switch (activePage) {\n      case \"projects\":\n        return <TiltSlideTransitionProjects images={projectImages} />;\n      case \"info\":\n        return (\n          <TiltSlideTransitionInfo\n            image={portraitImage}\n            paragraph={infoParagraph}\n          />\n        );\n      case \"home\":\n      default:\n        return <TiltSlideTransitionHome heroTitle={heroTitle} />;\n    }\n  };\n\n  return (\n    <div\n      className={cn(\"tst-root\", className)}\n      ref={ref}\n      style={style as CSSProperties}\n      {...props}\n    >\n      <style>{TILT_SLIDE_TRANSITION_STYLES}</style>\n      <TiltSlideTransitionNavbar\n        activePage={activePage}\n        brandName={brandName}\n        navItems={navItems}\n        onNavigate={handleNavigate}\n      />\n      <div\n        className=\"page-content page-enter page-exit\"\n        style={{\n          height: \"100%\",\n          viewTransitionName: \"page-content\",\n          width: \"100%\",\n        }}\n      >\n        {renderActiveView()}\n      </div>\n    </div>\n  );\n}\n\nexport {\n  TiltSlideTransition as ViewPageTransition,\n  TiltSlideTransitionHome as ViewPageTransitionHome,\n  TiltSlideTransitionInfo as ViewPageTransitionInfo,\n  TiltSlideTransitionNavbar as ViewPageTransitionNavbar,\n  TiltSlideTransitionProjects as ViewPageTransitionProjects,\n};\n",
      "type": "registry:ui",
      "target": "components/sora-ui/catalog/tilt-slide-transition.tsx"
    }
  ],
  "meta": {
    "keywords": [
      "tilt-transition",
      "slide-transition",
      "view-transition",
      "page-transition",
      "gsap",
      "splittext",
      "lenis",
      "portfolio",
      "editorial",
      "codegrid"
    ],
    "inspiration": {
      "type": "reimplemented",
      "label": "Codegrid & Paul Kalkbrenner",
      "url": "https://www.paulkalkbrenner.net",
      "stack": "Next.js, View Transition API, GSAP, and Lenis"
    }
  },
  "type": "registry:ui"
}
