{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"animated-sidebar","type":"registry:component","title":"Animated Sidebar","description":"A composable application sidebar with morphing nested navigation that folds into an animated icon rail on desktop and becomes a focus-managed sheet on mobile.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/animated-sidebar.tsx","type":"registry:component","target":"@components/motion/animated-sidebar.tsx","content":"\"use client\";\n// beui.dev/components/motion/animated-sidebar\n\nimport { ChevronRight } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  type ButtonHTMLAttributes,\n  type CSSProperties,\n  createContext,\n  forwardRef,\n  type HTMLAttributes,\n  type ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { SharedLayoutBg } from \"@/components/motion/shared-layout-bg\";\nimport {\n  EASE_DRAWER,\n  EASE_OUT,\n  SPRING_LAYOUT,\n  SPRING_PRESS,\n} from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\ntype SidebarState = \"expanded\" | \"collapsed\";\ntype SidebarSide = \"left\" | \"right\";\ntype SidebarVariant = \"sidebar\" | \"floating\" | \"inset\";\ntype SidebarCollapsible = \"offcanvas\" | \"icon\" | \"none\";\n\nconst MOBILE_QUERY = \"(max-width: 767px)\";\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\";\n\nconst PANEL_TRANSITION = {\n  duration: 0.36,\n  ease: EASE_DRAWER,\n} as const;\n\n// The desktop rail is one surface changing shape, so a lightly underdamped\n// spring makes the width settle without scaling or stretching its contents.\nconst SIDEBAR_MORPH_TRANSITION = {\n  type: \"spring\",\n  stiffness: 380,\n  damping: 28,\n  mass: 0.75,\n} as const;\n\nconst LABEL_ENTER_TRANSITION = {\n  duration: 0.2,\n  delay: 0.08,\n  ease: EASE_OUT,\n} as const;\n\nconst LABEL_EXIT_TRANSITION = {\n  duration: 0.12,\n  ease: EASE_OUT,\n} as const;\n\nconst SUBMENU_TRANSITION = {\n  duration: 0.18,\n  ease: EASE_OUT,\n} as const;\n\nconst SUBMENU_VARIANTS: Variants = {\n  closed: {\n    opacity: 0,\n    clipPath: \"inset(0 0 100% 0 round 8px)\",\n    transition: {\n      duration: 0.14,\n      ease: EASE_OUT,\n      staggerChildren: 0.025,\n      staggerDirection: -1,\n    },\n  },\n  open: {\n    opacity: 1,\n    clipPath: \"inset(0 0 0% 0 round 8px)\",\n    transition: {\n      duration: 0.2,\n      delayChildren: 0.035,\n      ease: EASE_OUT,\n      staggerChildren: 0.045,\n    },\n  },\n};\n\nconst SUBMENU_ITEM_VARIANTS: Variants = {\n  closed: {\n    opacity: 0,\n    y: -6,\n    filter: \"blur(3px)\",\n  },\n  open: {\n    opacity: 1,\n    y: 0,\n    filter: \"blur(0px)\",\n    transition: SUBMENU_TRANSITION,\n  },\n};\n\nconst REDUCED_TRANSITION = {\n  duration: 0.16,\n  ease: EASE_OUT,\n} as const;\n\nconst FOCUSABLE_SELECTOR = [\n  \"a[href]\",\n  \"button:not([disabled])\",\n  \"input:not([disabled])\",\n  \"select:not([disabled])\",\n  \"textarea:not([disabled])\",\n  \"[tabindex]:not([tabindex='-1'])\",\n].join(\",\");\n\nfunction subscribeToMobileQuery(callback: () => void) {\n  const query = window.matchMedia(MOBILE_QUERY);\n  query.addEventListener(\"change\", callback);\n  return () => query.removeEventListener(\"change\", callback);\n}\n\nfunction getMobileSnapshot() {\n  return window.matchMedia(MOBILE_QUERY).matches;\n}\n\nfunction getServerMobileSnapshot() {\n  return false;\n}\n\nfunction useIsMobile() {\n  return useSyncExternalStore(\n    subscribeToMobileQuery,\n    getMobileSnapshot,\n    getServerMobileSnapshot,\n  );\n}\n\ninterface AnimatedSidebarContextValue {\n  isMobile: boolean;\n  layoutId: string;\n  open: boolean;\n  openMobile: boolean;\n  reduce: boolean;\n  setOpen: (open: boolean) => void;\n  setOpenMobile: (open: boolean) => void;\n  state: SidebarState;\n  toggleSidebar: () => void;\n  triggerRef: React.RefObject<HTMLButtonElement | null>;\n}\n\nconst AnimatedSidebarContext =\n  createContext<AnimatedSidebarContextValue | null>(null);\n\ninterface AnimatedSidebarPanelContextValue {\n  collapsed: boolean;\n  collapsible: SidebarCollapsible;\n  side: SidebarSide;\n}\n\nconst AnimatedSidebarPanelContext =\n  createContext<AnimatedSidebarPanelContextValue | null>(null);\n\nexport function useAnimatedSidebar() {\n  const context = useContext(AnimatedSidebarContext);\n  if (!context) {\n    throw new Error(\n      \"useAnimatedSidebar must be used inside AnimatedSidebarProvider.\",\n    );\n  }\n  return context;\n}\n\nfunction useAnimatedSidebarPanel() {\n  const context = useContext(AnimatedSidebarPanelContext);\n  if (!context) {\n    throw new Error(\n      \"Animated Sidebar parts must be used inside AnimatedSidebar.\",\n    );\n  }\n  return context;\n}\n\ntype SidebarProviderStyle = CSSProperties & {\n  \"--sidebar-width\"?: string;\n  \"--sidebar-width-icon\"?: string;\n  \"--sidebar-width-mobile\"?: string;\n};\n\nexport interface AnimatedSidebarProviderProps\n  extends HTMLAttributes<HTMLDivElement> {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  openMobile?: boolean;\n  defaultOpenMobile?: boolean;\n  onOpenMobileChange?: (open: boolean) => void;\n  style?: SidebarProviderStyle;\n}\n\nexport function AnimatedSidebarProvider({\n  children,\n  open,\n  defaultOpen = true,\n  onOpenChange,\n  openMobile,\n  defaultOpenMobile = false,\n  onOpenMobileChange,\n  className,\n  style,\n  ...props\n}: AnimatedSidebarProviderProps) {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen);\n  const [internalOpenMobile, setInternalOpenMobile] =\n    useState(defaultOpenMobile);\n  const isMobile = useIsMobile();\n  const reduce = useReducedMotion() ?? false;\n  const generatedId = useId();\n  const triggerRef = useRef<HTMLButtonElement>(null);\n  const desktopOpen = open ?? internalOpen;\n  const mobileOpen = openMobile ?? internalOpenMobile;\n\n  const setOpen = useCallback(\n    (nextOpen: boolean) => {\n      if (open === undefined) setInternalOpen(nextOpen);\n      onOpenChange?.(nextOpen);\n    },\n    [onOpenChange, open],\n  );\n\n  const setOpenMobile = useCallback(\n    (nextOpen: boolean) => {\n      if (openMobile === undefined) setInternalOpenMobile(nextOpen);\n      onOpenMobileChange?.(nextOpen);\n    },\n    [onOpenMobileChange, openMobile],\n  );\n\n  const toggleSidebar = useCallback(() => {\n    if (isMobile) setOpenMobile(!mobileOpen);\n    else setOpen(!desktopOpen);\n  }, [desktopOpen, isMobile, mobileOpen, setOpen, setOpenMobile]);\n\n  useEffect(() => {\n    const handleShortcut = (event: KeyboardEvent) => {\n      if (\n        event.key.toLowerCase() === SIDEBAR_KEYBOARD_SHORTCUT &&\n        (event.metaKey || event.ctrlKey)\n      ) {\n        event.preventDefault();\n        toggleSidebar();\n      }\n    };\n\n    window.addEventListener(\"keydown\", handleShortcut);\n    return () => window.removeEventListener(\"keydown\", handleShortcut);\n  }, [toggleSidebar]);\n\n  return (\n    <AnimatedSidebarContext.Provider\n      value={{\n        isMobile,\n        layoutId: `${generatedId}-active`,\n        open: desktopOpen,\n        openMobile: mobileOpen,\n        reduce,\n        setOpen,\n        setOpenMobile,\n        state: desktopOpen ? \"expanded\" : \"collapsed\",\n        toggleSidebar,\n        triggerRef,\n      }}\n    >\n      <div\n        {...props}\n        data-slot=\"sidebar-wrapper\"\n        data-state={desktopOpen ? \"expanded\" : \"collapsed\"}\n        style={{\n          \"--sidebar-width\": \"16rem\",\n          \"--sidebar-width-icon\": \"4.25rem\",\n          \"--sidebar-width-mobile\": \"18rem\",\n          ...style,\n        }}\n        className={cn(\n          \"group/sidebar-wrapper flex min-h-svh w-full min-w-0\",\n          className,\n        )}\n      >\n        {children}\n      </div>\n    </AnimatedSidebarContext.Provider>\n  );\n}\n\nfunction MobileSidebar({\n  ariaLabel,\n  children,\n  className,\n  side,\n}: {\n  ariaLabel: string;\n  children: ReactNode;\n  className?: string;\n  side: SidebarSide;\n}) {\n  const context = useAnimatedSidebar();\n  const panelRef = useRef<HTMLDivElement>(null);\n  const [mounted, setMounted] = useState(false);\n\n  useEffect(() => setMounted(true), []);\n\n  useEffect(() => {\n    if (!context.openMobile) return;\n\n    const body = document.body;\n    const scrollY = window.scrollY;\n    const previousBodyStyles = {\n      left: body.style.left,\n      overflow: body.style.overflow,\n      position: body.style.position,\n      right: body.style.right,\n      top: body.style.top,\n    };\n\n    body.style.position = \"fixed\";\n    body.style.top = `-${scrollY}px`;\n    body.style.left = \"0\";\n    body.style.right = \"0\";\n    body.style.overflow = \"hidden\";\n\n    const focusFrame = requestAnimationFrame(() => {\n      const firstFocusable =\n        panelRef.current?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR);\n      (firstFocusable ?? panelRef.current)?.focus({ preventScroll: true });\n    });\n\n    return () => {\n      cancelAnimationFrame(focusFrame);\n      body.style.position = previousBodyStyles.position;\n      body.style.top = previousBodyStyles.top;\n      body.style.left = previousBodyStyles.left;\n      body.style.right = previousBodyStyles.right;\n      body.style.overflow = previousBodyStyles.overflow;\n      window.scrollTo(0, scrollY);\n      context.triggerRef.current?.focus({ preventScroll: true });\n    };\n  }, [context.openMobile, context.triggerRef]);\n\n  if (!mounted) return null;\n\n  return createPortal(\n    <div\n      className={cn(\n        \"pointer-events-none fixed inset-0 z-50 md:hidden\",\n        context.openMobile ? \"visible\" : \"invisible\",\n      )}\n    >\n      <motion.button\n        type=\"button\"\n        aria-label=\"Close sidebar\"\n        tabIndex={context.openMobile ? 0 : -1}\n        initial={false}\n        animate={{ opacity: context.openMobile ? 1 : 0 }}\n        transition={\n          context.reduce ? REDUCED_TRANSITION : PANEL_TRANSITION\n        }\n        onClick={() => context.setOpenMobile(false)}\n        className={cn(\n          \"absolute inset-0 bg-black/40\",\n          context.openMobile\n            ? \"pointer-events-auto\"\n            : \"pointer-events-none\",\n        )}\n      />\n\n      <motion.div\n        ref={panelRef}\n        role=\"dialog\"\n        aria-modal=\"true\"\n        aria-label={ariaLabel}\n        aria-hidden={!context.openMobile}\n        inert={!context.openMobile}\n        tabIndex={-1}\n        data-mobile=\"true\"\n        data-state={context.openMobile ? \"expanded\" : \"collapsed\"}\n        data-side={side}\n        initial={false}\n        animate={{\n          opacity: context.reduce\n            ? context.openMobile\n              ? 1\n              : 0\n            : 1,\n          x: context.reduce\n            ? 0\n            : context.openMobile\n              ? \"0%\"\n              : side === \"left\"\n                ? \"-100%\"\n                : \"100%\",\n        }}\n        transition={\n          context.reduce ? REDUCED_TRANSITION : PANEL_TRANSITION\n        }\n        onKeyDown={(event) => {\n          if (event.key === \"Escape\") {\n            event.preventDefault();\n            context.setOpenMobile(false);\n            return;\n          }\n\n          if (event.key !== \"Tab\") return;\n          const focusable = panelRef.current\n            ? Array.from(\n                panelRef.current.querySelectorAll<HTMLElement>(\n                  FOCUSABLE_SELECTOR,\n                ),\n              )\n            : [];\n\n          if (focusable.length === 0) {\n            event.preventDefault();\n            panelRef.current?.focus();\n            return;\n          }\n\n          const first = focusable[0];\n          const last = focusable[focusable.length - 1];\n          if (event.shiftKey && document.activeElement === first) {\n            event.preventDefault();\n            last.focus();\n          } else if (!event.shiftKey && document.activeElement === last) {\n            event.preventDefault();\n            first.focus();\n          }\n        }}\n        className={cn(\n          \"pointer-events-auto absolute inset-y-0 flex h-dvh w-(--sidebar-width-mobile) max-w-[88vw] flex-col overflow-hidden\",\n          \"border-border bg-background shadow-2xl will-change-transform\",\n          side === \"left\" ? \"left-0 border-r\" : \"right-0 border-l\",\n          !context.openMobile && \"pointer-events-none\",\n          className,\n        )}\n      >\n        <AnimatedSidebarPanelContext.Provider\n          value={{ collapsed: false, collapsible: \"none\", side }}\n        >\n          {children}\n        </AnimatedSidebarPanelContext.Provider>\n      </motion.div>\n    </div>,\n    document.body,\n  );\n}\n\nexport interface AnimatedSidebarProps\n  extends Omit<HTMLMotionProps<\"aside\">, \"children\"> {\n  children?: ReactNode;\n  side?: SidebarSide;\n  variant?: SidebarVariant;\n  collapsible?: SidebarCollapsible;\n  ariaLabel?: string;\n  panelClassName?: string;\n}\n\nexport const AnimatedSidebar = forwardRef<HTMLElement, AnimatedSidebarProps>(\n  function AnimatedSidebar(\n    {\n      side = \"left\",\n      variant = \"sidebar\",\n      collapsible = \"icon\",\n      ariaLabel = \"Sidebar\",\n      children,\n      className,\n      panelClassName,\n      style,\n      ...props\n    },\n    forwardedRef,\n  ) {\n    const context = useAnimatedSidebar();\n    const collapsed = collapsible !== \"none\" && !context.open;\n    const offcanvas = collapsed && collapsible === \"offcanvas\";\n    const width = offcanvas\n      ? \"0px\"\n      : collapsed\n        ? \"var(--sidebar-width-icon)\"\n        : \"var(--sidebar-width)\";\n\n    if (context.isMobile) {\n      return (\n        <MobileSidebar\n          ariaLabel={ariaLabel}\n          className={className}\n          side={side}\n        >\n          {children}\n        </MobileSidebar>\n      );\n    }\n\n    return (\n      <motion.aside\n        {...props}\n        ref={forwardedRef}\n        initial={false}\n        aria-label={ariaLabel}\n        data-slot=\"sidebar\"\n        data-state={collapsed ? \"collapsed\" : \"expanded\"}\n        data-collapsible={collapsible}\n        data-variant={variant}\n        data-side={side}\n        animate={{ width }}\n        transition={\n          context.reduce ? { duration: 0 } : SIDEBAR_MORPH_TRANSITION\n        }\n        style={style}\n        className={cn(\n          \"group/sidebar relative hidden h-auto shrink-0 md:block will-change-[width]\",\n          \"peer\",\n          side === \"right\" && \"order-last\",\n          className,\n        )}\n      >\n        <motion.div\n          initial={false}\n          animate={{\n            opacity: offcanvas ? 0 : 1,\n            x: offcanvas ? (side === \"left\" ? \"-100%\" : \"100%\") : \"0%\",\n          }}\n          transition={\n            context.reduce ? REDUCED_TRANSITION : PANEL_TRANSITION\n          }\n          className={cn(\n            \"sticky top-0 flex h-svh w-full flex-col overflow-hidden bg-background\",\n            variant === \"sidebar\" &&\n              (side === \"left\" ? \"border-border border-r\" : \"border-border border-l\"),\n            variant === \"floating\" &&\n              \"m-2 h-[calc(100svh-1rem)] rounded-2xl border border-border shadow-sm\",\n            variant === \"inset\" && \"m-2 h-[calc(100svh-1rem)] rounded-2xl\",\n            panelClassName,\n          )}\n        >\n          <AnimatedSidebarPanelContext.Provider\n            value={{ collapsed, collapsible, side }}\n          >\n            {children}\n          </AnimatedSidebarPanelContext.Provider>\n        </motion.div>\n      </motion.aside>\n    );\n  },\n);\n\nexport interface AnimatedSidebarTriggerProps\n  extends ButtonHTMLAttributes<HTMLButtonElement> {}\n\nexport const AnimatedSidebarTrigger = forwardRef<\n  HTMLButtonElement,\n  AnimatedSidebarTriggerProps\n>(function AnimatedSidebarTrigger(\n  { className, onClick, type = \"button\", ...props },\n  forwardedRef,\n) {\n  const context = useAnimatedSidebar();\n  const expanded = context.isMobile ? context.openMobile : context.open;\n\n  return (\n    <button\n      {...props}\n      ref={(node) => {\n        context.triggerRef.current = node;\n        if (typeof forwardedRef === \"function\") forwardedRef(node);\n        else if (forwardedRef) forwardedRef.current = node;\n      }}\n      type={type}\n      aria-label={props[\"aria-label\"] ?? \"Toggle sidebar\"}\n      aria-expanded={expanded}\n      data-slot=\"sidebar-trigger\"\n      data-state={expanded ? \"expanded\" : \"collapsed\"}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) context.toggleSidebar();\n      }}\n      className={cn(\n        \"inline-flex size-10 shrink-0 items-center justify-center rounded-xl outline-none\",\n        \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n        className,\n      )}\n    />\n  );\n});\n\nexport interface AnimatedSidebarCloseProps\n  extends ButtonHTMLAttributes<HTMLButtonElement> {}\n\nexport const AnimatedSidebarClose = forwardRef<\n  HTMLButtonElement,\n  AnimatedSidebarCloseProps\n>(function AnimatedSidebarClose(\n  { className, onClick, type = \"button\", ...props },\n  forwardedRef,\n) {\n  const context = useAnimatedSidebar();\n\n  return (\n    <button\n      {...props}\n      ref={forwardedRef}\n      type={type}\n      aria-label={props[\"aria-label\"] ?? \"Close sidebar\"}\n      onClick={(event) => {\n        onClick?.(event);\n        if (event.defaultPrevented) return;\n        if (context.isMobile) context.setOpenMobile(false);\n        else context.setOpen(false);\n      }}\n      className={cn(\n        \"inline-flex size-10 shrink-0 items-center justify-center rounded-xl outline-none\",\n        \"focus-visible:ring-2 focus-visible:ring-ring\",\n        className,\n      )}\n    />\n  );\n});\n\nexport interface AnimatedSidebarRailProps\n  extends ButtonHTMLAttributes<HTMLButtonElement> {}\n\nexport const AnimatedSidebarRail = forwardRef<\n  HTMLButtonElement,\n  AnimatedSidebarRailProps\n>(function AnimatedSidebarRail(\n  { className, onClick, type = \"button\", ...props },\n  forwardedRef,\n) {\n  const context = useAnimatedSidebar();\n  const panel = useAnimatedSidebarPanel();\n\n  return (\n    <button\n      {...props}\n      ref={forwardedRef}\n      type={type}\n      data-side={panel.side}\n      aria-label={props[\"aria-label\"] ?? \"Toggle sidebar\"}\n      title=\"Toggle sidebar\"\n      tabIndex={-1}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) context.toggleSidebar();\n      }}\n      className={cn(\n        \"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 outline-none md:block\",\n        \"after:absolute after:inset-y-0 after:left-1/2 after:w-px after:bg-transparent after:transition-colors hover:after:bg-border\",\n        \"data-[side=right]:right-0 data-[side=right]:translate-x-1/2 data-[side=left]:left-full\",\n        className,\n      )}\n    />\n  );\n});\n\nexport interface AnimatedSidebarInsetProps\n  extends HTMLMotionProps<\"main\"> {}\n\nexport const AnimatedSidebarInset = forwardRef<\n  HTMLElement,\n  AnimatedSidebarInsetProps\n>(function AnimatedSidebarInset({ className, ...props }, forwardedRef) {\n  return (\n    <motion.main\n      {...props}\n      ref={forwardedRef}\n      data-slot=\"sidebar-inset\"\n      className={cn(\n        \"relative flex min-h-svh min-w-0 flex-1 flex-col bg-background\",\n        \"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-2xl md:peer-data-[variant=inset]:shadow-sm\",\n        className,\n      )}\n    />\n  );\n});\n\nexport const AnimatedSidebarHeader = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function AnimatedSidebarHeader({ className, ...props }, forwardedRef) {\n  return (\n    <div\n      {...props}\n      ref={forwardedRef}\n      data-slot=\"sidebar-header\"\n      className={cn(\"flex shrink-0 flex-col gap-2 p-3\", className)}\n    />\n  );\n});\n\nexport const AnimatedSidebarContent = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function AnimatedSidebarContent({ className, ...props }, forwardedRef) {\n  return (\n    <div\n      {...props}\n      ref={forwardedRef}\n      data-slot=\"sidebar-content\"\n      className={cn(\n        \"flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto overflow-x-hidden overscroll-contain px-2 py-2\",\n        className,\n      )}\n    />\n  );\n});\n\nexport const AnimatedSidebarFooter = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function AnimatedSidebarFooter({ className, ...props }, forwardedRef) {\n  return (\n    <div\n      {...props}\n      ref={forwardedRef}\n      data-slot=\"sidebar-footer\"\n      className={cn(\n        \"flex shrink-0 flex-col gap-2 border-border border-t p-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]\",\n        className,\n      )}\n    />\n  );\n});\n\nexport const AnimatedSidebarGroup = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function AnimatedSidebarGroup({ className, ...props }, forwardedRef) {\n  return (\n    <div\n      {...props}\n      ref={forwardedRef}\n      data-slot=\"sidebar-group\"\n      className={cn(\"flex w-full min-w-0 flex-col px-1 py-1.5\", className)}\n    />\n  );\n});\n\nexport const AnimatedSidebarGroupLabel = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function AnimatedSidebarGroupLabel(\n  { children, className, ...props },\n  forwardedRef,\n) {\n  const { collapsed } = useAnimatedSidebarPanel();\n\n  return (\n    <div\n      {...props}\n      ref={forwardedRef}\n      aria-hidden={collapsed}\n      data-slot=\"sidebar-group-label\"\n      className={cn(\n        \"mb-1 h-7 overflow-hidden px-2 text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground transition-opacity\",\n        collapsed ? \"opacity-0\" : \"opacity-100\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n});\n\nexport const AnimatedSidebarGroupContent = forwardRef<\n  HTMLDivElement,\n  HTMLAttributes<HTMLDivElement>\n>(function AnimatedSidebarGroupContent(\n  { className, ...props },\n  forwardedRef,\n) {\n  return (\n    <div\n      {...props}\n      ref={forwardedRef}\n      data-slot=\"sidebar-group-content\"\n      className={cn(\"w-full min-w-0\", className)}\n    />\n  );\n});\n\nexport const AnimatedSidebarMenu = forwardRef<\n  HTMLUListElement,\n  HTMLAttributes<HTMLUListElement>\n>(function AnimatedSidebarMenu(\n  { children, className, ...props },\n  forwardedRef,\n) {\n  return (\n    <SharedLayoutBg\n      {...props}\n      ref={forwardedRef as React.Ref<HTMLElement>}\n      as=\"ul\"\n      inset={0}\n      pillClassName=\"rounded-xl bg-muted/70\"\n      pillContainerClassName=\"inset-y-auto top-0 h-9\"\n      data-slot=\"sidebar-menu\"\n      className={cn(\"flex w-full min-w-0 list-none flex-col gap-0.5\", className)}\n    >\n      {children}\n    </SharedLayoutBg>\n  );\n});\n\nexport const AnimatedSidebarMenuItem = forwardRef<\n  HTMLLIElement,\n  HTMLMotionProps<\"li\">\n>(function AnimatedSidebarMenuItem({ className, ...props }, forwardedRef) {\n  return (\n    <motion.li\n      {...props}\n      ref={forwardedRef}\n      layout=\"position\"\n      transition={SPRING_LAYOUT}\n      data-slot=\"sidebar-menu-item\"\n      className={cn(\"relative\", className)}\n    />\n  );\n});\n\nexport interface AnimatedSidebarMenuSubProps\n  extends Omit<HTMLMotionProps<\"ul\">, \"children\"> {\n  open: boolean;\n  children?: ReactNode;\n}\n\nexport const AnimatedSidebarMenuSub = forwardRef<\n  HTMLUListElement,\n  AnimatedSidebarMenuSubProps\n>(function AnimatedSidebarMenuSub(\n  { open, children, className, ...props },\n  forwardedRef,\n) {\n  const context = useAnimatedSidebar();\n  const panel = useAnimatedSidebarPanel();\n\n  return (\n    <AnimatePresence initial={false} mode=\"popLayout\">\n      {open && !panel.collapsed ? (\n        <motion.ul\n          {...props}\n          ref={forwardedRef}\n          key=\"sidebar-submenu\"\n          variants={context.reduce ? undefined : SUBMENU_VARIANTS}\n          initial={context.reduce ? false : \"closed\"}\n          animate={context.reduce ? { opacity: 1 } : \"open\"}\n          exit={context.reduce ? { opacity: 0 } : \"closed\"}\n          transition={context.reduce ? { duration: 0.12 } : undefined}\n          data-slot=\"sidebar-menu-sub\"\n          className={cn(\n            \"relative mt-1 ml-5 flex min-w-0 flex-col gap-0.5 border-border border-l pl-3\",\n            className,\n          )}\n        >\n          {children}\n        </motion.ul>\n      ) : null}\n    </AnimatePresence>\n  );\n});\n\nexport const AnimatedSidebarMenuSubItem = forwardRef<\n  HTMLLIElement,\n  HTMLMotionProps<\"li\">\n>(function AnimatedSidebarMenuSubItem(\n  { className, ...props },\n  forwardedRef,\n) {\n  return (\n    <motion.li\n      {...props}\n      ref={forwardedRef}\n      variants={SUBMENU_ITEM_VARIANTS}\n      data-slot=\"sidebar-menu-sub-item\"\n      className={cn(\"relative min-w-0\", className)}\n    />\n  );\n});\n\nexport interface AnimatedSidebarMenuSubButtonProps {\n  children: ReactNode;\n  icon?: ReactNode;\n  href?: string;\n  isActive?: boolean;\n  disabled?: boolean;\n  closeOnSelect?: boolean;\n  target?: \"_blank\" | \"_self\" | \"_parent\" | \"_top\";\n  rel?: string;\n  onSelect?: () => void;\n  className?: string;\n}\n\nexport function AnimatedSidebarMenuSubButton({\n  children,\n  icon,\n  href,\n  isActive = false,\n  disabled = false,\n  closeOnSelect = true,\n  target,\n  rel,\n  onSelect,\n  className,\n}: AnimatedSidebarMenuSubButtonProps) {\n  const context = useAnimatedSidebar();\n\n  const select = (\n    event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>,\n  ) => {\n    if (disabled) {\n      event.preventDefault();\n      return;\n    }\n    onSelect?.();\n    if (context.isMobile && closeOnSelect) context.setOpenMobile(false);\n  };\n\n  const content = (\n    <>\n      <span\n        aria-hidden=\"true\"\n        className=\"grid size-4 shrink-0 place-items-center\"\n      >\n        {icon ?? <span className=\"size-1 rounded-full bg-current\" />}\n      </span>\n      <span className=\"min-w-0 flex-1 truncate\">{children}</span>\n    </>\n  );\n\n  const interactiveClassName = cn(\n    \"flex min-h-8 w-full min-w-0 items-center gap-2 rounded-lg px-2 text-left text-xs outline-none\",\n    \"text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground\",\n    \"focus-visible:bg-muted/70 focus-visible:ring-2 focus-visible:ring-ring\",\n    isActive && \"bg-muted/70 text-foreground\",\n    disabled && \"cursor-not-allowed opacity-40\",\n    className,\n  );\n\n  return href ? (\n    <motion.a\n      href={href}\n      target={target}\n      rel={\n        rel ??\n        (target === \"_blank\" ? \"noreferrer noopener\" : undefined)\n      }\n      aria-current={isActive ? \"page\" : undefined}\n      aria-disabled={disabled || undefined}\n      tabIndex={disabled ? -1 : undefined}\n      onClick={select}\n      whileTap={context.reduce || disabled ? undefined : { scale: 0.98 }}\n      transition={SPRING_PRESS}\n      className={interactiveClassName}\n    >\n      {content}\n    </motion.a>\n  ) : (\n    <motion.button\n      type=\"button\"\n      disabled={disabled}\n      aria-current={isActive ? \"page\" : undefined}\n      onClick={select}\n      whileTap={context.reduce || disabled ? undefined : { scale: 0.98 }}\n      transition={SPRING_PRESS}\n      className={interactiveClassName}\n    >\n      {content}\n    </motion.button>\n  );\n}\n\nexport interface AnimatedSidebarMenuButtonProps {\n  children: ReactNode;\n  icon?: ReactNode;\n  badge?: ReactNode;\n  href?: string;\n  isActive?: boolean;\n  ariaExpanded?: boolean;\n  disabled?: boolean;\n  closeOnSelect?: boolean;\n  target?: \"_blank\" | \"_self\" | \"_parent\" | \"_top\";\n  rel?: string;\n  onSelect?: () => void;\n  className?: string;\n}\n\nexport function AnimatedSidebarMenuButton({\n  children,\n  icon,\n  badge,\n  href,\n  isActive = false,\n  ariaExpanded,\n  disabled = false,\n  closeOnSelect,\n  target,\n  rel,\n  onSelect,\n  className,\n}: AnimatedSidebarMenuButtonProps) {\n  const context = useAnimatedSidebar();\n  const panel = useAnimatedSidebarPanel();\n  const textLabel = typeof children === \"string\" ? children : undefined;\n\n  const select = (\n    event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>,\n  ) => {\n    if (disabled) {\n      event.preventDefault();\n      return;\n    }\n    onSelect?.();\n    const shouldCloseOnSelect =\n      closeOnSelect ?? ariaExpanded === undefined;\n    if (context.isMobile && shouldCloseOnSelect) {\n      context.setOpenMobile(false);\n    }\n  };\n\n  const content = (\n    <>\n      {isActive ? (\n        <motion.span\n          layoutId={context.layoutId}\n          transition={context.reduce ? { duration: 0 } : SPRING_LAYOUT}\n          className=\"absolute inset-0 rounded-xl bg-muted\"\n        />\n      ) : null}\n      {icon ? (\n        <span\n          aria-hidden=\"true\"\n          className=\"relative z-10 grid size-5 shrink-0 place-items-center\"\n        >\n          {icon}\n        </span>\n      ) : null}\n      <motion.span\n        initial={false}\n        animate={{\n          opacity: panel.collapsed ? 0 : 1,\n          x: panel.collapsed ? -4 : 0,\n        }}\n        transition={\n          context.reduce\n            ? REDUCED_TRANSITION\n            : panel.collapsed\n              ? LABEL_EXIT_TRANSITION\n              : LABEL_ENTER_TRANSITION\n        }\n        aria-hidden={panel.collapsed}\n        className={cn(\n          \"relative z-10 min-w-0 flex-1 truncate\",\n          panel.collapsed && \"pointer-events-none\",\n        )}\n      >\n        {children}\n      </motion.span>\n      {badge && !panel.collapsed ? (\n        <span className=\"relative z-10 shrink-0 text-xs text-muted-foreground\">\n          {badge}\n        </span>\n      ) : null}\n      {ariaExpanded !== undefined ? (\n        <motion.span\n          aria-hidden=\"true\"\n          initial={false}\n          animate={{\n            opacity: panel.collapsed ? 0 : 1,\n            rotate: ariaExpanded ? 90 : 0,\n            x: panel.collapsed ? 4 : 0,\n          }}\n          transition={context.reduce ? { duration: 0 } : SPRING_LAYOUT}\n          className=\"relative z-10 grid size-4 shrink-0 place-items-center text-muted-foreground\"\n        >\n          <ChevronRight className=\"size-3.5\" />\n        </motion.span>\n      ) : null}\n    </>\n  );\n\n  const interactiveClassName = cn(\n    \"relative flex min-h-9 w-full min-w-0 items-center gap-2.5 overflow-hidden rounded-xl px-3 text-left text-sm font-medium outline-none\",\n    \"text-muted-foreground transition-colors hover:text-foreground\",\n    \"focus-visible:bg-muted/70 focus-visible:ring-2 focus-visible:ring-ring\",\n    isActive && \"text-foreground\",\n    disabled && \"cursor-not-allowed opacity-40\",\n    className,\n  );\n\n  return href ? (\n    <motion.a\n      href={href}\n      target={target}\n      rel={\n        rel ??\n        (target === \"_blank\" ? \"noreferrer noopener\" : undefined)\n      }\n      aria-current={isActive ? \"page\" : undefined}\n      aria-expanded={ariaExpanded}\n      aria-disabled={disabled || undefined}\n      aria-label={panel.collapsed ? textLabel : undefined}\n      title={panel.collapsed ? textLabel : undefined}\n      tabIndex={disabled ? -1 : undefined}\n      onClick={select}\n      whileTap={context.reduce || disabled ? undefined : { scale: 0.98 }}\n      transition={SPRING_PRESS}\n      className={interactiveClassName}\n    >\n      {content}\n    </motion.a>\n  ) : (\n    <motion.button\n      type=\"button\"\n      disabled={disabled}\n      aria-current={isActive ? \"page\" : undefined}\n      aria-expanded={ariaExpanded}\n      aria-label={panel.collapsed ? textLabel : undefined}\n      title={panel.collapsed ? textLabel : undefined}\n      onClick={select}\n      whileTap={context.reduce || disabled ? undefined : { scale: 0.98 }}\n      transition={SPRING_PRESS}\n      className={interactiveClassName}\n    >\n      {content}\n    </motion.button>\n  );\n}\n"},{"path":"components/motion/shared-layout-bg.tsx","type":"registry:component","target":"@components/motion/shared-layout-bg.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  Children,\n  cloneElement,\n  forwardRef,\n  type HTMLAttributes,\n  isValidElement,\n  type MouseEvent,\n  type ReactElement,\n  type ReactNode,\n  type Ref,\n  useId,\n  useState,\n} from \"react\";\nimport { SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface SharedLayoutBgProps\n  extends Omit<HTMLAttributes<HTMLElement>, \"children\"> {\n  children: ReactNode;\n  /** Semantic container used for the children. */\n  as?: \"div\" | \"ul\";\n  /** Tailwind class applied to the moving pill. Defaults to a subtle foreground tint. */\n  pillClassName?: string;\n  /** Horizontal inset of the pill relative to each row (px). Default 20. */\n  inset?: number;\n  /** Optional positioning override for the pill wrapper inside each item. */\n  pillContainerClassName?: string;\n}\n\nconst variants: Variants = {\n  initial: { opacity: 0, filter: \"blur(6px)\" },\n  animate: { opacity: 1, filter: \"blur(0px)\" },\n  exit: (isActive: boolean) =>\n    !isActive ? { opacity: 0, filter: \"blur(6px)\" } : {},\n};\n\nconst reducedVariants: Variants = {\n  initial: { opacity: 0 },\n  animate: { opacity: 1 },\n  exit: (isActive: boolean) => (!isActive ? { opacity: 0 } : {}),\n};\n\nexport const SharedLayoutBg = forwardRef<HTMLElement, SharedLayoutBgProps>(\n  function SharedLayoutBg(\n    {\n      children,\n      as = \"div\",\n      className,\n      onMouseLeave,\n      pillClassName,\n      pillContainerClassName,\n      inset = 20,\n      ...props\n    },\n    forwardedRef,\n  ) {\n  const [activeId, setActiveId] = useState<string | null>(null);\n  const uid = useId();\n  const reduce = useReducedMotion();\n\n    const renderedChildren = Children.toArray(children)\n      .filter(isValidElement)\n      .map((child, index) => {\n        const el = child as ReactElement<{\n          className?: string;\n          onMouseEnter?: () => void;\n          children?: ReactNode;\n        }>;\n        const childKey = el.key ? String(el.key) : `item-${index}`;\n        return cloneElement(\n          el,\n          {\n            key: childKey,\n            className: cn(\"relative\", el.props.className),\n            onMouseEnter: () => {\n              el.props.onMouseEnter?.();\n              setActiveId(childKey);\n            },\n          },\n          <>\n            <AnimatePresence custom={activeId !== null}>\n              {activeId !== null ? (\n                <motion.div\n                  variants={reduce ? reducedVariants : variants}\n                  initial=\"initial\"\n                  animate=\"animate\"\n                  exit=\"exit\"\n                  custom={activeId !== null}\n                  className={cn(\n                    \"pointer-events-none absolute inset-y-0\",\n                    pillContainerClassName,\n                  )}\n                  style={{ left: -inset, right: -inset }}\n                >\n                  {activeId === childKey ? (\n                    <motion.div\n                      layoutId={`shared-bg-${uid}`}\n                      transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n                      className={cn(\n                        \"pointer-events-none h-full w-full rounded-2xl bg-primary/[0.06]\",\n                        pillClassName,\n                      )}\n                    />\n                  ) : null}\n                </motion.div>\n              ) : null}\n            </AnimatePresence>\n            <div className=\"relative z-10\">{el.props.children}</div>\n          </>,\n        );\n      });\n\n    const handleMouseLeave = (event: MouseEvent<HTMLElement>) => {\n      setActiveId(null);\n      onMouseLeave?.(event);\n    };\n\n    // layoutRoot scopes the pill's layout projection to this list, so fixed or\n    // scrolled ancestors can't smear scroll offsets into its movement.\n    return as === \"ul\" ? (\n      <motion.ul\n        {...(props as HTMLMotionProps<\"ul\">)}\n        ref={forwardedRef as Ref<HTMLUListElement>}\n        layoutRoot\n        onMouseLeave={handleMouseLeave}\n        className={cn(\"flex w-full flex-col\", className)}\n      >\n        {renderedChildren}\n      </motion.ul>\n    ) : (\n      <motion.div\n        {...(props as HTMLMotionProps<\"div\">)}\n        ref={forwardedRef as Ref<HTMLDivElement>}\n        layoutRoot\n        onMouseLeave={handleMouseLeave}\n        className={cn(\"flex w-full flex-col\", className)}\n      >\n        {renderedChildren}\n      </motion.div>\n    );\n  },\n);\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}