ORBIT-EO ideas
You are given a task to integrate an existing React component in the codebase

The codebase should support:

  • shadcn project structure
  • Tailwind CSS
  • Typescript

If it doesn’t, provide instructions on how to setup project via shadcn CLI, install Tailwind or Typescript.

Determine the default path for components and styles.
If default path for components is not /components/ui, provide instructions on why it’s important to create this folder
Copy-paste this component to /components/ui folder:

sunset-skyline-hero.tsx
"use client"
 
import { useEffect, useRef, useState } from "react"
 
// ─────────────────────────────────────────────────────────────
// TOKYO SKYLINE HERO — locked scroll-scrub video hero
// Same technique as the metro/"City Opens" hero: body pinned with
// position:fixed while active, wheel/touch drive video.currentTime
// forward and backward, nothing else moves. Past the point where
// the source video ends, scroll keeps driving two extra layers —
// a composing Japanese brand mark and a transparent skyline
// cutout placed above it — before the page unlocks and continues.
// No dependencies, system fonts only.
// ─────────────────────────────────────────────────────────────
 
export interface NavItem {
  label: string
  href: string
}
 
export interface TokyoSkylineHeroProps {
  videoSrc?: string
  /** Transparent-background PNG of just the skyline silhouette, cropped identically to the video's last frame. */
  skylineSrc?: string
  title?: string
  scrollHint?: string
  /** Japanese brand mark that composes in once the video reaches its end. */
  brandMark?: string
  navItems?: NavItem[]
  signature?: { name: string; url: string } | false
  /** Total input distance (px) needed to scrub the full video. Tune to taste. */
  scrubDistance?: number
  className?: string
  style?: React.CSSProperties
}
 
const DEFAULT_VIDEO = "https://cdn.21st.dev/assets/mirror/23/234bc821170e75a6b8d2e42a952078f858eb054a51fab2a5baa928a10fdc245d.mp4"
const DEFAULT_SKYLINE = "https://cdn.21st.dev/assets/mirror/97/97fe4402ea1d5d05c2b82befe6dbf73da1d6669b995e6d5ae9f2ebea68b1107a.png"
const DEFAULT_SIGNATURE = { name: "guglielmogiannattasio.exe", url: "https://www.guglielmogiannattasio.it" }
const DEFAULT_NAV: NavItem[] = [
  { label: "ホーム", href: "#home" },
  { label: "作品", href: "#work" },
  { label: "予約", href: "#contact" },
]
const SANS = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
 
const COL_BG = "#1a0f0a"
const COL_TEXT = "#fff4e8"
 
// Progress breakpoints — tuned so the window disappears early (this
// footage clears it inside the first quarter) and the last ~20% of
// scroll is spent on the reveal rather than on the video itself.
const VIDEO_END = 0.78
const TEXT_START = 0.8
const TEXT_END = 0.93
const SKYLINE_START = 0.85
const SKYLINE_END = 0.96
 
function clamp(v: number, min: number, max: number) {
  return Math.min(max, Math.max(min, v))
}
 
