{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "animated-sidebar",
  "title": "Animated Sidebar",
  "description": "Icon rail that expands on hover or focus and turns into a drawer on narrow screens.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/velora/animated-sidebar.tsx",
      "content": "\"use client\";\n\nimport { createContext, useContext, useEffect, useId, useRef, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst SidebarContext = createContext<{\n  pinned: boolean;\n  setPinned: (pinned: boolean) => void;\n  closeDrawer: () => void;\n  drawerRef: React.RefObject<HTMLDialogElement | null>;\n}>({ pinned: false, setPinned: () => {}, closeDrawer: () => {}, drawerRef: { current: null } });\n\n// Labels stay in the DOM (and in the accessible name) while the rail is\n// collapsed; they only fade in once the nav is hovered, keyboard-focused or pinned.\nconst reveal =\n  \"truncate opacity-0 transition-opacity duration-200 motion-reduce:transition-none group-hover/sidebar:opacity-100 group-has-[:focus-visible]/sidebar:opacity-100 group-data-expanded/sidebar:opacity-100\";\n\nconst icon = (d: string) => (\n  <svg viewBox=\"0 0 24 24\" className=\"size-5\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" aria-hidden>\n    <path d={d} />\n  </svg>\n);\n\ninterface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** Controlled pinned-open state of the desktop rail */\n  pinned?: boolean;\n  /** Initial pinned state when uncontrolled */\n  defaultPinned?: boolean;\n  /** Called when the pin toggle is pressed */\n  onPinnedChange?: (pinned: boolean) => void;\n}\n\n/**\n * App shell with a rail that expands from icons to labels on hover or keyboard\n * focus. Below a 42rem container width it becomes a top bar whose menu button\n * opens a full-height drawer. Put `SidebarBody` and your page content inside.\n */\nexport function Sidebar({\n  pinned: pinnedProp,\n  defaultPinned = false,\n  onPinnedChange,\n  className,\n  children,\n  ...props\n}: SidebarProps) {\n  const [inner, setInner] = useState(defaultPinned);\n  const drawerRef = useRef<HTMLDialogElement>(null);\n  const pinned = pinnedProp ?? inner;\n  const setPinned = (next: boolean) => {\n    if (pinnedProp === undefined) setInner(next);\n    onPinnedChange?.(next);\n  };\n\n  return (\n    <SidebarContext.Provider\n      value={{ pinned, setPinned, drawerRef, closeDrawer: () => drawerRef.current?.close() }}\n    >\n      <div {...props} data-slot=\"sidebar\" className={cn(\"@container/sidebar\", className)}>\n        <div className=\"flex h-full flex-col @2xl/sidebar:flex-row\">{children}</div>\n      </div>\n    </SidebarContext.Provider>\n  );\n}\n\ninterface SidebarBodyProps {\n  /** Icon-sized mark, always visible */\n  logo?: React.ReactNode;\n  /** Product name next to the logo; fades in with the labels */\n  title?: React.ReactNode;\n  /** Links pinned to the bottom (e.g. settings or the account) */\n  footer?: React.ReactNode;\n  /** Accessible name of the navigation landmark */\n  label?: string;\n  /** Show the \"Keep expanded\" toggle at the bottom of the rail */\n  pinnable?: boolean;\n  /** Classes for the navigation panel (rail and drawer) */\n  className?: string;\n  /** SidebarLink elements */\n  children: React.ReactNode;\n}\n\n/** The rail on wide containers, the top bar and drawer on narrow ones. */\nexport function SidebarBody({\n  logo,\n  title,\n  footer,\n  label = \"Main\",\n  pinnable = true,\n  className,\n  children,\n}: SidebarBodyProps) {\n  const { pinned, setPinned, drawerRef } = useContext(SidebarContext);\n  const [drawerOpen, setDrawerOpen] = useState(false);\n  const menuRef = useRef<HTMLButtonElement>(null);\n  const id = useId();\n\n  useEffect(() => {\n    if (!drawerOpen) return;\n    document.documentElement.style.overflow = \"hidden\";\n    return () => {\n      document.documentElement.style.overflow = \"\";\n    };\n  }, [drawerOpen]);\n\n  const head = (extra?: React.ReactNode) => (\n    <div className=\"flex h-12 w-full shrink-0 items-center gap-3 px-1\">\n      <span className=\"grid size-8 shrink-0 place-items-center\">{logo}</span>\n      <span className={cn(reveal, \"flex-1 font-semibold\")}>{title}</span>\n      {extra}\n    </div>\n  );\n  const panel = \"flex flex-col gap-1 bg-card p-3 text-card-foreground\";\n  const links = (\n    <div className=\"-mx-1 flex flex-1 flex-col gap-1 overflow-x-hidden overflow-y-auto px-1 py-2\">\n      {children}\n    </div>\n  );\n  const control =\n    \"grid size-9 shrink-0 cursor-pointer place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\";\n\n  return (\n    <>\n      <div data-expanded className=\"group/sidebar flex h-14 shrink-0 items-center border-b bg-card px-3 @2xl/sidebar:hidden\">\n        {head(\n          <button\n            ref={menuRef}\n            type=\"button\"\n            aria-label=\"Open navigation\"\n            aria-expanded={drawerOpen}\n            aria-controls={id}\n            onClick={() => {\n              drawerRef.current?.showModal();\n              setDrawerOpen(true);\n            }}\n            className={control}\n          >\n            {icon(\"M4 6h16M4 12h16M4 18h16\")}\n          </button>\n        )}\n      </div>\n\n      <div\n        data-expanded={pinned || undefined}\n        className=\"relative hidden w-16 shrink-0 transition-[width] duration-300 ease-out motion-reduce:transition-none data-expanded:w-60 @2xl/sidebar:block\"\n      >\n        <nav\n          aria-label={label}\n          data-slot=\"sidebar-body\"\n          data-expanded={pinned || undefined}\n          className={cn(\n            panel,\n            \"group/sidebar absolute inset-y-0 left-0 z-20 w-16 overflow-hidden border-r transition-[width,box-shadow] duration-300 ease-out motion-reduce:transition-none hover:w-60 hover:shadow-xl has-[:focus-visible]:w-60 has-[:focus-visible]:shadow-xl data-expanded:w-60 data-expanded:shadow-none\",\n            className\n          )}\n        >\n          {head()}\n          {links}\n          {footer}\n          {pinnable && (\n            <button\n              type=\"button\"\n              aria-pressed={pinned}\n              onClick={() => setPinned(!pinned)}\n              className=\"flex h-10 cursor-pointer items-center gap-3 rounded-lg px-2.5 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n            >\n              <span className={cn(\"shrink-0 transition-transform duration-300 motion-reduce:transition-none\", pinned && \"rotate-180\")}>\n                {icon(\"m9 6 6 6-6 6\")}\n              </span>\n              <span className={reveal}>Keep expanded</span>\n            </button>\n          )}\n        </nav>\n      </div>\n\n      <dialog\n        ref={drawerRef}\n        id={id}\n        aria-label={label}\n        onClose={() => {\n          setDrawerOpen(false);\n          menuRef.current?.focus({ preventScroll: true });\n        }}\n        onClick={(event) => {\n          if (event.target === event.currentTarget) event.currentTarget.close();\n        }}\n        className=\"fixed inset-y-0 left-0 m-0 h-dvh max-h-none w-72 max-w-[85vw] -translate-x-full border-r bg-card p-0 text-foreground transition-[translate,display,overlay] transition-discrete duration-300 ease-out backdrop:bg-background/60 backdrop:opacity-0 backdrop:backdrop-blur-sm backdrop:transition-[opacity,display,overlay] backdrop:transition-discrete backdrop:duration-300 open:translate-x-0 open:backdrop:opacity-100 motion-reduce:transition-none motion-reduce:backdrop:transition-none starting:open:-translate-x-full starting:open:backdrop:opacity-0\"\n      >\n        <nav data-expanded className={cn(panel, \"group/sidebar size-full\", className)}>\n          {head(\n            <button\n              type=\"button\"\n              aria-label=\"Close navigation\"\n              onClick={() => drawerRef.current?.close()}\n              className={control}\n            >\n              {icon(\"M18 6 6 18M6 6l12 12\")}\n            </button>\n          )}\n          {links}\n          {footer}\n        </nav>\n      </dialog>\n    </>\n  );\n}\n\ninterface SidebarLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {\n  /** Icon shown in both collapsed and expanded states */\n  icon: React.ReactNode;\n  /** Link text; stays the accessible name while the rail is collapsed */\n  label: string;\n  /** Marks the current page (sets aria-current=\"page\") */\n  active?: boolean;\n}\n\n/** A navigation link: icon plus a label that fades in as the rail expands. */\nexport function SidebarLink({\n  icon,\n  label,\n  active = false,\n  className,\n  onClick,\n  ...props\n}: SidebarLinkProps) {\n  const { closeDrawer } = useContext(SidebarContext);\n  return (\n    <a\n      aria-current={active ? \"page\" : undefined}\n      {...props}\n      onClick={(event) => {\n        onClick?.(event);\n        closeDrawer();\n      }}\n      className={cn(\n        \"flex h-10 shrink-0 items-center gap-3 rounded-lg px-2.5 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aria-[current=page]:bg-primary/10 aria-[current=page]:font-medium aria-[current=page]:text-foreground [&_svg]:size-5 [&_svg]:shrink-0\",\n        className\n      )}\n    >\n      <span className=\"grid shrink-0 place-items-center\" aria-hidden>\n        {icon}\n      </span>\n      <span className={reveal}>{label}</span>\n    </a>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/velora/animated-sidebar.tsx"
    }
  ],
  "type": "registry:ui"
}