{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"in-page-navbar","type":"registry:block","title":"In-page navbar","description":"A smart, fully responsive, and accessible navigation bar that tracks section progress as you scroll, providing a clear visual indicator of your journey through the page. Best suited for single-page layouts where all content lives on a single page.","dependencies":["motion"],"files":[{"path":"registry/100xui/blocks/in-page-navbar/components/in-page-navbar.tsx","content":"\"use client\";\nimport React from \"react\";\nimport {\n  AnimatePresence,\n  motion,\n  useScroll,\n  useTransform,\n  type Variants,\n  type MotionValue,\n} from \"motion/react\";\nimport { EqualIcon, XIcon } from \"lucide-react\";\nimport { useIsServer } from \"@/hooks/use-is-server\";\nimport { cn } from \"@/lib/utils\";\n\nconst fadeVariants: Variants = {\n  fadeIn: { opacity: 1 },\n  fadeOut: { opacity: 0 },\n};\n\ninterface NavSection extends Omit<React.ComponentProps<\"a\">, \"href\"> {\n  label: string;\n  id: string;\n}\n\nexport interface InPageNavbarProps extends React.ComponentProps<\"div\"> {\n  logo: React.ReactElement;\n  sections: NavSection[];\n}\n\nexport function InPageNavbar({\n  logo,\n  sections,\n  className,\n  ...rest\n}: InPageNavbarProps) {\n  const [isSidebarOpen, setIsSidebarOpen] = React.useState(false);\n  const [isSmallScreen, setIsSmallScreen] = React.useState(false);\n  const isServer = useIsServer();\n\n  const navButtons = (\n    <NavButtonGroup\n      sections={sections}\n      isServer={isServer}\n      className=\"gap-x-1.5 gap-y-1 max-sm:mx-auto max-sm:grid max-sm:max-w-fit max-sm:grid-rows-3 max-sm:py-8 max-sm:text-sm sm:flex sm:items-center\"\n    />\n  );\n\n  React.useEffect(() => {\n    const handleMediaQuery = ({ matches }: { matches: boolean }) =>\n      setIsSmallScreen(matches);\n    const mediaQuery = window.matchMedia(\"(max-width:639px)\");\n    mediaQuery.addEventListener(\"change\", handleMediaQuery);\n    return () => mediaQuery.removeEventListener(\"change\", handleMediaQuery);\n  }, []);\n\n  return (\n    <>\n      <div\n        className={cn(\n          \"pointer-events-none fixed inset-x-4.5 top-1.5 z-[100]\",\n          \"[&_a,button]:focus-visible:ring-offset-background [&_a,button]:focus-visible:ring-ring [&_a,button]:transition-opacity [&_a,button]:duration-150 [&_a,button]:ease-out [&_a,button]:hover:opacity-70 [&_a,button]:focus-visible:opacity-70 [&_a,button]:focus-visible:ring-1 [&_a,button]:focus-visible:ring-offset-1 [&_a,button]:focus-visible:outline-0\",\n          className,\n        )}\n        {...rest}\n      >\n        <motion.div\n          initial={false}\n          animate={isSidebarOpen && isSmallScreen ? \"fadeOut\" : \"fadeIn\"}\n          variants={fadeVariants}\n          className=\"bg-card/85 border-border/50 pointer-events-auto mx-auto flex max-w-xl items-center justify-between rounded-lg border p-3 text-sm font-medium shadow-md backdrop-blur-[2px] sm:rounded-xl\"\n        >\n          <a\n            href=\"#\"\n            tabIndex={1}\n            onClick={(e) => {\n              e.preventDefault();\n              window.history.pushState(null, \"\", window.location.pathname);\n              window.scrollTo({ top: 0, behavior: \"smooth\" });\n            }}\n            className=\"rounded-full p-0.5\"\n          >\n            {logo}\n          </a>\n          <>\n            <nav className=\"hidden sm:block\">{navButtons}</nav>\n            <button\n              tabIndex={2}\n              aria-label=\"Open sidebar\"\n              onClick={() => {\n                setIsSmallScreen(true);\n                setIsSidebarOpen(true);\n              }}\n              className=\"text-foreground -mr-2 cursor-pointer rounded-full px-1 sm:hidden\"\n            >\n              <EqualIcon />\n            </button>\n          </>\n        </motion.div>\n      </div>\n      <AnimatePresence>\n        {isSmallScreen && isSidebarOpen && (\n          <motion.div\n            initial=\"fadeOut\"\n            animate=\"fadeIn\"\n            exit=\"fadeOut\"\n            variants={fadeVariants}\n            className=\"bg-card/85 border-border/50 [&_a,button]:focus-visible:ring-offset-background [&_a,button]:focus-visible:ring-ring fixed inset-x-4.5 top-1.5 z-[110] overflow-hidden rounded-lg border shadow-md backdrop-blur-[2px] [&_a,button]:transition-opacity [&_a,button]:duration-150 [&_a,button]:ease-out [&_a,button]:hover:opacity-70 [&_a,button]:focus-visible:opacity-70 [&_a,button]:focus-visible:ring-1 [&_a,button]:focus-visible:ring-offset-1 [&_a,button]:focus-visible:outline-0\"\n          >\n            <button\n              tabIndex={3}\n              aria-label=\"Close sidebar\"\n              onClick={() => setIsSidebarOpen(false)}\n              className=\"text-foreground absolute top-3 right-2 cursor-pointer rounded-full\"\n            >\n              <XIcon />\n            </button>\n            {navButtons}\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </>\n  );\n}\n\ninterface NavButtonGroupProps {\n  sections: NavSection[];\n  isServer: boolean;\n  className: string;\n}\n\nfunction NavButtonGroup({\n  sections,\n  isServer,\n  className,\n}: NavButtonGroupProps) {\n  const { scrollY } = useScroll();\n\n  return (\n    <nav className={className}>\n      {sections.map((props) => (\n        <NavItem\n          key={props.id}\n          scrollY={scrollY}\n          isServer={isServer}\n          {...props}\n        />\n      ))}\n    </nav>\n  );\n}\n\ninterface NavItemProps extends NavSection {\n  scrollY: MotionValue<number>;\n  isServer: boolean;\n}\n\nfunction NavItem({\n  label,\n  id,\n  isServer,\n  scrollY,\n  onClick,\n  className,\n  ...rest\n}: NavItemProps) {\n  const [targetElement, setTargetElement] = React.useState<HTMLElement | null>(\n    null,\n  );\n\n  const sectionProgress = useTransform(scrollY, (latest) => {\n    if (isServer || !targetElement) return 0;\n    const viewportHeight = window.innerHeight;\n    const targetElementTop = targetElement.offsetTop;\n    const targetElementHeight = targetElement.offsetHeight;\n    const visibleRatio =\n      (latest + viewportHeight - targetElementTop) / targetElementHeight;\n    return Math.min(1, Math.max(visibleRatio, 0));\n  });\n\n  const clipPath = useTransform(\n    sectionProgress,\n    [0, 1],\n    [\"inset(0% 100% 0% 0%)\", \"inset(0% 0% 0% 0%)\"],\n  );\n\n  React.useEffect(() => {\n    const element = document.getElementById(id);\n    if (!element) {\n      throw new Error(`No section found with id=\"${id}\".`);\n    }\n    setTargetElement(element);\n  }, [id]);\n\n  return (\n    <a\n      tabIndex={2}\n      {...rest}\n      href={`#${id}`}\n      onClick={(e) => {\n        e.preventDefault();\n        window.history.pushState(null, \"\", `#${id}`);\n        targetElement?.scrollIntoView({ behavior: \"smooth\" });\n        onClick?.(e);\n      }}\n      className={cn(\n        \"bg-background text-foreground border-border relative overflow-hidden rounded-full border font-medium capitalize transition-opacity duration-150 ease-out sm:text-xs\",\n        className,\n      )}\n    >\n      <span className=\"relative z-20 inline-block size-full px-4 py-2.5 text-center leading-none sm:px-3 sm:py-2\">\n        {label}\n      </span>\n      {targetElement && (\n        <motion.span\n          initial=\"fadeOut\"\n          animate=\"fadeIn\"\n          variants={fadeVariants}\n          style={{ clipPath }}\n          className=\"bg-muted text-muted-foreground absolute -inset-0.5 z-30 grid place-items-center rounded-[inherit] leading-none\"\n        >\n          {label}\n        </motion.span>\n      )}\n    </a>\n  );\n}\n","type":"registry:component"},{"path":"registry/100xui/shared/hooks/use-is-server.ts","content":"import React from \"react\";\n\nexport function useIsServer() {\n  const isServer = React.useRef(typeof window === \"undefined\");\n  return isServer.current;\n}\n","type":"registry:hook"}]}