export default function TokyoSkylineHero({
  videoSrc = DEFAULT_VIDEO,
  skylineSrc = DEFAULT_SKYLINE,
  title = "陽は、まだ沈んでいない。",
  scrollHint = "SCROLL",
  brandMark = "東京の窓",
  navItems = DEFAULT_NAV,
  signature = DEFAULT_SIGNATURE,
  scrubDistance = 3200,
  className,
  style,
}: TokyoSkylineHeroProps) {
  const sectionRef = useRef<HTMLDivElement>(null)
  const videoRef = useRef<HTMLVideoElement>(null)
  const titleRef = useRef<HTMLDivElement>(null)
  const hintRef = useRef<HTMLDivElement>(null)
  const brandRef = useRef<HTMLDivElement>(null)
  const skylineRef = useRef<HTMLImageElement>(null)
  const progressBarRef = useRef<HTMLDivElement>(null)
  const [ready, setReady] = useState(false)
 
  useEffect(() => {
    const video = videoRef.current
    const section = sectionRef.current
    if (!video || !section) return
 
    const reduceMotion =
      typeof window !== "undefined" &&
      window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
 
    let duration = 0
    let rafId = 0
    let targetProgress = 0
    let currentProgress = 0
    let hasStartedScrolling = false
    let isSeeking = false
    let pendingTime: number | null = null
    let locked = false
    let lockedScrollY = 0
    let touchStartY = 0
 
    const onLoadedData = () => {
      duration = video.duration || 0
      setReady(true)
      if (reduceMotion) {
        video.currentTime = duration
      }
    }
    video.addEventListener("loadeddata", onLoadedData)
 
    const onSeeked = () => {
      isSeeking = false
      if (pendingTime !== null) {
        const t = pendingTime
        pendingTime = null
        isSeeking = true
        video.currentTime = t
      }
    }
    video.addEventListener("seeked", onSeeked)
 
    function seekTo(t: number) {
      if (isSeeking) {
        pendingTime = t
        return
      }
      isSeeking = true
      video!.currentTime = t
    }
 
    function engageLock() {
      if (locked || typeof document === "undefined") return
      locked = true
      lockedScrollY = window.scrollY
      const b = document.body.style
      b.position = "fixed"
      b.top = `-${lockedScrollY}px`
      b.left = "0"
      b.right = "0"
      b.width = "100%"
    }
 
    function releaseLock() {
      if (!locked || typeof document === "undefined") return
      locked = false
      const y = lockedScrollY
      const b = document.body.style
      b.position = ""
      b.top = ""
      b.left = ""
      b.right = ""
      b.width = ""
      window.scrollTo(0, y)
    }
 
    engageLock()
 
    function addDelta(deltaY: number) {
      const next = clamp(targetProgress + deltaY / scrubDistance, 0, 1)
      targetProgress = next
      if (targetProgress > 0.001) hasStartedScrolling = true
      return true
    }
 
    const onWheel = (e: WheelEvent) => {
      addDelta(e.deltaY)
      e.preventDefault()
    }
 
    const onTouchStart = (e: TouchEvent) => {
      touchStartY = e.touches[0]?.clientY ?? 0
    }
    const onTouchMove = (e: TouchEvent) => {
      const y = e.touches[0]?.clientY ?? touchStartY
      const deltaY = touchStartY - y
      touchStartY = y
      addDelta(deltaY)
      e.preventDefault()
    }
 
    window.addEventListener("wheel", onWheel, { passive: false })
    window.addEventListener("touchstart", onTouchStart, { passive: true })
    window.addEventListener("touchmove", onTouchMove, { passive: false })
 
    function frame() {
      currentProgress += (targetProgress - currentProgress) * 0.18
 
      if (duration > 0) {
        const videoT = clamp(currentProgress / VIDEO_END, 0, 1)
        seekTo(videoT * duration)
      }
 
      if (titleRef.current) {
        const t = 1 - clamp(currentProgress / 0.3, 0, 1)
        titleRef.current.style.opacity = String(t)
        titleRef.current.style.transform = `translateY(${(1 - t) * -24}px) scale(${0.96 + t * 0.04})`
        titleRef.current.style.filter = `blur(${(1 - t) * 10}px)`
      }
      if (hintRef.current) {
        hintRef.current.style.opacity = hasStartedScrolling ? "0" : "1"
      }
      if (brandRef.current) {
        const t = clamp((currentProgress - TEXT_START) / (TEXT_END - TEXT_START), 0, 1)
        brandRef.current.style.opacity = String(t)
        brandRef.current.style.transform = `translateY(${(1 - t) * 16}px) scale(${0.98 + t * 0.02})`
        brandRef.current.style.filter = `blur(${(1 - t) * 6}px)`
        brandRef.current.style.letterSpacing = `${(1 - t) * 0.25}em`
      }
      if (skylineRef.current) {
        const t = clamp((currentProgress - SKYLINE_START) / (SKYLINE_END - SKYLINE_START), 0, 1)
        skylineRef.current.style.opacity = String(t)
      }
      if (progressBarRef.current) {
        progressBarRef.current.style.transform = `scaleX(${currentProgress})`
      }
 
      rafId = requestAnimationFrame(frame)
    }
 
    if (!reduceMotion) {
      rafId = requestAnimationFrame(frame)
    }
 
    return () => {
      video.removeEventListener("loadeddata", onLoadedData)
      video.removeEventListener("seeked", onSeeked)
      window.removeEventListener("wheel", onWheel)
      window.removeEventListener("touchstart", onTouchStart)
      window.removeEventListener("touchmove", onTouchMove)
      cancelAnimationFrame(rafId)
      releaseLock()
    }
  }, [scrubDistance])
 
  return (
    <div
      ref={sectionRef}
      className={className}
      style={{
        position: "relative",
        height: "100dvh",
        width: "100%",
        overflow: "hidden",
        background: COL_BG,
        ...style,
      }}
    >
      <video
        ref={videoRef}
        src={videoSrc}
        muted
        playsInline
        preload="auto"
        style={{
          position: "absolute",
          inset: 0,
          width: "100%",
          height: "100%",
          objectFit: "cover",
          opacity: ready ? 1 : 0,
          transition: "opacity 0.6s ease",
        }}
      />
 
      {/* Japanese brand mark — sits above the video but below the skyline cutout, so buildings appear to sit in front of it. */}
      <div
        ref={brandRef}
        style={{
          position: "absolute",
          inset: 0,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          padding: "0 8% 20vh",
                    textAlign: "center",
          opacity: 0,
          pointerEvents: "none",
          zIndex: 1,
        }}
      >
                <style>{`
         @media (max-width: 640px) {
  .tsh-brand-mark {
    writing-mode: vertical-rl;
    text-orientation: upright;
    font-size: clamp(64px, 22vw, 140px) !important;
    line-height: 1.15 !important;
    letter-spacing: 0.05em !important;
    transform: translate(-18%, -12%);
  }
}
        `}</style>
        <span
          className="tsh-brand-mark"
          style={{
            fontFamily: SANS,
            fontWeight: 800,
            fontSize: "clamp(56px, 15vw, 240px)",
            lineHeight: 1.05,
            letterSpacing: "-0.01em",
            color: COL_TEXT,
            textShadow: "0 8px 60px rgba(0,0,0,0.45)",
          }}
        >
          {brandMark}
        </span>
      </div>
 
      {/* Skyline cutout — transparent everywhere but the buildings, pixel-aligned with the video beneath it. */}
      <img
        ref={skylineRef}
        src={skylineSrc}
        alt=""
        aria-hidden="true"
        style={{
          position: "absolute",
          inset: 0,
          width: "100%",
          height: "100%",
          objectFit: "cover",
          opacity: 0,
          pointerEvents: "none",
          zIndex: 2,
        }}
      />
 
      <div
        style={{
          position: "absolute",
          inset: 0,
          background: "linear-gradient(180deg, rgba(26,15,10,0.35), rgba(26,15,10,0) 30%, rgba(26,15,10,0.15) 70%, rgba(26,15,10,0.55))",
          pointerEvents: "none",
          zIndex: 3,
        }}
      />
 
      <div
        ref={titleRef}
        style={{
          position: "absolute",
          inset: 0,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          padding: "0 6%",
          textAlign: "center",
          pointerEvents: "none",
          zIndex: 3,
        }}
      >
        <span
          style={{
            fontFamily: SANS,
            fontWeight: 800,
            fontSize: "clamp(24px, 5.4vw, 64px)",
            lineHeight: 1.25,
            letterSpacing: "-0.01em",
            color: COL_TEXT,
            textShadow: "0 4px 30px rgba(0,0,0,0.5)",
          }}
        >
          {title}
        </span>
      </div>
 
      {/* Liquid navbar — always visible, independent of scroll progress. */}
      <nav
        style={{
          position: "absolute",
          top: "clamp(14px, 3vh, 28px)",
          left: "50%",
          transform: "translateX(-50%)",
          display: "flex",
          alignItems: "center",
          gap: "clamp(14px, 2.5vw, 28px)",
          padding: "clamp(8px, 1.4vw, 12px) clamp(18px, 3vw, 28px)",
          borderRadius: 999,
          background: "rgba(255,244,232,0.08)",
          backdropFilter: "blur(16px) saturate(160%)",
          WebkitBackdropFilter: "blur(16px) saturate(160%)",
          border: "1px solid rgba(255,244,232,0.14)",
          zIndex: 4,
        }}
      >
        {navItems.map((item) => (
          <a
            key={item.href}
            href={item.href}
            style={{
              fontFamily: SANS,
              fontWeight: 600,
              fontSize: "clamp(12px, 1.4vw, 14px)",
              color: "rgba(255,244,232,0.85)",
              textDecoration: "none",
              whiteSpace: "nowrap",
              transition: "color 0.2s ease",
            }}
            onMouseEnter={(e: React.MouseEvent<HTMLAnchorElement>) => {
              e.currentTarget.style.color = COL_TEXT
            }}
            onMouseLeave={(e: React.MouseEvent<HTMLAnchorElement>) => {
              e.currentTarget.style.color = "rgba(255,244,232,0.85)"
            }}
          >
            {item.label}
          </a>
        ))}
      </nav>
 
      <div
        ref={hintRef}
        style={{
          position: "absolute",
          left: "50%",
          bottom: "clamp(20px, 6vh, 48px)",
          transform: "translateX(-50%)",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          gap: 8,
          color: "rgba(255,244,232,0.75)",
          fontFamily: SANS,
          fontSize: "clamp(10px, 1.4vw, 12px)",
          fontWeight: 600,
          letterSpacing: "0.3em",
          transition: "opacity 0.4s ease",
          pointerEvents: "none",
          zIndex: 4,
        }}
      >
        <span>{scrollHint}</span>
        <svg width="14" height="18" viewBox="0 0 14 18" style={{ animation: "tokyo-hero-bounce 1.6s ease-in-out infinite" }}>
          <style>{`
            @keyframes tokyo-hero-bounce {
              0%, 100% { transform: translateY(0); opacity: 0.5; }
              50% { transform: translateY(5px); opacity: 1; }
            }
          `}</style>
          <path d="M7 1 L7 17 M2 12 L7 17 L12 12" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round" />
        </svg>
      </div>
 
      <div
        style={{
          position: "absolute",
          left: 0,
          right: 0,
          bottom: 0,
          height: 2,
          background: "rgba(255,244,232,0.12)",
          zIndex: 4,
        }}
      >
        <div
          ref={progressBarRef}
          style={{
            height: "100%",
            width: "100%",
            background: "linear-gradient(90deg, rgba(255,244,232,0.5), rgba(255,244,232,0.95))",
            transform: "scaleX(0)",
            transformOrigin: "left center",
          }}
        />
      </div>
 
      {signature && (
        <span
          style={{
            position: "absolute",
            right: "clamp(12px, 2.5vw, 24px)",
            bottom: "clamp(10px, 2vw, 18px)",
            fontFamily: SANS,
            fontWeight: 500,
            fontSize: "clamp(11px, 1.4vw, 13px)",
            letterSpacing: "0.01em",
            color: "rgba(255,244,232,0.6)",
            zIndex: 4,
          }}
        >
          by{" "}
          <a
            href={signature.url}
            target="_blank"
            rel="noopener noreferrer"
            style={{
              color: "rgba(255,244,232,0.6)",
              textDecoration: "none",
              transition: "color 0.2s ease",
            }}
            onMouseEnter={(e: React.MouseEvent<HTMLAnchorElement>) => {
              e.currentTarget.style.color = COL_TEXT
            }}
            onMouseLeave={(e: React.MouseEvent<HTMLAnchorElement>) => {
              e.currentTarget.style.color = "rgba(255,244,232,0.6)"
            }}
          >
            {signature.name}
          </a>
        </span>
      )}
    </div>
  )
}
 
 
demo.tsx
import TokyoSkylineHero from "@/components/ui/sunset-skyline-hero"
 
export default function Demo() {
  return <TokyoSkylineHero />
}

https://21st.dev/community/components/s/hero?preview=%2F%40gughigug%2Fcomponents%2Fsunset-skyline-hero