{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "layouts-scroll-header",
  "type": "registry:block",
  "title": "Scroll Header",
  "description": "Scroll-aware sticky header with fluid animated tab transitions. Ships with home, about, contact, and danger-zone demo pages.",
  "dependencies": [
    "motion",
    "lucide-react",
    "next-themes"
  ],
  "registryDependencies": [
    "skeleton"
  ],
  "files": [
    {
      "path": "components/layouts/scroll-header/scroll-header.tsx",
      "content": "\"use client\";\n\nimport React from \"react\";\nimport Link from \"next/link\";\nimport { ArrowLeftIcon, Sparkles } from \"lucide-react\";\nimport { motion, useScroll, useSpring, useTransform } from \"motion/react\";\n\nimport { ModeToggle } from \"@/components/mode-toggle\";\nimport { AnimatedTabs } from \"@/components/layouts/scroll-header/animated-tabs\";\n\nconst tabs = [\n  { label: \"Home\", value: \"home\", href: \"/preview/layouts/scroll-header\" },\n  {\n    label: \"About\",\n    value: \"about\",\n    href: \"/preview/layouts/scroll-header/about\",\n  },\n  {\n    label: \"Contact\",\n    value: \"contact\",\n    href: \"/preview/layouts/scroll-header/contact\",\n  },\n  {\n    label: \"Danger Zone\",\n    value: \"danger-zone\",\n    href: \"/preview/layouts/scroll-header/danger-zone\",\n  },\n];\n\n// Spring profile tuned for scroll-linked motion: tight, near-critical damping,\n// low mass. Responds quickly, settles cleanly, no visible overshoot.\nconst SCROLL_SPRING = { stiffness: 500, damping: 50, mass: 0.5 } as const;\n\nexport function ScrollHeader() {\n  // Read the window scroll position as a motion value (rAF-driven,\n  // passive listener, no React state, no re-renders per frame).\n  const { scrollY } = useScroll();\n\n  // Logo shrink: scale 1 → 0.8 over the first 33px of scroll.\n  const logoScaleRaw = useTransform(scrollY, [0, 33], [1, 0.8], {\n    clamp: true,\n  });\n  const logoScale = useSpring(logoScaleRaw, SCROLL_SPRING);\n\n  // Tab strip nudges right 0 → 40px over the first 80px of scroll to\n  // clear space for the shrunken logo corner.\n  const tabXRaw = useTransform(scrollY, [0, 80], [0, 40], { clamp: true });\n  const tabX = useSpring(tabXRaw, SCROLL_SPRING);\n\n  return (\n    <>\n      <header className=\"relative w-full bg-background\">\n        <motion.div\n          className=\"fixed left-0 top-0 z-50 pl-5 pt-5\"\n          style={{ scale: logoScale, transformOrigin: \"0 0\" }}\n        >\n          <div className=\"flex h-5 w-5 items-center justify-center rounded-md bg-foreground\">\n            <Sparkles className=\"h-3 w-3 text-background\" />\n          </div>\n        </motion.div>\n\n        <div className=\"flex items-center justify-between px-5 pb-0 pl-14 pt-3 font-mono\">\n          <div className=\"flex items-center gap-2\">\n            <Link\n              href=\"/layouts\"\n              className=\"inline-flex items-center gap-1.5 text-sm font-medium text-muted-foreground transition-colors hover:text-foreground\"\n            >\n              <ArrowLeftIcon className=\"h-3.5 w-3.5\" />\n              Layouts\n            </Link>\n            <span className=\"text-sm font-medium text-muted-foreground\">/</span>\n            <span className=\"text-sm font-medium\">Scroll Header</span>\n          </div>\n          <div className=\"flex items-center justify-end gap-2\">\n            <ModeToggle />\n          </div>\n        </div>\n      </header>\n\n      <div className=\"sticky top-0 z-40 overflow-x-hidden border-b border-border bg-background\">\n        <div className=\"flex items-center justify-center\">\n          <motion.div\n            className=\"flex flex-1 justify-center\"\n            style={{ x: tabX }}\n          >\n            <AnimatedTabs tabs={tabs} />\n          </motion.div>\n        </div>\n      </div>\n    </>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/layouts/scroll-header/scroll-header.tsx"
    },
    {
      "path": "components/layouts/scroll-header/animated-tabs.tsx",
      "content": "\"use client\";\n\nimport React from \"react\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { AnimatePresence, motion, type Transition } from \"motion/react\";\n\nimport useTabs, { type Tab } from \"@/hooks/layouts/use-tabs\";\nimport { cn } from \"@/lib/utils\";\n\ninterface AnimatedTabsProps {\n  tabs: Tab[];\n}\n\nconst transition = {\n  type: \"tween\",\n  ease: \"easeOut\",\n  duration: 0.15,\n};\n\nconst getHoverAnimationProps = (hoveredRect: DOMRect, navRect: DOMRect) => ({\n  x: hoveredRect.left - navRect.left - 10,\n  y: hoveredRect.top - navRect.top - 4,\n  width: hoveredRect.width + 20,\n  height: hoveredRect.height + 10,\n});\n\nconst Tabs = ({\n  tabs,\n  selectedTabIndex,\n  setSelectedTab,\n}: {\n  tabs: Tab[];\n  selectedTabIndex: number;\n  setSelectedTab: (input: [number, number]) => void;\n}) => {\n  const [buttonRefs, setButtonRefs] = React.useState<\n    Array<HTMLAnchorElement | null>\n  >([]);\n\n  React.useEffect(() => {\n    setButtonRefs((prev) => prev.slice(0, tabs.length));\n  }, [tabs.length]);\n\n  const navRef = React.useRef<HTMLDivElement>(null);\n  const navRect = navRef.current?.getBoundingClientRect();\n\n  const selectedRect = buttonRefs[selectedTabIndex]?.getBoundingClientRect();\n\n  const [hoveredTabIndex, setHoveredTabIndex] = React.useState<number | null>(\n    null,\n  );\n  const hoveredRect =\n    buttonRefs[hoveredTabIndex ?? -1]?.getBoundingClientRect();\n\n  return (\n    <nav\n      ref={navRef}\n      className=\"relative flex flex-shrink-0 items-center justify-center py-2\"\n      onPointerLeave={() => setHoveredTabIndex(null)}\n    >\n      {tabs.map((item, i) => {\n        const isActive = selectedTabIndex === i;\n        return (\n          <Link\n            key={item.value}\n            href={item.href || \"#\"}\n            className=\"relative z-20 flex h-8 cursor-pointer select-none items-center rounded-md bg-transparent px-4 transition-colors\"\n            onPointerEnter={() => setHoveredTabIndex(i)}\n            onFocus={() => setHoveredTabIndex(i)}\n            onClick={() => setSelectedTab([i, i > selectedTabIndex ? 1 : -1])}\n          >\n            <motion.span\n              ref={(el) => {\n                buttonRefs[i] = el as HTMLAnchorElement;\n              }}\n              className={cn(\"block text-sm\", {\n                \"text-zinc-500\": !isActive,\n                \"font-semibold text-black dark:text-white\": isActive,\n              })}\n            >\n              <span\n                className={item.value === \"danger-zone\" ? \"text-red-500\" : \"\"}\n              >\n                {item.label}\n              </span>\n            </motion.span>\n          </Link>\n        );\n      })}\n\n      <AnimatePresence>\n        {hoveredRect && navRect && (\n          <motion.div\n            key=\"hover\"\n            className={`absolute left-0 top-0 z-10 rounded-md ${\n              hoveredTabIndex ===\n              tabs.findIndex(({ value }) => value === \"danger-zone\")\n                ? \"bg-red-100 dark:bg-red-500/30\"\n                : \"bg-zinc-100 dark:bg-zinc-800\"\n            }`}\n            initial={{\n              ...getHoverAnimationProps(hoveredRect, navRect),\n              opacity: 0,\n            }}\n            animate={{\n              ...getHoverAnimationProps(hoveredRect, navRect),\n              opacity: 1,\n            }}\n            exit={{\n              ...getHoverAnimationProps(hoveredRect, navRect),\n              opacity: 0,\n            }}\n            transition={transition as Transition}\n          />\n        )}\n      </AnimatePresence>\n\n      <AnimatePresence>\n        {selectedRect && navRect && (\n          <motion.div\n            className={`absolute bottom-0 left-0 z-10 h-[2px] ${\n              selectedTabIndex ===\n              tabs.findIndex(({ value }) => value === \"danger-zone\")\n                ? \"bg-red-500\"\n                : \"bg-black dark:bg-white\"\n            }`}\n            initial={false}\n            animate={{\n              width: selectedRect.width + 18,\n              x: `calc(${selectedRect.left - navRect.left - 9}px)`,\n              opacity: 1,\n            }}\n            transition={transition as Transition}\n          />\n        )}\n      </AnimatePresence>\n    </nav>\n  );\n};\n\nexport function AnimatedTabs({ tabs }: AnimatedTabsProps) {\n  const pathname = usePathname();\n\n  const [hookProps] = React.useState(() => {\n    const matchedTab =\n      tabs.find((tab) => tab.href && pathname?.startsWith(tab.href)) ?? tabs[0];\n    return {\n      tabs: tabs.map(({ label, value, subRoutes, href }) => ({\n        label,\n        value,\n        subRoutes,\n        href,\n      })),\n      initialTabId: matchedTab.value,\n    };\n  });\n\n  const framer = useTabs(hookProps);\n\n  return (\n    <div className=\"relative flex w-full items-start justify-start overflow-x-auto overflow-y-hidden\">\n      <Tabs {...framer.tabProps} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/layouts/scroll-header/animated-tabs.tsx"
    },
    {
      "path": "components/layouts/scroll-header/demo-skeleton.tsx",
      "content": "import { Skeleton } from \"@/components/ui/skeleton\";\n\n/**\n * Placeholder body content for the ScrollHeader demo pages. The demo's focus\n * is the scroll-linked header animation — real copy would distract from that,\n * so every tab page renders skeleton blocks of enough total height to make\n * the scroll interaction feel real.\n */\nexport function DemoSkeleton() {\n  return (\n    <div className=\"min-h-screen bg-zinc-100 dark:bg-background\">\n      <div className=\"container mx-auto px-6 py-16\">\n        <div className=\"mx-auto flex max-w-4xl flex-col gap-12\">\n          {/* Hero */}\n          <div className=\"flex flex-col gap-4\">\n            <Skeleton className=\"h-10 w-3/4\" />\n            <Skeleton className=\"h-4 w-full\" />\n            <Skeleton className=\"h-4 w-5/6\" />\n          </div>\n\n          {/* Two-column paragraph block */}\n          <div className=\"grid gap-8 md:grid-cols-2\">\n            {Array.from({ length: 2 }).map((_, i) => (\n              <div key={i} className=\"flex flex-col gap-3\">\n                <Skeleton className=\"h-6 w-2/3\" />\n                <Skeleton className=\"h-3 w-full\" />\n                <Skeleton className=\"h-3 w-full\" />\n                <Skeleton className=\"h-3 w-5/6\" />\n                <Skeleton className=\"h-3 w-4/6\" />\n              </div>\n            ))}\n          </div>\n\n          {/* Card block */}\n          <div className=\"flex flex-col gap-3 rounded-lg border border-border p-6\">\n            <Skeleton className=\"h-5 w-1/3\" />\n            <Skeleton className=\"h-3 w-full\" />\n            <Skeleton className=\"h-3 w-full\" />\n            <Skeleton className=\"h-3 w-3/4\" />\n            <div className=\"mt-3 grid gap-2\">\n              {Array.from({ length: 4 }).map((_, i) => (\n                <Skeleton key={i} className=\"h-3 w-full\" />\n              ))}\n            </div>\n          </div>\n\n          {/* Feature grid */}\n          <div className=\"grid gap-6 sm:grid-cols-2 lg:grid-cols-3\">\n            {Array.from({ length: 6 }).map((_, i) => (\n              <div\n                key={i}\n                className=\"flex flex-col gap-3 rounded-lg border border-border p-5\"\n              >\n                <Skeleton className=\"h-9 w-9 rounded-md\" />\n                <Skeleton className=\"h-4 w-2/3\" />\n                <Skeleton className=\"h-3 w-full\" />\n                <Skeleton className=\"h-3 w-5/6\" />\n              </div>\n            ))}\n          </div>\n\n          {/* CTA row */}\n          <div className=\"flex flex-col items-center gap-4\">\n            <Skeleton className=\"h-7 w-1/2\" />\n            <Skeleton className=\"h-4 w-3/4\" />\n            <div className=\"flex gap-3 pt-2\">\n              <Skeleton className=\"h-11 w-32 rounded-lg\" />\n              <Skeleton className=\"h-11 w-32 rounded-lg\" />\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/layouts/scroll-header/demo-skeleton.tsx"
    },
    {
      "path": "components/layouts/scroll-header/footer.tsx",
      "content": "import Link from \"next/link\";\nimport { Button } from \"@/components/ui/button\";\n\nconst footerLinks = [\n  { label: \"Next.js\", href: \"https://nextjs.org/\" },\n  { label: \"Tailwind CSS\", href: \"https://tailwindcss.com/\" },\n  { label: \"Shadcn/ui\", href: \"https://ui.shadcn.com/\" },\n  { label: \"Motion\", href: \"https://motion.dev/\" },\n];\n\nexport function ScrollHeaderFooter() {\n  return (\n    <footer className=\"flex flex-col items-center gap-6 border-t border-border py-6\">\n      <p className=\"leading-7 [&:not(:first-child)]:mt-6\">\n        Scroll-aware navigation with fluid tab transitions. ✨\n      </p>\n      <ul className=\"flex items-center justify-center\">\n        {footerLinks.map((link) => (\n          <li key={link.href}>\n            <Button\n              variant=\"link\"\n              asChild\n              className=\"scroll-m-20 text-xl font-semibold tracking-tight\"\n            >\n              <Link target=\"_blank\" href={link.href}>\n                {link.label}\n              </Link>\n            </Button>\n          </li>\n        ))}\n      </ul>\n    </footer>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/layouts/scroll-header/footer.tsx"
    },
    {
      "path": "components/mode-toggle.tsx",
      "content": "\"use client\";\n\nimport { useState, useEffect, useRef, useId } from \"react\";\nimport { useTheme } from \"next-themes\";\nimport { motion } from \"motion/react\";\n\n/* ── Audio ── */\n\nlet _ctx: AudioContext | null = null;\nlet _buf: AudioBuffer | null = null;\n\nfunction audioCtx() {\n  if (!_ctx) {\n    _ctx = new (window.AudioContext ||\n      (window as unknown as { webkitAudioContext: typeof AudioContext })\n        .webkitAudioContext)();\n  }\n  if (_ctx.state === \"suspended\") _ctx.resume();\n  return _ctx;\n}\n\nfunction ensureBuf(ac: AudioContext): AudioBuffer {\n  if (_buf && _buf.sampleRate === ac.sampleRate) return _buf;\n  const rate = ac.sampleRate;\n  const len = Math.floor(rate * 0.006);\n  const buf = ac.createBuffer(1, len, rate);\n  const ch = buf.getChannelData(0);\n  for (let i = 0; i < len; i++) {\n    const t = i / len;\n    const sine = Math.sin(2 * Math.PI * 3400 * t);\n    const noise = Math.random() * 2 - 1;\n    ch[i] = (sine * 0.6 + noise * 0.4) * (1 - t) ** 3;\n  }\n  _buf = buf;\n  return buf;\n}\n\nfunction tick(last: React.MutableRefObject<number>) {\n  const now = performance.now();\n  if (now - last.current < 80) return;\n  last.current = now;\n  try {\n    const ac = audioCtx();\n    const buf = ensureBuf(ac);\n    const src = ac.createBufferSource();\n    const gain = ac.createGain();\n    src.buffer = buf;\n    gain.gain.value = 0.08;\n    src.connect(gain);\n    gain.connect(ac.destination);\n    src.start();\n  } catch {\n    /* silent */\n  }\n}\n\n/* ── Component ── */\n\nexport function ModeToggle() {\n  const { resolvedTheme, setTheme } = useTheme();\n  const rawId = useId();\n  const maskId = `mt${rawId.replace(/:/g, \"\")}`;\n  const lastSnd = useRef(0);\n  const isFirst = useRef(true);\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => {\n    setMounted(true);\n    requestAnimationFrame(() => {\n      isFirst.current = false;\n    });\n  }, []);\n\n  const isDark = resolvedTheme === \"dark\";\n\n  const toggle = () => {\n    setTheme(isDark ? \"light\" : \"dark\");\n    tick(lastSnd);\n  };\n\n  /* Placeholder during SSR to avoid layout shift */\n  if (!mounted) {\n    return <div style={{ width: 32, height: 32 }} />;\n  }\n\n  const spring = isFirst.current\n    ? { duration: 0 }\n    : { type: \"spring\" as const, stiffness: 380, damping: 30 };\n\n  return (\n    <motion.button\n      onClick={toggle}\n      whileHover={{ scale: 1.1 }}\n      whileTap={{ scale: 0.86 }}\n      transition={{ type: \"spring\", stiffness: 400, damping: 25 }}\n      style={{\n        background: \"none\",\n        border: \"none\",\n        cursor: \"pointer\",\n        padding: 6,\n        width: 32,\n        height: 32,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        color: \"currentColor\",\n        borderRadius: 8,\n        outline: \"none\",\n        WebkitTapHighlightColor: \"transparent\",\n      }}\n      aria-label=\"Toggle theme\"\n    >\n      <motion.svg\n        width=\"18\"\n        height=\"18\"\n        viewBox=\"0 0 24 24\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        initial={false}\n        animate={{ rotate: isDark ? 270 : 0 }}\n        transition={spring}\n        style={{ overflow: \"visible\" }}\n      >\n        <mask id={maskId}>\n          <rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" fill=\"white\" />\n          <motion.circle\n            initial={false}\n            animate={{ cx: isDark ? 17 : 33, cy: isDark ? 8 : 0 }}\n            transition={spring}\n            r=\"9\"\n            fill=\"black\"\n          />\n        </mask>\n\n        <motion.circle\n          cx=\"12\"\n          cy=\"12\"\n          fill=\"currentColor\"\n          stroke=\"none\"\n          mask={`url(#${maskId})`}\n          initial={false}\n          animate={{ r: isDark ? 9 : 5 }}\n          transition={spring}\n        />\n\n        <motion.g\n          initial={false}\n          animate={{\n            opacity: isDark ? 0 : 1,\n            scale: isDark ? 0 : 1,\n            rotate: isDark ? -30 : 0,\n          }}\n          transition={spring}\n          style={{ transformOrigin: \"12px 12px\" }}\n        >\n          <line x1=\"12\" y1=\"1\" x2=\"12\" y2=\"3\" />\n          <line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"23\" />\n          <line x1=\"1\" y1=\"12\" x2=\"3\" y2=\"12\" />\n          <line x1=\"21\" y1=\"12\" x2=\"23\" y2=\"12\" />\n          <line x1=\"5.64\" y1=\"5.64\" x2=\"4.22\" y2=\"4.22\" />\n          <line x1=\"18.36\" y1=\"5.64\" x2=\"19.78\" y2=\"4.22\" />\n          <line x1=\"5.64\" y1=\"18.36\" x2=\"4.22\" y2=\"19.78\" />\n          <line x1=\"18.36\" y1=\"18.36\" x2=\"19.78\" y2=\"19.78\" />\n        </motion.g>\n      </motion.svg>\n    </motion.button>\n  );\n}\n\nModeToggle.displayName = \"ModeToggle\";\n",
      "type": "registry:component",
      "target": "components/mode-toggle.tsx"
    },
    {
      "path": "hooks/layouts/use-tabs.ts",
      "content": "import { useState } from \"react\";\n\nexport interface Tab {\n  label: string;\n  value: string;\n  subRoutes?: string[];\n  href?: string;\n}\n\nexport default function useTabs({\n  tabs,\n  initialTabId,\n  onChange,\n}: {\n  tabs: Tab[];\n  initialTabId: string;\n  onChange?: (id: string) => void;\n}) {\n  const [[selectedTabIndex, direction], setSelectedTab] = useState(() => {\n    const indexOfInitialTab = tabs.findIndex(\n      (tab) => tab.value === initialTabId,\n    );\n    return [indexOfInitialTab === -1 ? 0 : indexOfInitialTab, 0];\n  });\n\n  return {\n    tabProps: {\n      tabs,\n      selectedTabIndex,\n      onChange,\n      setSelectedTab,\n    },\n    selectedTab: tabs[selectedTabIndex],\n    contentProps: {\n      direction,\n      selectedTabIndex,\n    },\n  };\n}\n",
      "type": "registry:hook",
      "target": "hooks/layouts/use-tabs.ts"
    },
    {
      "path": "app/preview/layouts/scroll-header/layout.tsx",
      "content": "import type { Metadata } from \"next\";\nimport { ScrollHeader } from \"@/components/layouts/scroll-header/scroll-header\";\n\nexport const metadata: Metadata = {\n  title: \"Animated Header — Ruixen Layouts\",\n  description:\n    \"A scroll-aware header with fluid animated tab transitions. Inspired by modern product-site navigation patterns.\",\n};\n\nexport default function ScrollHeaderLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <>\n      <ScrollHeader />\n      {children}\n    </>\n  );\n}\n",
      "type": "registry:page",
      "target": "app/scroll-header/layout.tsx"
    },
    {
      "path": "app/preview/layouts/scroll-header/page.tsx",
      "content": "import { DemoSkeleton } from \"@/components/layouts/scroll-header/demo-skeleton\";\n\nexport default function ScrollHeaderHomePage() {\n  return <DemoSkeleton />;\n}\n",
      "type": "registry:page",
      "target": "app/scroll-header/page.tsx"
    },
    {
      "path": "app/preview/layouts/scroll-header/about/page.tsx",
      "content": "import { DemoSkeleton } from \"@/components/layouts/scroll-header/demo-skeleton\";\n\nexport default function ScrollHeaderAboutPage() {\n  return <DemoSkeleton />;\n}\n",
      "type": "registry:page",
      "target": "app/scroll-header/about/page.tsx"
    },
    {
      "path": "app/preview/layouts/scroll-header/contact/page.tsx",
      "content": "import { DemoSkeleton } from \"@/components/layouts/scroll-header/demo-skeleton\";\n\nexport default function ScrollHeaderContactPage() {\n  return <DemoSkeleton />;\n}\n",
      "type": "registry:page",
      "target": "app/scroll-header/contact/page.tsx"
    },
    {
      "path": "app/preview/layouts/scroll-header/danger-zone/page.tsx",
      "content": "import { DemoSkeleton } from \"@/components/layouts/scroll-header/demo-skeleton\";\n\nexport default function ScrollHeaderDangerZonePage() {\n  return <DemoSkeleton />;\n}\n",
      "type": "registry:page",
      "target": "app/scroll-header/danger-zone/page.tsx"
    }
  ]
}