{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "phone-scroll-hero",
  "type": "registry:ui",
  "title": "Phone Scroll Hero",
  "description": "A hero whose 3D-tilted phone mockup flattens and climbs up over the headline as the page scrolls. Theme-aware with a reduced-motion fallback.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/ruixenui/phone-scroll-hero.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ── types ───────────────────────────────────────────────────── */\n\nexport interface PhoneScrollHeroProps {\n  /** Headline block. Sits *behind* the phone, so the phone can climb over it. */\n  titleComponent: React.ReactNode;\n  /** Rendered inside the phone screen. */\n  children?: React.ReactNode;\n  /**\n   * How far the phone climbs into the title, as a share of its own height.\n   * A percentage keeps the overlap proportional at every breakpoint without a\n   * resize listener. Default `20`.\n   */\n  overlap?: number;\n  /** Backward tilt of the phone at the start of the scroll, in deg. Default `22`. */\n  tilt?: number;\n  className?: string;\n}\n\n/* ── helpers ─────────────────────────────────────────────────── */\n\nconst useIsomorphicLayoutEffect =\n  typeof window !== \"undefined\" ? React.useLayoutEffect : React.useEffect;\n\n/* ── pieces ──────────────────────────────────────────────────── */\n\n/** Device chassis. Every surface is a shadcn token, so the frame follows the\n *  host theme; only the camera island is a fixed colour (see below). */\nfunction PhoneFrame({ children }: { children?: React.ReactNode }) {\n  return (\n    <div className=\"relative mx-auto w-[13rem] sm:w-[15rem] md:w-[17rem]\">\n      {/* side buttons */}\n      <span\n        aria-hidden\n        className=\"absolute -left-[2px] top-[16%] h-7 w-[3px] rounded-l-sm bg-muted-foreground/30\"\n      />\n      <span\n        aria-hidden\n        className=\"absolute -left-[2px] top-[24%] h-11 w-[3px] rounded-l-sm bg-muted-foreground/30\"\n      />\n      <span\n        aria-hidden\n        className=\"absolute -right-[2px] top-[22%] h-14 w-[3px] rounded-r-sm bg-muted-foreground/30\"\n      />\n      <div className=\"aspect-[9/19.5] rounded-[2.5rem] border border-border bg-muted p-[0.35rem] shadow-[0_10px_20px_rgba(0,0,0,0.18),0_40px_60px_-20px_rgba(0,0,0,0.35)]\">\n        <div className=\"relative h-full w-full overflow-hidden rounded-[2.2rem] bg-background ring-1 ring-inset ring-border/70\">\n          {/* Camera island. Deliberately *not* themed — it is a hole in the\n              glass, so it stays black in light and dark, exactly as on a real\n              device showing a dark app. */}\n          <div\n            aria-hidden\n            className=\"absolute left-1/2 top-2 z-10 h-[1.1rem] w-[4.25rem] -translate-x-1/2 rounded-full bg-black\"\n          />\n          {children ?? (\n            <div className=\"flex h-full w-full items-center justify-center bg-muted\">\n              <span className=\"text-[0.65rem] font-medium uppercase tracking-wider text-muted-foreground/70\">\n                Replace screen\n              </span>\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\n/* ── component ───────────────────────────────────────────────── */\n\n/**\n * A hero whose phone mockup starts tilted back on the X axis, straightens to\n * flat as the section scrolls through, and climbs up over the headline.\n *\n * - Pass the headline as `titleComponent`; it renders *behind* the phone.\n * - Pass the screen content as `children` (a `<video>`, an `<img>`, or JSX).\n * - Tune how long the motion takes with `className=\"h-[…]\"`.\n *\n * Honours `prefers-reduced-motion`: the phone renders flat and static, and no\n * scroll listener is attached.\n */\nexport function PhoneScrollHero({\n  titleComponent,\n  children,\n  overlap = 20,\n  tilt = 22,\n  className,\n}: PhoneScrollHeroProps) {\n  const prefersReducedMotion = useReducedMotion();\n  const sectionRef = React.useRef<HTMLElement>(null);\n\n  // Scroll progress (0 → 1) read from the section's *own* window. motion's\n  // `useScroll` always binds to the global `window`, so it goes dead when the\n  // component is portaled into another document (the docs preview iframe).\n  // `ownerDocument.defaultView` is correct in the top page and the frame alike.\n  const progress = useMotionValue(0);\n\n  useIsomorphicLayoutEffect(() => {\n    if (prefersReducedMotion) return;\n    const el = sectionRef.current;\n    if (!el) return;\n    const win = el.ownerDocument.defaultView ?? window;\n    let raf = 0;\n    // Mirrors motion's default [\"start start\", \"end end\"] offset: 0 when the\n    // section top meets the viewport top, 1 when its bottom meets the bottom.\n    const update = () => {\n      raf = 0;\n      const rect = el.getBoundingClientRect();\n      const travel = Math.max(1, rect.height - win.innerHeight);\n      progress.set(Math.min(1, Math.max(0, -rect.top / travel)));\n    };\n    const onScroll = () => {\n      if (!raf) raf = win.requestAnimationFrame(update);\n    };\n    update();\n    win.addEventListener(\"scroll\", onScroll, { passive: true });\n    win.addEventListener(\"resize\", onScroll);\n    return () => {\n      win.removeEventListener(\"scroll\", onScroll);\n      win.removeEventListener(\"resize\", onScroll);\n      if (raf) win.cancelAnimationFrame(raf);\n    };\n  }, [prefersReducedMotion, progress]);\n\n  const rotateX = useTransform(progress, [0, 1], [tilt, 0]);\n  const scale = useTransform(progress, [0, 1], [0.94, 1]);\n  // The title drifts up far slower than the phone, so the gap between them\n  // closes as you scroll and the phone ends up over the letters.\n  const titleY = useTransform(progress, [0, 1], [0, -24]);\n  const phoneY = useTransform(progress, [0, 1], [\"0%\", `-${overlap}%`]);\n\n  // Reduced motion: no tilt, no climb — the phone just sits under the title.\n  const still = Boolean(prefersReducedMotion);\n\n  return (\n    <section\n      ref={sectionRef}\n      aria-label=\"Hero\"\n      className={cn(\n        \"relative flex h-[60rem] w-full items-center justify-center overflow-hidden p-2 md:h-[80rem] md:p-8\",\n        className,\n      )}\n    >\n      <div className=\"w-full\" style={{ perspective: \"1200px\" }}>\n        <motion.div\n          style={{ y: still ? 0 : titleY }}\n          className=\"relative z-0 mx-auto max-w-5xl px-4 text-center\"\n        >\n          {titleComponent}\n        </motion.div>\n\n        {/* Higher z-index than the title — this is what makes it overlap. */}\n        <motion.div\n          style={{\n            y: still ? 0 : phoneY,\n            rotateX: still ? 0 : rotateX,\n            scale: still ? 1 : scale,\n          }}\n          className=\"relative z-10 -mt-6 md:-mt-12\"\n        >\n          <PhoneFrame>{children}</PhoneFrame>\n        </motion.div>\n      </div>\n    </section>\n  );\n}\n\nexport default PhoneScrollHero;\n",
      "type": "registry:ui",
      "target": "components/ruixen/phone-scroll-hero.tsx"
    }
  ]
}