{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sidebar",
  "title": "Sidebar",
  "description": "A collapsible column of navigation with nested groups, an active item and badges, toggled with Mod+B, saved through a prop, and a drawer on phones.",
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json",
    "https://miseui.junsu4621.workers.dev/r/button.json",
    "https://miseui.junsu4621.workers.dev/r/drawer.json",
    "https://miseui.junsu4621.workers.dev/r/dropdown-menu.json",
    "https://miseui.junsu4621.workers.dev/r/tooltip.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/sidebar.tsx",
      "content": "import type * as React from \"react\"\nimport { useContext, useId } from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { PanelLeft } from \"lucide-react\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport { Button } from \"@/registry/miseui/ui/button\"\nimport { Drawer, DrawerContent, DrawerTitle } from \"@/registry/miseui/ui/drawer\"\nimport { SidebarItem, SidebarNested, type LinkRenderProps, type SidebarItemProps, type SidebarNestedProps } from \"@/registry/miseui/ui/sidebar-items\"\nimport {\n  SidebarContext,\n  SidebarProvider,\n  isToggleShortcut,\n  useSidebar,\n  type SidebarEffect,\n  type SidebarProviderProps,\n  type SidebarVariant,\n} from \"@/registry/miseui/ui/sidebar-state\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nconst DEFAULT_WIDTH = 240\n/** 40 px items plus 8 px padding each side. */\nconst ICON_WIDTH = 56\nconst DRAWER_WIDTH_CLASS = \"w-[min(18rem,calc(100vw-3rem))]\"\n\nconst SURFACE_CLASS: Record<SidebarVariant, string> = {\n  plain: \"h-full border-e bg-background\",\n  floating: \"m-2 h-[calc(100%-1rem)] rounded-xl border bg-background shadow-sm dark:shadow-black/40\",\n  inset: \"h-full bg-muted\",\n  rail: \"h-full border-e bg-background\",\n}\n\ntype AsideAttributes = Omit<React.ComponentProps<\"aside\">, \"onDrag\" | \"onDragStart\" | \"onDragEnd\" | \"onAnimationStart\">\n\ninterface SidebarProps extends AsideAttributes, Omit<SidebarProviderProps, \"children\"> {\n  /** Names the navigation landmark and the drawer. */\n  label?: string\n  /** Open width in px. */\n  width?: number\n}\n\nfunction SidebarSurface({ label = \"Main\", width = DEFAULT_WIDTH, className, style, children, ...props }: AsideAttributes & { label?: string; width?: number }) {\n  const { variant, effect, icons, drawer, drawerOpen, setDrawerOpen } = useSidebar()\n  const reduceMotion = useReducedMotion() ?? false\n  if (drawer) {\n    return (\n      <Drawer open={drawerOpen} onOpenChange={setDrawerOpen} side=\"left\">\n        <DrawerContent aria-describedby={undefined} className={DRAWER_WIDTH_CLASS} data-testid=\"sidebar-drawer\">\n          <DrawerTitle className=\"sr-only\">{label}</DrawerTitle>\n          <div data-slot=\"sidebar\" data-variant={variant} data-drawer=\"true\" aria-label={label} className={cn(\"flex min-h-0 flex-1 flex-col pt-12 has-[[data-slot=sidebar-header]]:pt-0\", className)}>\n            {children}\n          </div>\n        </DrawerContent>\n      </Drawer>\n    )\n  }\n  const size = icons ? ICON_WIDTH : width\n  const shared = {\n    \"data-slot\": \"sidebar\",\n    \"data-variant\": variant,\n    \"data-effect\": effect,\n    \"data-collapsed\": icons ? \"true\" : undefined,\n    \"aria-label\": label,\n    className: cn(\"flex max-w-full shrink-0 flex-col overflow-hidden\", SURFACE_CLASS[variant], className),\n  }\n  if (effect === \"spring\") {\n    return (\n      <motion.aside {...shared} {...props} style={style} initial={false} animate={{ width: size }} transition={reduceMotion ? { duration: 0 } : SPRING_DEFAULT}>\n        {children}\n      </motion.aside>\n    )\n  }\n  return (\n    <aside {...shared} {...props} style={{ ...style, width: size }}>\n      {children}\n    </aside>\n  )\n}\n\n/** Brings its own provider unless one is already there. Inside a provider, set `variant` and friends on the provider. */\nfunction Sidebar({ variant, effect, collapsed, defaultCollapsed, onCollapsedChange, shortcut, mobile, ...props }: SidebarProps) {\n  const outer = useContext(SidebarContext)\n  if (outer) return <SidebarSurface {...props} />\n  return (\n    <SidebarProvider variant={variant} effect={effect} collapsed={collapsed} defaultCollapsed={defaultCollapsed} onCollapsedChange={onCollapsedChange} shortcut={shortcut} mobile={mobile}>\n      <SidebarSurface {...props} />\n    </SidebarProvider>\n  )\n}\n\nfunction SidebarHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"sidebar-header\" className={cn(\"flex h-14 shrink-0 items-center gap-3 overflow-hidden px-3 whitespace-nowrap\", className)} {...props} />\n}\n\nfunction SidebarContent({ className, label = \"Primary\", ...props }: React.ComponentProps<\"nav\"> & { label?: string }) {\n  return <nav data-slot=\"sidebar-content\" aria-label={label} className={cn(\"grid min-h-0 flex-1 grid-cols-[minmax(0,1fr)] content-start gap-4 overflow-x-hidden overflow-y-auto p-2\", className)} {...props} />\n}\n\nfunction SidebarFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"sidebar-footer\" className={cn(\"mt-auto shrink-0 border-t p-2\", className)} {...props} />\n}\n\nfunction SidebarGroup({ label, className, children, ...props }: React.ComponentProps<\"div\"> & { label?: string }) {\n  const { icons } = useSidebar()\n  const labelId = useId()\n  return (\n    <div data-slot=\"sidebar-group\" role=\"group\" aria-labelledby={label ? labelId : undefined} className={cn(\"grid grid-cols-[minmax(0,1fr)] gap-0.5\", className)} {...props}>\n      {label ? (\n        <p id={labelId} className={icons ? \"sr-only\" : \"flex h-8 items-center truncate px-3 text-[13px] text-muted-foreground\"}>\n          {label}\n        </p>\n      ) : null}\n      <ul className=\"grid grid-cols-[minmax(0,1fr)] gap-0.5\">{children}</ul>\n    </div>\n  )\n}\n\nfunction SidebarTrigger({ className, children, \"aria-label\": ariaLabel = \"Toggle sidebar\", onClick, ...props }: React.ComponentProps<typeof Button>) {\n  const { toggle, expanded, canToggle, shortcut } = useSidebar()\n  if (!canToggle) return null\n  return (\n    <Button\n      data-slot=\"sidebar-trigger\"\n      variant=\"ghost\"\n      size=\"icon\"\n      aria-label={ariaLabel}\n      aria-expanded={expanded}\n      aria-keyshortcuts={shortcut ? \"Meta+B Control+B\" : undefined}\n      className={cn(\"shrink-0 pointer-coarse:size-10\", className)}\n      onClick={(event) => {\n        onClick?.(event)\n        toggle()\n      }}\n      {...props}\n    >\n      {children ?? <PanelLeft aria-hidden=\"true\" />}\n    </Button>\n  )\n}\n\n/** The page beside an `inset` sidebar: raised on its own surface while the sidebar's tint runs around it. */\nfunction SidebarInset({ className, ...props }: React.ComponentProps<\"div\">) {\n  return <div data-slot=\"sidebar-inset\" className={cn(\"my-2 me-2 min-w-0 flex-1 overflow-auto rounded-xl border bg-background max-md:m-0 max-md:rounded-none max-md:border-0\", className)} {...props} />\n}\n\nexport {\n  Sidebar,\n  SidebarProvider,\n  SidebarHeader,\n  SidebarContent,\n  SidebarFooter,\n  SidebarGroup,\n  SidebarItem,\n  SidebarNested,\n  SidebarTrigger,\n  SidebarInset,\n  useSidebar,\n  isToggleShortcut,\n  type LinkRenderProps,\n  type SidebarEffect,\n  type SidebarItemProps,\n  type SidebarNestedProps,\n  type SidebarProps,\n  type SidebarProviderProps,\n  type SidebarVariant,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/sidebar-items.tsx",
      "content": "import type * as React from \"react\"\nimport { createContext, useContext, useId, useState } from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { ChevronRight } from \"lucide-react\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from \"@/registry/miseui/ui/dropdown-menu\"\nimport { useSidebar } from \"@/registry/miseui/ui/sidebar-state\"\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/registry/miseui/ui/tooltip\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\n/**\n * gap-3 starts the label exactly at the 40 px item's clip edge, so narrowing the column wipes the words\n * away and they stay in the accessibility tree, which an icon-only item would otherwise lose.\n */\nconst ITEM_CLASS = [\n  \"relative flex h-9 w-full items-center gap-3 overflow-hidden rounded-md px-3 text-start text-sm whitespace-nowrap text-foreground/80 outline-none pointer-coarse:h-10\",\n  \"transition-colors duration-(--duration-fast) ease-(--ease-mise) hover:bg-accent hover:text-foreground\",\n  \"focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset\",\n  \"data-[active=true]:bg-accent data-[active=true]:font-medium data-[active=true]:text-foreground\",\n  \"aria-disabled:pointer-events-none aria-disabled:opacity-50\",\n  \"[&_svg]:size-4 [&_svg]:shrink-0\",\n].join(\" \")\n\nconst INSET_ACTIVE_CLASS = \"data-[active=true]:bg-background data-[active=true]:shadow-xs\"\nconst NESTED_ITEM_CLASS = \"h-8\"\n\ninterface ItemLevel {\n  nested: boolean\n  flyout: boolean\n}\n\nconst ItemLevelContext = createContext<ItemLevel>({ nested: false, flyout: false })\n\ninterface LinkRenderProps {\n  className: string\n  children: React.ReactNode\n  \"aria-current\"?: \"page\"\n  \"data-active\"?: \"true\"\n  \"data-slot\": string\n  onClick?: React.MouseEventHandler<HTMLElement>\n}\n\ninterface SidebarItemProps {\n  icon?: React.ReactNode\n  /** Marks the current page: `aria-current` and the tinted row. */\n  active?: boolean\n  /** A short count or word at the end of the row. Collapsed, it becomes a mark on the icon. */\n  badge?: React.ReactNode\n  href?: string\n  disabled?: boolean\n  onClick?: React.MouseEventHandler<HTMLElement>\n  /** For a router's link: `render={(props) => <Link to=\"/inbox\" {...props} />}`. */\n  render?: (props: LinkRenderProps) => React.ReactElement\n  className?: string\n  children: React.ReactNode\n}\n\nfunction ItemBody({ icon, badge, icons, children }: Pick<SidebarItemProps, \"icon\" | \"badge\" | \"children\"> & { icons: boolean }) {\n  return (\n    <>\n      {icon}\n      <span className={icons ? \"shrink-0\" : \"min-w-0 flex-1 truncate\"}>{children}</span>\n      {/* Without the comma, the accessible name runs together as \"Inbox3\". */}\n      {badge !== undefined ? <span className=\"sr-only\">, </span> : null}\n      {badge !== undefined && !icons ? <span className=\"ms-auto shrink-0 ps-2 text-xs font-normal text-muted-foreground tabular-nums\">{badge}</span> : null}\n      {badge !== undefined && icons ? <span data-slot=\"sidebar-badge-mark\" aria-hidden=\"true\" className=\"absolute top-1.5 right-1.5 size-2 rounded-full bg-mise ring-2 ring-background\" /> : null}\n      {badge !== undefined && icons ? <span className=\"sr-only\">{badge}</span> : null}\n    </>\n  )\n}\n\nfunction SidebarItem({ icon, active = false, badge, href, disabled = false, onClick, render, className, children }: SidebarItemProps) {\n  const { icons, drawer, setDrawerOpen, variant } = useSidebar()\n  const { nested, flyout } = useContext(ItemLevelContext)\n  const props: LinkRenderProps = {\n    className: cn(ITEM_CLASS, variant === \"inset\" && !flyout && INSET_ACTIVE_CLASS, nested && !flyout && NESTED_ITEM_CLASS, flyout && \"h-auto rounded-sm px-2 py-1.5\", className),\n    children: <ItemBody icon={icon} badge={badge} icons={icons && !nested}>{children}</ItemBody>,\n    \"aria-current\": active ? \"page\" : undefined,\n    \"data-active\": active ? \"true\" : undefined,\n    \"data-slot\": \"sidebar-item\",\n    onClick: (event) => {\n      onClick?.(event)\n      if (drawer) setDrawerOpen(false)\n    },\n  }\n  const element = disabled ? (\n    <span {...props} aria-disabled=\"true\" onClick={undefined} />\n  ) : render ? (\n    render(props)\n  ) : href ? (\n    <a {...props} href={href} />\n  ) : (\n    <button {...props} type=\"button\" />\n  )\n  if (flyout) return <DropdownMenuItem asChild disabled={disabled} className=\"focus:bg-accent\">{element}</DropdownMenuItem>\n  if (!icons || nested) return <li>{element}</li>\n  return (\n    <li>\n      <Tooltip>\n        <TooltipTrigger asChild>{element}</TooltipTrigger>\n        <TooltipContent side=\"right\">\n          {children}\n          {badge !== undefined ? <span className=\"ms-2 tabular-nums opacity-70\">{badge}</span> : null}\n        </TooltipContent>\n      </Tooltip>\n    </li>\n  )\n}\n\ninterface SidebarNestedProps {\n  icon?: React.ReactNode\n  label: string\n  /** A page inside is the current one. */\n  active?: boolean\n  defaultOpen?: boolean\n  className?: string\n  children: React.ReactNode\n}\n\n/** An icon has no room for a nested list, so collapsed the children open beside it as a real menu. */\nfunction NestedFlyout({ icon, label, active, className, children }: SidebarNestedProps) {\n  return (\n    <li>\n      <DropdownMenu>\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <DropdownMenuTrigger asChild>\n              <button type=\"button\" data-slot=\"sidebar-nested-trigger\" data-active={active ? \"true\" : undefined} className={cn(ITEM_CLASS, className)}>\n                {icon}\n                <span className=\"shrink-0\">{label}</span>\n              </button>\n            </DropdownMenuTrigger>\n          </TooltipTrigger>\n          <TooltipContent side=\"right\">{label}</TooltipContent>\n        </Tooltip>\n        <DropdownMenuContent side=\"right\" align=\"start\" aria-label={label}>\n          <DropdownMenuLabel>{label}</DropdownMenuLabel>\n          <ItemLevelContext.Provider value={{ nested: true, flyout: true }}>{children}</ItemLevelContext.Provider>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </li>\n  )\n}\n\nfunction NestedList({ id, open, spring, children }: { id: string; open: boolean; spring: boolean; children: React.ReactNode }) {\n  const reduceMotion = useReducedMotion() ?? false\n  const list = (\n    <ul id={id} className=\"ms-5 mt-0.5 grid grid-cols-[minmax(0,1fr)] gap-0.5 border-s ps-2\">\n      <ItemLevelContext.Provider value={{ nested: true, flyout: false }}>{children}</ItemLevelContext.Provider>\n    </ul>\n  )\n  if (!spring) return open ? list : null\n  return (\n    <AnimatePresence initial={false}>\n      {open ? (\n        <motion.div\n          initial={{ height: 0 }}\n          animate={{ height: \"auto\" }}\n          exit={{ height: 0 }}\n          transition={reduceMotion ? { duration: 0 } : SPRING_DEFAULT}\n          className=\"overflow-hidden\"\n        >\n          {list}\n        </motion.div>\n      ) : null}\n    </AnimatePresence>\n  )\n}\n\nfunction SidebarNested(props: SidebarNestedProps) {\n  const { icon, label, active = false, defaultOpen = false, className, children } = props\n  const { icons, effect, variant } = useSidebar()\n  const [open, setOpen] = useState(defaultOpen)\n  const listId = useId()\n  if (icons) return <NestedFlyout {...props} />\n  return (\n    <li>\n      <button\n        type=\"button\"\n        data-slot=\"sidebar-nested-trigger\"\n        data-active={active && !open ? \"true\" : undefined}\n        aria-expanded={open}\n        aria-controls={open ? listId : undefined}\n        className={cn(ITEM_CLASS, variant === \"inset\" && INSET_ACTIVE_CLASS, className)}\n        onClick={() => setOpen(!open)}\n      >\n        {icon}\n        <span className=\"min-w-0 flex-1 truncate text-left\">{label}</span>\n        <ChevronRight aria-hidden=\"true\" className={cn(\"ms-auto text-muted-foreground transition-transform duration-(--duration-fast) ease-(--ease-mise) motion-reduce:transition-none\", open && \"rotate-90\")} />\n      </button>\n      <NestedList id={listId} open={open} spring={effect === \"spring\"}>\n        {children}\n      </NestedList>\n    </li>\n  )\n}\n\nexport { SidebarItem, SidebarNested, type LinkRenderProps, type SidebarItemProps, type SidebarNestedProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/sidebar-state.tsx",
      "content": "import type * as React from \"react\"\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState, useSyncExternalStore } from \"react\"\n\ntype SidebarVariant = \"plain\" | \"floating\" | \"inset\" | \"rail\"\ntype SidebarEffect = \"spring\"\n\nconst PHONE_QUERY = \"(max-width: 767px)\"\n\n/** Either modifier, so the shortcut works without sniffing the platform: Ctrl+B on a Mac does nothing else here. */\nfunction isToggleShortcut(event: Pick<KeyboardEvent, \"key\" | \"metaKey\" | \"ctrlKey\" | \"altKey\" | \"shiftKey\">) {\n  return event.key.toLowerCase() === \"b\" && (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey\n}\n\n/** Mod+B is bold in every text editor, so the sidebar leaves it alone wherever someone is typing. */\nfunction isEditableTarget(target: EventTarget | null) {\n  if (!(target instanceof HTMLElement)) return false\n  return target.isContentEditable || target.tagName === \"INPUT\" || target.tagName === \"TEXTAREA\" || target.tagName === \"SELECT\"\n}\n\nfunction useIsPhone(query = PHONE_QUERY) {\n  return useSyncExternalStore(\n    (notify) => {\n      const media = window.matchMedia(query)\n      media.addEventListener(\"change\", notify)\n      return () => media.removeEventListener(\"change\", notify)\n    },\n    () => window.matchMedia(query).matches,\n    () => false,\n  )\n}\n\ninterface SidebarContextValue {\n  variant: SidebarVariant\n  effect: SidebarEffect | undefined\n  collapsed: boolean\n  /** Showing as a drawer rather than a rail beside the page. */\n  drawer: boolean\n  drawerOpen: boolean\n  setDrawerOpen: (open: boolean) => void\n  /** Icons only: collapsed or `rail`. A drawer always has room for words. */\n  icons: boolean\n  /** The state a press on the toggle can reach. A wide `rail` has none. */\n  canToggle: boolean\n  shortcut: boolean\n  toggle: () => void\n  expanded: boolean\n}\n\nconst SidebarContext = createContext<SidebarContextValue | null>(null)\n\nfunction useSidebar() {\n  const context = useContext(SidebarContext)\n  if (!context) throw new Error(\"useSidebar must be used inside a Sidebar or a SidebarProvider.\")\n  return context\n}\n\ninterface SidebarProviderProps {\n  /** `plain` a full-height column, `floating` a card inset from the edges, `inset` a tinted column around a raised page, `rail` icons only with tooltips. */\n  variant?: SidebarVariant\n  /** `spring` carries the width change on a spring, so a quick second press reverses it. */\n  effect?: SidebarEffect\n  /** Save this wherever you keep preferences and hand it back. The sidebar never touches storage. */\n  collapsed?: boolean\n  defaultCollapsed?: boolean\n  onCollapsedChange?: (collapsed: boolean) => void\n  /** Mod+B toggles. Turn off when two sidebars share a page. */\n  shortcut?: boolean\n  /** Forces drawer mode on or off. Left out, it follows the viewport: phones get a drawer. */\n  mobile?: boolean\n  children?: React.ReactNode\n}\n\nfunction SidebarProvider({ variant = \"plain\", effect, collapsed: collapsedProp, defaultCollapsed = false, onCollapsedChange, shortcut = true, mobile, children }: SidebarProviderProps) {\n  const isPhone = useIsPhone()\n  const drawer = mobile ?? isPhone\n  const [uncontrolled, setUncontrolled] = useState(defaultCollapsed)\n  const [drawerOpen, setDrawerOpen] = useState(false)\n  const collapsed = collapsedProp ?? uncontrolled\n  const canToggle = drawer || variant !== \"rail\"\n\n  const toggle = useCallback(() => {\n    if (drawer) {\n      setDrawerOpen((open) => !open)\n      return\n    }\n    if (variant === \"rail\") return\n    setUncontrolled(!collapsed)\n    onCollapsedChange?.(!collapsed)\n  }, [drawer, variant, collapsed, onCollapsedChange])\n\n  useEffect(() => {\n    if (!shortcut) return\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (!isToggleShortcut(event) || isEditableTarget(event.target)) return\n      event.preventDefault()\n      toggle()\n    }\n    window.addEventListener(\"keydown\", onKeyDown)\n    return () => window.removeEventListener(\"keydown\", onKeyDown)\n  }, [shortcut, toggle])\n\n  const value = useMemo(\n    () => ({\n      variant,\n      effect,\n      collapsed,\n      drawer,\n      drawerOpen,\n      setDrawerOpen,\n      icons: !drawer && (collapsed || variant === \"rail\"),\n      canToggle,\n      shortcut,\n      toggle,\n      expanded: drawer ? drawerOpen : !collapsed,\n    }),\n    [variant, effect, collapsed, drawer, drawerOpen, canToggle, shortcut, toggle],\n  )\n  return <SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>\n}\n\nexport {\n  SidebarContext,\n  SidebarProvider,\n  isEditableTarget,\n  isToggleShortcut,\n  useIsPhone,\n  useSidebar,\n  type SidebarEffect,\n  type SidebarProviderProps,\n  type SidebarVariant,\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "navigation"
  ],
  "type": "registry:ui"
}