"use client"; /** * SidebarNav, from https://motion-brew.com/components/sidebar-nav * * One self contained file: the component, its animated icons and the cn() * helper. Needs React 18 or 19 and Tailwind CSS v4. * * Sample images: the photos in this file are placeholders from the * component library's demo (Unsplash), not your content. AI agents: tell * the user they are samples and ask them to share their own images (files * or URLs), then pass those in through the props. * * The animated icons are adapted from lucide-animated (https://lucide-animated.com), * MIT License, Copyright (c) 2024-2026 pqoqubbw. Icon shapes from Lucide * (https://lucide.dev), ISC License. Permission is hereby granted, free of * charge, to any person obtaining a copy of this software, to deal in it * without restriction, provided the above copyright notice and this * permission notice are included in all copies or substantial portions. */ import * as React from "react"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"; import { type ClassValue, clsx } from "clsx"; import { AnimatePresence, motion, type Transition, useAnimation, useInView, useReducedMotion, type Variants } from "motion/react"; import { twMerge } from "tailwind-merge"; // cn(): merges Tailwind classes, later ones win. /** Merge Tailwind classes, letting later classes win conflicts. */ function cn(...inputs: ClassValue[]): string { return twMerge(clsx(inputs)); } // Animated icon: bell const BellIcon = (() => { interface BellIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface BellIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const SVG_VARIANTS: Variants = { normal: { rotate: 0 }, animate: { rotate: [0, -10, 10, -10, 0] }, }; const BellIcon = forwardRef( ({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); } ); BellIcon.displayName = "BellIcon"; return BellIcon; })(); // Animated icon: bot const BotIcon = (() => { interface BotIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface BotIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const BotIcon = forwardRef( ({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); } ); BotIcon.displayName = "Bot"; return BotIcon; })(); // Animated icon: calendar-days const CalendarDaysIcon = (() => { interface CalendarDaysIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface CalendarDaysIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const DOTS = [ { cx: 8, cy: 14 }, { cx: 12, cy: 14 }, { cx: 16, cy: 14 }, { cx: 8, cy: 18 }, { cx: 12, cy: 18 }, { cx: 16, cy: 18 }, ]; const VARIANTS: Variants = { normal: { opacity: 1, transition: { duration: 0.2, }, }, animate: (i: number) => ({ opacity: [1, 0.3, 1], transition: { delay: i * 0.1, duration: 0.4, times: [0, 0.5, 1], }, }), }; const CalendarDaysIcon = forwardRef< CalendarDaysIconHandle, CalendarDaysIconProps >(({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > {DOTS.map((dot, index) => ( ))} ); }); CalendarDaysIcon.displayName = "CalendarDaysIcon"; return CalendarDaysIcon; })(); // Animated icon: chart-line const ChartLineIcon = (() => { interface ChartLineIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface ChartLineIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const VARIANTS: Variants = { normal: { pathLength: 1, opacity: 1, }, animate: { pathLength: [0, 1], opacity: [0, 1], transition: { delay: 0.15, duration: 0.3, opacity: { delay: 0.1 }, }, }, }; const ChartLineIcon = forwardRef( ({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); } ); ChartLineIcon.displayName = "ChartLineIcon"; return ChartLineIcon; })(); // Animated icon: chevron-down const ChevronDownIcon = (() => { interface ChevronDownIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface ChevronDownIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const DEFAULT_TRANSITION: Transition = { times: [0, 0.4, 1], duration: 0.5, }; const ChevronDownIcon = forwardRef( ({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); } ); ChevronDownIcon.displayName = "ChevronDownIcon"; return ChevronDownIcon; })(); // Animated icon: compass const CompassIcon = (() => { interface CompassIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface CompassIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const CompassIcon = forwardRef( ({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); } ); CompassIcon.displayName = "CompassIcon"; return CompassIcon; })(); // Animated icon: folders const FoldersIcon = (() => { interface FoldersIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface FoldersIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const FoldersIcon = forwardRef( ({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); } ); FoldersIcon.displayName = "FoldersIcon"; return FoldersIcon; })(); // Animated icon: home const HomeIcon = (() => { interface HomeIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface HomeIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const DEFAULT_TRANSITION: Transition = { duration: 0.6, opacity: { duration: 0.2 }, }; const PATH_VARIANTS: Variants = { normal: { pathLength: 1, opacity: 1, }, animate: { opacity: [0, 1], pathLength: [0, 1], }, }; const HomeIcon = forwardRef( ({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); } ); HomeIcon.displayName = "HomeIcon"; return HomeIcon; })(); // Animated icon: layout-grid const LayoutGridIcon = (() => { interface LayoutGridIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface LayoutGridIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const RECT_1_VARIANTS: Variants = { normal: { translateX: 0, translateY: 0 }, animate: { translateX: [0, 11, 11, 0], translateY: [0, 0, 0, 0], transition: { duration: 0.8, ease: "easeInOut", times: [0, 0.4, 0.6, 1] }, }, }; const RECT_2_VARIANTS: Variants = { normal: { translateX: 0, translateY: 0 }, animate: { translateX: [0, 0, 0, 0], translateY: [0, 11, 11, 0], transition: { duration: 0.8, ease: "easeInOut", times: [0, 0.4, 0.6, 1] }, }, }; const RECT_3_VARIANTS: Variants = { normal: { translateX: 0, translateY: 0 }, animate: { translateX: [0, -11, -11, 0], translateY: [0, 0, 0, 0], transition: { duration: 0.8, ease: "easeInOut", times: [0, 0.4, 0.6, 1] }, }, }; const RECT_4_VARIANTS: Variants = { normal: { translateX: 0, translateY: 0 }, animate: { translateX: [0, 0, 0, 0], translateY: [0, -11, -11, 0], transition: { duration: 0.8, ease: "easeInOut", times: [0, 0.4, 0.6, 1] }, }, }; const LayoutGridIcon = forwardRef( ({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); } ); LayoutGridIcon.displayName = "LayoutGridIcon"; return LayoutGridIcon; })(); // Animated icon: message-circle const MessageCircleIcon = (() => { interface MessageCircleIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface MessageCircleIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const ICON_VARIANTS: Variants = { normal: { scale: 1, rotate: 0, }, animate: { scale: 1.05, rotate: [0, -7, 7, 0], transition: { rotate: { duration: 0.5, ease: "easeInOut", }, scale: { type: "spring", stiffness: 400, damping: 10, }, }, }, }; const MessageCircleIcon = forwardRef< MessageCircleIconHandle, MessageCircleIconProps >(({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); }); MessageCircleIcon.displayName = "MessageCircleIcon"; return MessageCircleIcon; })(); // Animated icon: panel-left-close const PanelLeftCloseIcon = (() => { interface PanelLeftCloseIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface PanelLeftCloseIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const DEFAULT_TRANSITION: Transition = { times: [0, 0.4, 1], duration: 0.5, }; const PATH_VARIANTS: Variants = { normal: { x: 0 }, animate: { x: [0, -1.5, 0] }, }; const PanelLeftCloseIcon = forwardRef< PanelLeftCloseIconHandle, PanelLeftCloseIconProps >(({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); }); PanelLeftCloseIcon.displayName = "PanelLeftCloseIcon"; return PanelLeftCloseIcon; })(); // Animated icon: panel-left-open const PanelLeftOpenIcon = (() => { interface PanelLeftOpenIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface PanelLeftOpenIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const DEFAULT_TRANSITION: Transition = { times: [0, 0.4, 1], duration: 0.5, }; const PATH_VARIANTS: Variants = { normal: { x: 0 }, animate: { x: [0, 1.5, 0] }, }; const PanelLeftOpenIcon = forwardRef< PanelLeftOpenIconHandle, PanelLeftOpenIconProps >(({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); }); PanelLeftOpenIcon.displayName = "PanelLeftOpenIcon"; return PanelLeftOpenIcon; })(); // Animated icon: plug-zap const PlugZapIcon = (() => { interface PlugZapIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface PlugZapIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const ZAP_VARIANT: Variants = { normal: { opacity: 1 }, animate: { opacity: [1, 0.4, 1], transition: { duration: 1, repeat: Number.POSITIVE_INFINITY, ease: "easeInOut", }, }, }; const PlugZapIcon = forwardRef( ({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); } ); PlugZapIcon.displayName = "PlugZapIcon"; return PlugZapIcon; })(); // Animated icon: search const SearchIcon = (() => { interface SearchIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface SearchIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const SearchIcon = forwardRef( ({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); } ); SearchIcon.displayName = "SearchIcon"; return SearchIcon; })(); // Animated icon: settings const SettingsIcon = (() => { interface SettingsIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface SettingsIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const SettingsIcon = forwardRef( ({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); } ); SettingsIcon.displayName = "SettingsIcon"; return SettingsIcon; })(); // Animated icon: users const UsersIcon = (() => { interface UsersIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface UsersIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const PATH_VARIANTS: Variants = { normal: { translateX: 0, transition: { type: "spring", stiffness: 200, damping: 13, }, }, animate: { translateX: [-6, 0], transition: { delay: 0.1, type: "spring", stiffness: 200, damping: 13, }, }, }; const UsersIcon = forwardRef( ({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); } ); UsersIcon.displayName = "UsersIcon"; return UsersIcon; })(); // Animated icon: workflow const WorkflowIcon = (() => { interface WorkflowIconHandle { startAnimation: () => void; stopAnimation: () => void; } interface WorkflowIconProps extends React.HTMLAttributes { /** Size in px; by default it follows className, like a lucide icon. */ size?: number; /** Play while true; when set, hover no longer drives it. */ active?: boolean; /** Line weight, like a lucide icon's. Default `2`. */ strokeWidth?: number | string; } const TRANSITION: Transition = { duration: 0.3, opacity: { delay: 0.15 }, }; const VARIANTS: Variants = { normal: { pathLength: 1, opacity: 1, }, animate: (custom: number) => ({ pathLength: [0, 1], opacity: [0, 1], transition: { ...TRANSITION, delay: 0.1 * custom, }, }), }; const WorkflowIcon = forwardRef( ({ onMouseEnter, onMouseLeave, className, size, active, style, strokeWidth, ...props }, ref) => { const controls = useAnimation(); const isControlledRef = useRef(false); useImperativeHandle(ref, () => { isControlledRef.current = true; return { startAnimation: () => controls.start("animate"), stopAnimation: () => controls.start("normal"), }; }); const handleMouseEnter = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseEnter?.(e); } else { controls.start("animate"); } }, [controls, onMouseEnter] ); const handleMouseLeave = useCallback( (e: React.MouseEvent) => { if (isControlledRef.current) { onMouseLeave?.(e); } else { controls.start("normal"); } }, [controls, onMouseLeave] ); // Play with the nearest interactive parent, or whenever `active` is set. // Reduced motion keeps the icon still. const wrap = useRef(null); const reduced = useReducedMotion(); const [hosted, setHosted] = useState(false); useEffect(() => { const fake = {} as React.MouseEvent; if (reduced) { setHosted(true); return; } if (active !== undefined) { setHosted(true); (active ? handleMouseEnter : handleMouseLeave)(fake); return; } const host = wrap.current?.parentElement?.closest("button, a, [role=button], [role=menuitem], [role=tab], [role=switch], [role=radio], label"); setHosted(!!host); if (!host) return; const on = () => handleMouseEnter(fake); const off = () => handleMouseLeave(fake); host.addEventListener("pointerenter", on); host.addEventListener("pointerleave", off); return () => { host.removeEventListener("pointerenter", on); host.removeEventListener("pointerleave", off); }; }, [active, reduced, handleMouseEnter, handleMouseLeave]); return ( svg]:size-full", className)} style={size ? { width: size, height: size, ...style } : style} onMouseEnter={hosted ? undefined : handleMouseEnter} onMouseLeave={hosted ? undefined : handleMouseLeave} {...props} > ); } ); WorkflowIcon.displayName = "WorkflowIcon"; return WorkflowIcon; })(); // SidebarNav /** * SidebarNav * * Trigger: press and hover (plus keys and in view). A white app sidebar that * folds between a full rail of icons and labels and a slim rail of icons only. * The card glides to its new width while the icons hold still on their own * line, so nothing jumps: labels blur out on the way in and come back one * after another on the way out, section titles shrink to short hairlines, * count badges become dots on their icons, and a single dark tooltip slides * from row to row under the pointer while the rail is slim. A soft gray pill * glides to the chosen page and a lighter one follows the pointer; every * icon plays its own animation while its row is hovered. Groups open into a * threaded list where a short ink tick slides to the chosen child. The fold * button glides from the end of the brand row to just under the logo, so it * stays in reach on the slim rail, and the logo's pinwheel turns when pointed * at. The arrow keys move through the rows. * Reduced motion keeps every state without movement. * * Engine: motion/react (width tween, staggered label fades, layoutId pills, * a spring tooltip, AnimatePresence for the group) with animated * icons from lucide-animated (MIT), vendored in src/icons. */ export type SidebarIcon = | "home" | "search" | "messages" | "projects" | "analytics" | "calendar" | "agents" | "workflows" | "integrations" | "apps" | "explore" | "team" | "bell"; export interface SidebarChild { id: string; label: string; badge?: number; } export interface SidebarItem { id: string; label: string; /** One of the built in animated icons. */ icon: SidebarIcon; /** A count on the right; a dot on the icon while the rail is slim. */ badge?: number; /** A small green tag, like "New". */ tag?: string; /** Pages under this one, shown in a threaded list that opens and closes. */ children?: SidebarChild[]; } export interface SidebarSection { /** A small title over the rows; a short hairline while the rail is slim. */ label?: string; items: SidebarItem[]; } export interface SidebarNavProps { /** The wordmark beside the logo. */ brand?: string; /** The logo mark, about 28px. Default a pinwheel. It stays on the slim rail. */ logo?: React.ReactNode; sections?: SidebarSection[]; /** The chosen page id (controlled). */ value?: string; defaultValue?: string; onNavigate?: (id: string) => void; /** Groups open at the start, by item id. */ defaultOpen?: string[]; /** Slim rail, icons only (controlled). */ collapsed?: boolean; defaultCollapsed?: boolean; onCollapsedChange?: (collapsed: boolean) => void; user?: { name: string; email?: string; avatar?: string }; onSettings?: () => void; className?: string; } const WIDE = 256; const SLIM = 64; // Rows sit inside 12px of padding and start with a 40px icon slot. const LABEL_W = WIDE - 24 - 40; const GLIDE = [0.22, 1, 0.36, 1] as const; const PILL = { type: "spring", stiffness: 520, damping: 40 } as const; const TIP = { type: "spring", stiffness: 600, damping: 42 } as const; const FOLD = (still: boolean) => still ? { duration: 0 } : { duration: 0.45, ease: GLIDE }; const CARD: Variants = { hidden: { opacity: 0, y: 16, scale: 0.98 }, shown: { opacity: 1, y: 0, scale: 1, transition: { duration: 0.55, ease: GLIDE, staggerChildren: 0.035, delayChildren: 0.1, }, }, }; const ROW: Variants = { hidden: { opacity: 0, y: 8 }, shown: { opacity: 1, y: 0, transition: { duration: 0.4, ease: GLIDE } }, }; type IconType = React.ComponentType<{ active?: boolean; className?: string }>; const ICONS: Record = { home: HomeIcon, search: SearchIcon, messages: MessageCircleIcon, projects: FoldersIcon, analytics: ChartLineIcon, calendar: CalendarDaysIcon, agents: BotIcon, workflows: WorkflowIcon, integrations: PlugZapIcon, apps: LayoutGridIcon, explore: CompassIcon, team: UsersIcon, bell: BellIcon, }; const DEFAULT_SECTIONS: SidebarSection[] = [ { label: "Workspace", items: [ { id: "home", label: "Home", icon: "home" }, { id: "messages", label: "Messages", icon: "messages", badge: 4 }, { id: "projects", label: "Projects", icon: "projects", children: [ { id: "website", label: "Website refresh" }, { id: "mobile", label: "Mobile app", badge: 2 }, { id: "brand", label: "Brand kit" }, ], }, { id: "analytics", label: "Analytics", icon: "analytics" }, ], }, { label: "Automation", items: [ { id: "agents", label: "Agents", icon: "agents", tag: "New" }, { id: "workflows", label: "Workflows", icon: "workflows" }, { id: "integrations", label: "Integrations", icon: "integrations" }, ], }, ]; const DEFAULT_USER = { name: "Jordan Lee", email: "jordan@lumen.app", avatar: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=96&h=96&fit=crop&crop=faces", }; const initialsOf = (name: string) => name .split(/\s+/) .slice(0, 2) .map((w) => w[0]?.toUpperCase() ?? "") .join(""); /** The default logo: a two tone pinwheel that turns a quarter when pointed at. */ function PinwheelMark({ turn }: { turn: boolean }) { return ( {[0, 90, 180, 270].map((deg, i) => ( ))} ); } export const SidebarNav = React.forwardRef( function SidebarNav( { brand = "Lumen", logo, sections = DEFAULT_SECTIONS, value: valueProp, defaultValue = "home", onNavigate, defaultOpen = ["projects"], collapsed: collapsedProp, defaultCollapsed = false, onCollapsedChange, user = DEFAULT_USER, onSettings, className, }, ref, ) { const still = !!useReducedMotion(); const id = React.useId(); const root = React.useRef(null); React.useImperativeHandle(ref, () => root.current!); const inView = useInView(root, { once: true, amount: 0.3 }); const [collapsedState, setCollapsedState] = React.useState(defaultCollapsed); const collapsed = collapsedProp ?? collapsedState; const setCollapsed = (next: boolean) => { setCollapsedState(next); onCollapsedChange?.(next); setTip(null); }; const [valueState, setValue] = React.useState(defaultValue); const value = valueProp ?? valueState; const go = (next: string) => { setValue(next); onNavigate?.(next); }; const [open, setOpen] = React.useState(() => new Set(defaultOpen)); const toggleGroup = (key: string) => setOpen((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); const [hover, setHover] = React.useState(null); const [logoHover, setLogoHover] = React.useState(false); // One tooltip for the slim rail, sliding to the row under the pointer. const [tip, setTip] = React.useState<{ label: string; note?: string; y: number; } | null>(null); const showTip = (el: HTMLElement, label: string, note?: string) => { if (!collapsed || !root.current) return; const a = el.getBoundingClientRect(); const b = root.current.getBoundingClientRect(); setTip({ label, note, y: a.top - b.top + a.height / 2 }); }; // Arrow keys move through the rows. const onKey = (e: React.KeyboardEvent) => { if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return; const rows = Array.from( root.current?.querySelectorAll("[data-nav-row]") ?? [], ); if (!rows.length) return; e.preventDefault(); const at = rows.indexOf(document.activeElement as HTMLElement); const next = e.key === "ArrowDown" ? (at + 1) % rows.length : (at - 1 + rows.length) % rows.length; rows[at < 0 ? 0 : next]?.focus(); }; const fade = (order: number) => ({ animate: collapsed ? { opacity: 0, x: -6, filter: "blur(4px)" } : { opacity: 1, x: 0, filter: "blur(0px)" }, transition: still ? { duration: 0 } : collapsed ? { duration: 0.16, ease: "easeOut" as const } : { duration: 0.32, ease: GLIDE, delay: 0.1 + order * 0.02 }, }); const pill = (layout: string, tone: string) => ( ); // A soft fade at the foot of the list while more rows sit below. const navRef = React.useRef(null); const [more, setMore] = React.useState(false); const measure = React.useCallback(() => { const el = navRef.current; if (el) setMore(el.scrollTop + el.clientHeight < el.scrollHeight - 4); }, []); React.useEffect(() => { const el = navRef.current; if (!el || typeof ResizeObserver === "undefined") return; const ro = new ResizeObserver(measure); ro.observe(el); Array.from(el.children).forEach((c) => ro.observe(c)); return () => ro.disconnect(); }, [measure, sections]); let order = 0; return ( {/* Brand: the logo and wordmark, and the fold button. */}
setLogoHover(true)} onPointerLeave={() => setLogoHover(false)} > {logo ?? }
{brand}
{/* Room under the logo for the fold button while the rail is slim. */} {/* One button that glides from the end of the brand row to just under the logo. */} setCollapsed(!collapsed)} onPointerEnter={(e) => showTip(e.currentTarget, "Expand sidebar")} onPointerLeave={() => setTip(null)} onFocus={(e) => showTip(e.currentTarget, "Expand sidebar")} onBlur={() => setTip(null)} initial={false} animate={{ x: collapsed ? 0 : WIDE - 24 - 40, y: collapsed ? 48 : 0, }} whileTap={still ? undefined : { scale: 0.92 }} transition={FOLD(still)} className="absolute left-3 top-0 grid size-10 cursor-pointer place-items-center rounded-xl text-zinc-500 outline-none transition-colors hover:bg-[#242423]/[0.05] hover:text-[#242423] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#242423]/25" > {/* Pages. */} {/* Account. */}
showTip(e.currentTarget, user.name, user.email) } onPointerLeave={() => setTip(null)} > {user.avatar ? ( ) : ( {initialsOf(user.name)} )}
{user.name} {user.email && ( {user.email} )}
{/* The slim rail's tooltip, one for every row, sliding between them. */} {collapsed && tip && (
{tip.label} {tip.note && ( {tip.note} )}
)}
); }, );