{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"sticky-header","type":"registry:ui","title":"Sticky Header","description":"Condenses as you go down.","dependencies":["motion"],"categories":["scroll"],"docs":"https://www.interior.dev/docs/sticky-header","files":[{"path":"registry/interior/sticky-header.tsx","type":"registry:ui","target":"components/interior/sticky-header.tsx","content":"\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport {\n  motion,\n  useMotionValueEvent,\n  useReducedMotion,\n  useScroll,\n  useSpring,\n  useTransform,\n  type MotionValue,\n} from \"motion/react\";\n\nconst SMOOTH = { stiffness: 240, damping: 44, mass: 0.6 } as const;\n\nexport type UseCondenseOptions = {\n  range?: number;\n};\n\nexport type UseCondenseResult<T extends HTMLElement> = {\n  ref: React.RefObject<T | null>;\n  progress: MotionValue<number>;\n  condensed: boolean;\n};\n\nexport function useCondense<T extends HTMLElement = HTMLDivElement>({\n  range = 48,\n}: UseCondenseOptions = {}): UseCondenseResult<T> {\n  const ref = useRef<T | null>(null);\n  const { scrollY } = useScroll({ container: ref });\n\n  const progress = useTransform(scrollY, [0, Math.max(1, range)], [0, 1], {\n    clamp: true,\n  });\n\n  const [condensed, setCondensed] = useState(false);\n  useMotionValueEvent(progress, \"change\", (p) => {\n    const done = p >= 1;\n    setCondensed((prev) => (prev === done ? prev : done));\n  });\n\n  return { ref, progress, condensed };\n}\n\nexport type StickyHeaderProps = {\n  title: string;\n  children: React.ReactNode;\n  subtitle?: string;\n  leading?: React.ReactNode;\n  actions?: React.ReactNode;\n  expandedHeight?: number;\n  compactHeight?: number;\n  maxHeight?: number;\n  className?: string;\n};\n\nexport function StickyHeader({\n  title,\n  children,\n  subtitle,\n  leading,\n  actions,\n  expandedHeight = 68,\n  compactHeight = 48,\n  maxHeight = 320,\n  className = \"\",\n}: StickyHeaderProps) {\n  const tall = Math.max(expandedHeight, compactHeight);\n  const short = Math.min(expandedHeight, compactHeight);\n  const travel = Math.max(1, tall - short);\n\n  const { ref, progress: tracked, condensed } = useCondense<HTMLDivElement>({\n    range: Math.max(64, travel * 3),\n  });\n  const reduced = useReducedMotion();\n  const sprung = useSpring(tracked, SMOOTH);\n  const progress = reduced ? tracked : sprung;\n\n  const plate = useTransform(progress, (p) => (tall - travel * p) / tall);\n  const edge = useTransform(progress, (p) => tall - travel * p);\n  const lifted = useTransform(progress, [0, 0.12], [0, 1], { clamp: true });\n\n  const bigY = useTransform(progress, (p) => -travel * p);\n  const bigOpacity = useTransform(progress, [0, 0.45], [1, 0], { clamp: true });\n  const bigScale = useTransform(progress, (p) => 1 - 0.05 * p);\n\n  const smallOpacity = useTransform(progress, [0.55, 0.9], [0, 1], {\n    clamp: true,\n  });\n  const smallY = useTransform(smallOpacity, (o) => (1 - o) * 6);\n\n  return (\n    <div\n      className={`relative overflow-hidden rounded-[14px] border border-stone-200 bg-white shadow-[0_1px_2px_rgba(28,25,23,0.06),0_4px_10px_-8px_rgba(28,25,23,0.45)] dark:border-white/[0.16] dark:bg-[#1D1D1A] dark:shadow-[0_1px_6px_rgba(0,0,0,0.45)] ${className}`}\n    >\n      <div\n        ref={ref}\n\n        // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex\n        tabIndex={0}\n        role=\"region\"\n        aria-label={title}\n        style={{ maxHeight, scrollPaddingTop: short + 10 }}\n        className=\"overflow-y-auto overscroll-y-contain outline-none [scrollbar-gutter:stable] focus-visible:bg-[#4568FF]/[0.06] focus-visible:shadow-[inset_0_0_0_1px_#4568FF] dark:focus-visible:bg-[#93B0FF]/[0.06] dark:focus-visible:shadow-[inset_0_0_0_1px_#93B0FF]\"\n      >\n        <div aria-hidden style={{ height: tall }} />\n        {children}\n\n        <div\n          aria-hidden\n          className=\"pointer-events-none sticky bottom-0 -mt-6 h-6 bg-gradient-to-t from-white to-transparent dark:from-[#1D1D1A]\"\n        />\n      </div>\n      <header\n        data-condensed={condensed ? \"true\" : \"false\"}\n        style={{ height: tall }}\n        className=\"pointer-events-none absolute inset-x-0 top-0\"\n      >\n        <motion.div\n          aria-hidden\n          style={{ height: tall, scaleY: plate }}\n          className=\"absolute inset-x-0 top-0 origin-top bg-white dark:bg-[#1D1D1A]\"\n        />\n        <motion.div\n          aria-hidden\n          style={{ y: edge, opacity: lifted }}\n          className=\"absolute inset-x-0 top-0 h-px shadow-[0_6px_16px_-10px_rgba(28,25,23,0.4)] dark:shadow-[0_6px_16px_-10px_rgba(0,0,0,0.7)]\"\n        />\n        <motion.div\n          aria-hidden\n          style={{ y: edge, opacity: lifted }}\n          className=\"absolute inset-x-0 top-0 h-5 bg-gradient-to-b from-white to-transparent dark:from-[#1D1D1A]\"\n        />\n        <motion.div\n          aria-hidden\n          style={{ y: edge, opacity: lifted }}\n          className=\"absolute inset-x-0 top-0 h-px bg-stone-200 dark:bg-white/[0.16]\"\n        />\n        <div className=\"absolute inset-x-0 top-0 flex items-start gap-2.5 px-4 pt-3\">\n          {leading ? (\n            <div className=\"pointer-events-auto flex h-6 shrink-0 items-center\">\n              {leading}\n            </div>\n          ) : null}\n\n          <div className=\"relative min-w-0 flex-1\">\n            <motion.div\n              style={{\n                y: bigY,\n                opacity: bigOpacity,\n                scale: bigScale,\n                transformOrigin: \"left top\",\n              }}\n            >\n              <h2 className=\"truncate text-[20px] font-medium leading-[1.2] tracking-[-0.03em] text-stone-800 dark:text-stone-100\">\n                {title}\n              </h2>\n              {subtitle ? (\n                <p className=\"mt-0.5 truncate text-[11.5px] leading-[1.35] text-stone-500 dark:text-stone-400\">\n                  {subtitle}\n                </p>\n              ) : null}\n            </motion.div>\n            <motion.span\n              aria-hidden\n              style={{ opacity: smallOpacity, y: smallY }}\n              className=\"absolute inset-x-0 top-[3px] truncate text-[13px] font-medium leading-[1.4] text-stone-700 dark:text-stone-200\"\n            >\n              {title}\n            </motion.span>\n          </div>\n\n          {actions ? (\n            <div className=\"pointer-events-auto flex h-6 shrink-0 items-center gap-1.5\">\n              {actions}\n            </div>\n          ) : null}\n        </div>\n      </header>\n    </div>\n  );\n}\n"}]}