{
  "name": "navbar-01-default",
  "type": "registry:block",
  "tier": "free",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://www.craftui.space/r/craftui-base.json"
  ],
  "files": [
    {
      "path": "components/craftui/sections/navbar-01/default.tsx",
      "content": "\"use client\";\n\nimport { motion, useReducedMotion, type Transition } from \"motion/react\";\nimport { useState } from \"react\";\n\n/* ==================================================================== *\n * The strip\n *\n * One shape, three states. The strip is always the full cell height,\n * bottom-anchored, and `scaleY` squashes it down: to 0 when idle, to a\n * thin bar on the active route, to 1 when filled. Width never changes, so\n * the fill reads as the underline rising rather than as a box appearing.\n *\n * Every end of the range is a known number, so nothing is measured and\n * the only animated property is a transform — it stays on the compositor.\n * Scale is also why the exit is the entrance played backwards: one spring,\n * interruptible mid-flight. Fading instead would race the retraction and\n * make the strip blink away before it finished going down.\n *\n * Mobile runs the same strip turned on its side: rows instead of cells,\n * rising out of each row's rule instead of the bar's. What it cannot run\n * is hover — touch has none — so there the fill *is* the active state and\n * the thin bar is dropped. Two states, same shape, same spring.\n * ==================================================================== */\n\ntype NavItem = { label: string; href: string };\n\ntype NavbarProps = {\n  items?: NavItem[];\n  className?: string;\n  transition?: Transition;\n  labelDuration?: number;\n  labelDelay?: number;\n\n  accent?: string;\n  labelColorOnFill?: string;\n  barHeight?: number;\n};\n\nconst navItems: NavItem[] = [\n  { label: \"Home\", href: \"#home\" },\n  { label: \"About\", href: \"#about\" },\n  { label: \"Services\", href: \"#services\" },\n  { label: \"Contact\", href: \"#contact\" },\n];\n\n/**\n * The desktop bar's height in pixels. It is `md:h-16` in the markup and the\n * strip's full travel here — `barHeight` is turned into a scale factor\n * against it, which is what keeps the active bar from needing a measurement.\n * Change one and you must change the other.\n */\nconst NAV_HEIGHT = 64;\n\n/** Separator and bottom rule, as an inset shadow — see the note on <li>. */\nconst RULE = \"rgb(255 255 255 / 0.14)\";\n\n/**\n * The panel is the one thing here that animates a layout property, because\n * pushing the page down is the whole point of this pattern — there is no\n * transform that moves everything below it. A tween, not the item spring: a\n * spring's overshoot on height would visibly bounce the page content under\n * it. The ease is a standard decelerating curve, quick out of the gate.\n */\nconst PANEL_TRANSITION: Transition = {\n  duration: 0.28,\n  ease: [0.32, 0.72, 0, 1],\n};\n\n/** Seconds between adjacent rows on open. */\nconst ROW_STAGGER = 0.035;\n\ntype CellProps = {\n  item: NavItem;\n  /** Where the strip ends up: 0 hidden, `barScaleY` a thin bar, 1 a full fill. */\n  fill: number;\n  /** Whether the label sits on the accent and needs the dark ink. */\n  inked: boolean;\n  accent: string;\n  labelColorOnFill: string;\n  labelDuration: number;\n  labelDelay: number;\n  transition: Transition;\n  reduceMotion: boolean;\n  isActive: boolean;\n  onSelect: () => void;\n  onHover: (hovered: boolean) => void;\n  /** Rule position differs by axis: a left edge in the row, a top edge stacked. */\n  ruleShadow?: string;\n  liClassName: string;\n  linkClassName: string;\n  /** Column rows only — the staggered reveal as the panel opens. */\n  reveal?: { open: boolean; index: number };\n};\n\nfunction NavCell({\n  item,\n  fill,\n  inked,\n  accent,\n  labelColorOnFill,\n  labelDuration,\n  labelDelay,\n  transition,\n  reduceMotion,\n  isActive,\n  onSelect,\n  onHover,\n  ruleShadow,\n  liClassName,\n  linkClassName,\n  reveal,\n}: CellProps) {\n  return (\n    <motion.li\n      animate={\n        reveal ? { opacity: reveal.open ? 1 : 0, y: reveal.open ? 0 : -6 } : undefined\n      }\n      className={`relative isolate ${liClassName}`}\n      initial={reveal ? false : undefined}\n      style={{ boxShadow: ruleShadow }}\n      transition={\n        reveal && !reduceMotion\n          ? { ...PANEL_TRANSITION, delay: reveal.open ? reveal.index * ROW_STAGGER : 0 }\n          : { duration: 0 }\n      }\n    >\n      <a\n        aria-current={isActive ? \"page\" : undefined}\n        className={`relative flex items-center text-[11px] font-semibold tracking-[0.18em] text-white uppercase outline-none focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:ring-inset ${linkClassName}`}\n        href={item.href}\n        onBlur={() => onHover(false)}\n        onClick={(event) => {\n          event.preventDefault();\n          onSelect();\n        }}\n        onFocus={() => onHover(true)}\n        // pointerType guards the sticky-hover-after-tap problem on touch,\n        // where there is no matching leave event.\n        onPointerEnter={(event) => {\n          if (event.pointerType === \"mouse\") onHover(true);\n        }}\n      >\n        <motion.span\n          animate={{ scaleY: fill }}\n          aria-hidden=\"true\"\n          // Bottom-anchored: the strip grows upward out of the rule below it\n          // and never moves sideways.\n          className=\"absolute inset-x-0 bottom-0 -z-10 h-full origin-bottom\"\n          style={{ background: accent }}\n          transition={reduceMotion ? { duration: 0 } : transition}\n        />\n        <span\n          style={{\n            color: inked ? labelColorOnFill : undefined,\n            // The ink lands just after the accent arrives, not with it. No\n            // delay on the way out, so it clears before the strip retracts.\n            transition: reduceMotion\n              ? \"none\"\n              : `color ${labelDuration}s ease-out ${inked ? labelDelay : 0}s`,\n          }}\n        >\n          {item.label}\n        </span>\n      </a>\n    </motion.li>\n  );\n}\n\nexport default function Navbar01({\n  items = navItems,\n  className,\n  transition = { type: \"spring\", stiffness: 440, damping: 36, mass: 0.4 },\n  labelDuration = 0.07,\n  labelDelay = 0.04,\n\n  accent = \"oklch(0.935 0.198 111.5)\",\n  labelColorOnFill = \"oklch(0.145 0 0)\",\n  barHeight = 3,\n}: NavbarProps) {\n  const [activeHref, setActiveHref] = useState(items[0]?.href);\n  const [hoveredHref, setHoveredHref] = useState<string | null>(null);\n  const [isOpen, setIsOpen] = useState(false);\n  const reduceMotion = useReducedMotion() ?? false;\n\n  const barScaleY = Math.min(barHeight / NAV_HEIGHT, 1);\n\n  const shared = {\n    accent,\n    labelColorOnFill,\n    labelDuration,\n    labelDelay,\n    transition,\n    reduceMotion,\n  };\n\n  return (\n    <nav\n      // `md:pr-6` mirrors the brand's left gutter, and it is also what makes\n      // the last item's right rule visible: flush against the container the\n      // 1px shadow sits on the clipping edge, where a fractional width or an\n      // ancestor's `overflow-hidden` swallows it.\n      className={`w-full shadow-[inset_0_-1px_0_0_rgb(255_255_255/0.14)] md:flex md:h-16 md:items-center md:justify-between md:pr-6 ${className ?? \"\"}`}\n      // The whole bar clears the hover so leaving sideways doesn't strand a fill.\n      onPointerLeave={() => setHoveredHref(null)}\n    >\n      <div className=\"flex h-16 items-center justify-between pr-4 pl-6 md:h-full md:pr-0\">\n        <a\n          className=\"flex items-center gap-2.5 text-[18px] font-semibold tracking-tight text-white\"\n          href={items[0]?.href ?? \"#\"}\n        >\n          {/* Dropped below md: with the mark gone the wordmark starts at the\n              row padding itself, which is the same 24px the stacked labels\n              use — one text edge down the open menu, nothing hanging in the\n              gutter to line it up against. */}\n          <svg\n            aria-hidden=\"true\"\n            className=\"hidden size-5 shrink-0 md:block\"\n            fill=\"currentColor\"\n            viewBox=\"0 0 24 24\"\n          >\n            <circle cx=\"12\" cy=\"6\" r=\"3.4\" />\n            <circle cx=\"6\" cy=\"15.5\" r=\"3.4\" />\n            <circle cx=\"18\" cy=\"15.5\" r=\"3.4\" />\n          </svg>\n          <span>MP2 Collective</span>\n        </a>\n\n        <button\n          aria-controls=\"navbar-01-panel\"\n          aria-expanded={isOpen}\n          aria-label={isOpen ? \"Close menu\" : \"Open menu\"}\n          className=\"-mr-2 flex size-10 items-center justify-center outline-none focus-visible:ring-2 focus-visible:ring-white/70 md:hidden\"\n          onClick={() => setIsOpen((open) => !open)}\n          type=\"button\"\n        >\n          {/* Two bars that cross into an ✕. Both rotate about the same centre,\n              so the close state is the open state folded rather than a second\n              icon swapped in. */}\n          <span aria-hidden=\"true\" className=\"relative block h-4 w-5\">\n            {[-4, 4].map((offset, index) => (\n              <motion.span\n                animate={{\n                  rotate: isOpen ? (index === 0 ? 45 : -45) : 0,\n                  y: isOpen ? 0 : offset,\n                }}\n                className=\"absolute top-1/2 left-0 block h-0.5 w-full -translate-y-1/2 rounded-full bg-white\"\n                key={offset}\n                transition={reduceMotion ? { duration: 0 } : transition}\n              />\n            ))}\n          </span>\n        </button>\n      </div>\n\n      {/* Desktop: the original row, cells side by side. */}\n      <ul className=\"hidden h-full md:flex md:items-stretch\">\n        {items.map((item, index) => {\n          const isActive = activeHref === item.href;\n          const isFilled = hoveredHref === item.href;\n\n          return (\n            <NavCell\n              {...shared}\n              fill={isFilled ? 1 : isActive ? barScaleY : 0}\n              inked={isFilled}\n              isActive={isActive}\n              item={item}\n              key={item.href}\n              liClassName=\"h-full\"\n              linkClassName=\"h-full px-7\"\n              onHover={(hovered) => setHoveredHref(hovered ? item.href : null)}\n              onSelect={() => setActiveHref(item.href)}\n              // Separators are `inset` box-shadows, not borders: an inset\n              // shadow paints inside the box instead of adding a pixel to it,\n              // so item widths stay exactly what the padding says — which is\n              // what the strip is sized from. A left edge on every item gives\n              // the lines *between* items and closes the run at its start;\n              // the last item adds a right edge so it doesn't trail off.\n              ruleShadow={[\n                `inset 1px 0 0 0 ${RULE}`,\n                index === items.length - 1 && `inset -1px 0 0 0 ${RULE}`,\n              ]\n                .filter(Boolean)\n                .join(\", \")}\n            />\n          );\n        })}\n      </ul>\n\n      {/* Mobile: the same cells stacked, unrolled inside the bar so the page\n          is pushed down rather than covered. `height: auto` is measured by\n          Motion, so the panel never needs a hardcoded open height. */}\n      <motion.div\n        animate={{ height: isOpen ? \"auto\" : 0 }}\n        className=\"overflow-hidden md:hidden\"\n        id=\"navbar-01-panel\"\n        initial={false}\n        transition={reduceMotion ? { duration: 0 } : PANEL_TRANSITION}\n      >\n        <ul className=\"flex flex-col\">\n          {items.map((item, index) => {\n            const isActive = activeHref === item.href;\n\n            return (\n              <NavCell\n                {...shared}\n                // No hover on touch, so the fill is the active state outright\n                // and the thin bar has nothing left to mark.\n                fill={isActive ? 1 : 0}\n                inked={isActive}\n                isActive={isActive}\n                item={item}\n                key={item.href}\n                liClassName=\"\"\n                linkClassName=\"h-14 px-6\"\n                onHover={() => {}}\n                onSelect={() => {\n                  setActiveHref(item.href);\n                  setIsOpen(false);\n                }}\n                reveal={{ index, open: isOpen }}\n                // Stacked, the rule moves to the top edge of every row —\n                // including the first, which is what separates the list from\n                // the brand row above it. The first row also carries a left\n                // edge and the last a right edge, so the column is bracketed\n                // rather than left running open at both ends.\n                ruleShadow={[\n                  `inset 0 1px 0 0 ${RULE}`,\n                  index === 0 && `inset 1px 0 0 0 ${RULE}`,\n                  index === items.length - 1 && `inset -1px 0 0 0 ${RULE}`,\n                ]\n                  .filter(Boolean)\n                  .join(\", \")}\n              />\n            );\n          })}\n        </ul>\n      </motion.div>\n    </nav>\n  );\n}\n",
      "type": "registry:component",
      "target": "~/components/craftui/sections/navbar-01/default.tsx"
    }
  ],
  "meta": {
    "displayName": "Navbar 01 - Navbar 01",
    "description": "One strip, three states: hidden when idle, a thin bar on the active route, a full fill on hover. Width never changes, so the fill reads as the underline rising rather than a box appearing. Below md the items unroll inside the bar as rows and push the page down.",
    "category": "navbar",
    "tags": [
      "navbar",
      "nav",
      "section",
      "header",
      "hover",
      "underline",
      "spring",
      "menu",
      "mobile",
      "responsive"
    ],
    "animationType": "hover"
  }
}