{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sidebar-001",
  "title": "Sidebar 001",
  "description": "Animated vertical sidebar with spring hover highlight, active indicator bar, and per-item dash pattern.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/components/unlumen/sidebar-001/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport {\n  createContext,\n  memo,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { motion, AnimatePresence } from \"motion/react\";\nimport { ChevronRight } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nconst MotionChevron = motion.create(ChevronRight);\n\nconst EFFECTS_KEY = \"sidebar-001-effects\";\n\nconst EffectsContext = createContext<{ enabled: boolean; toggle: () => void }>({\n  enabled: true,\n  toggle: () => {},\n});\n\nfunction EffectsProvider({\n  children,\n  defaultEnabled = true,\n}: {\n  children: React.ReactNode;\n  defaultEnabled?: boolean;\n}) {\n  const [enabled, setEnabled] = useState(() => {\n    if (typeof window === \"undefined\") return defaultEnabled;\n    const stored = localStorage.getItem(EFFECTS_KEY);\n    return stored !== null ? stored === \"true\" : defaultEnabled;\n  });\n\n  const toggle = useCallback(() => {\n    setEnabled((prev) => {\n      const next = !prev;\n      localStorage.setItem(EFFECTS_KEY, String(next));\n      return next;\n    });\n  }, []);\n\n  const value = useMemo(() => ({ enabled, toggle }), [enabled, toggle]);\n  return (\n    <EffectsContext.Provider value={value}>{children}</EffectsContext.Provider>\n  );\n}\n\nexport function useSidebar001Effects() {\n  return useContext(EffectsContext);\n}\n\n// ─── Hover context ────────────────────────────────────────────────────────────\n\ninterface HoverRect {\n  top: number;\n  height: number;\n  left: number;\n}\n\nconst HoverContext = createContext<{\n  hovered: string | null;\n  hoverRect: HoverRect | null;\n  containerRef: React.RefObject<HTMLDivElement | null>;\n  setHovered: (id: string | null, rect?: HoverRect | null) => void;\n}>({\n  hovered: null,\n  hoverRect: null,\n  containerRef: { current: null },\n  setHovered: () => {},\n});\n\nfunction HoverProvider({\n  children,\n  containerRef,\n}: {\n  children: React.ReactNode;\n  containerRef: React.RefObject<HTMLDivElement | null>;\n}) {\n  const [hovered, setHoveredId] = useState<string | null>(null);\n  const [hoverRect, setHoverRect] = useState<HoverRect | null>(null);\n\n  const setHovered = useCallback(\n    (id: string | null, rect?: HoverRect | null) => {\n      setHoveredId(id);\n      setHoverRect(rect ?? null);\n    },\n    [],\n  );\n\n  const value = useMemo(\n    () => ({ hovered, hoverRect, containerRef, setHovered }),\n    [hovered, hoverRect, containerRef, setHovered],\n  );\n\n  return (\n    <HoverContext.Provider value={value}>{children}</HoverContext.Provider>\n  );\n}\n\n// ─── Scroll to active ─────────────────────────────────────────────────────────\n\nfunction useScrollToActive(active: boolean) {\n  const ref = useRef<HTMLDivElement>(null);\n  const scrolled = useRef(false);\n\n  useEffect(() => {\n    if (!active || scrolled.current || !ref.current) return;\n    scrolled.current = true;\n    const el = ref.current;\n    const schedule =\n      typeof requestIdleCallback !== \"undefined\"\n        ? (cb: () => void) => requestIdleCallback(cb)\n        : (cb: () => void) => setTimeout(cb, 100);\n    const cancel =\n      typeof cancelIdleCallback !== \"undefined\"\n        ? cancelIdleCallback\n        : clearTimeout;\n    const id = schedule(() => {\n      const viewport = el.closest(\"[data-scroll-viewport]\");\n      if (!(viewport instanceof HTMLElement)) return;\n      const vpRect = viewport.getBoundingClientRect();\n      const elRect = el.getBoundingClientRect();\n      const offset =\n        elRect.top - vpRect.top - vpRect.height / 2 + elRect.height / 2;\n      if (Math.abs(offset) > 40)\n        viewport.scrollBy({ top: offset, behavior: \"smooth\" });\n    });\n    return () => cancel(id as number);\n  }, [active]);\n\n  useEffect(() => {\n    if (!active) scrolled.current = false;\n  }, [active]);\n\n  return ref;\n}\n\n// ─── HoverHighlight ───────────────────────────────────────────────────────────\n\nfunction HoverHighlight() {\n  const { hoverRect, hovered } = useContext(HoverContext);\n  const { enabled } = useContext(EffectsContext);\n\n  return (\n    <AnimatePresence>\n      {enabled && hovered && hoverRect && (\n        <motion.div\n          key=\"sb001-hover-bg\"\n          className=\"pointer-events-none absolute z-0 rounded-md bg-accent/50\"\n          style={{ right: 0 }}\n          initial={false}\n          animate={{\n            top: hoverRect.top + 2,\n            height: hoverRect.height - 4,\n            left: hoverRect.left,\n            opacity: 1,\n          }}\n          exit={{ opacity: 0 }}\n          transition={{ type: \"spring\", stiffness: 300, damping: 30 }}\n        />\n      )}\n    </AnimatePresence>\n  );\n}\n\n// ─── Sidebar001Item ───────────────────────────────────────────────────────────\n\nexport interface Sidebar001ItemProps {\n  href: string;\n  label: React.ReactNode;\n  isActive: boolean;\n  isNew?: boolean;\n  className?: string;\n  onClick?: React.MouseEventHandler<HTMLAnchorElement>;\n}\n\nexport const Sidebar001Item = memo(function Sidebar001Item({\n  href,\n  label,\n  isActive,\n  isNew,\n  className,\n  onClick,\n}: Sidebar001ItemProps) {\n  const { hovered, setHovered, containerRef } = useContext(HoverContext);\n  const isHovered = hovered === href;\n  const itemRef = useScrollToActive(isActive);\n\n  const opacity = isActive\n    ? 1\n    : hovered !== null\n      ? isHovered\n        ? 1\n        : 0.3\n      : 0.55;\n  const x = isActive ? 8 : isHovered ? 6 : 0;\n\n  return (\n    <div className=\"relative\">\n      {isActive && (\n        <motion.span\n          layoutId=\"sb001-active-bar\"\n          className=\"pointer-events-none absolute z-10 left-[4px] top-1/2 h-[1.8px] -translate-y-1/2 rounded-full bg-accent-pro\"\n          animate={{ width: 23 }}\n          transition={{ type: \"spring\", stiffness: 800, damping: 40 }}\n        />\n      )}\n\n      <motion.span\n        className=\"pointer-events-none absolute left-0 top-1/2 -translate-y-1/2 h-px bg-foreground/50\"\n        animate={{ width: isActive ? 0 : isHovered ? 26 : 18 }}\n        transition={{ type: \"spring\", stiffness: 600, damping: 30 }}\n      />\n      <motion.span className=\"pointer-events-none absolute w-[13px] left-0 top-1/4 h-px bg-foreground/30\" />\n      <motion.span className=\"pointer-events-none absolute w-[16px] left-0 top-0 h-px bg-foreground/30\" />\n      <motion.span className=\"pointer-events-none absolute w-[13px] left-0 top-3/4 h-px bg-foreground/30\" />\n\n      <motion.div\n        ref={itemRef}\n        animate={{ opacity, x }}\n        transition={{ type: \"spring\", stiffness: 700, damping: 30 }}\n        style={{ transformOrigin: \"left center\" }}\n      >\n        <a\n          href={href}\n          onClick={onClick}\n          onMouseEnter={() => {\n            const el = itemRef.current;\n            const container = containerRef.current;\n            if (el && container) {\n              const elRect = el.getBoundingClientRect();\n              const containerRect = container.getBoundingClientRect();\n              setHovered(href, {\n                top: elRect.top - containerRect.top,\n                height: elRect.height,\n                left: 25,\n              });\n            } else {\n              setHovered(href);\n            }\n          }}\n          onMouseLeave={() => setHovered(null)}\n          className={cn(\n            \"relative flex items-center gap-2 ml-2 pl-4 py-1.5 text-sm select-none\",\n            className,\n          )}\n        >\n          <span className=\"relative z-1 truncate\">{label}</span>\n          {isNew && (\n            <span className=\"size-1.5 rounded-full bg-accent-pro shrink-0\" />\n          )}\n        </a>\n      </motion.div>\n    </div>\n  );\n});\n\n// ─── Sidebar001Separator ──────────────────────────────────────────────────────\n\nexport function Sidebar001Separator({\n  children,\n  className,\n}: {\n  children?: React.ReactNode;\n  className?: string;\n}) {\n  return (\n    <div\n      className={cn(\n        \"px-0 py-3.5 mt-2 text-sm font-medium text-foreground/40\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n\n// ─── Sidebar001Group ──────────────────────────────────────────────────────────\n\nexport interface Sidebar001GroupProps {\n  label: React.ReactNode;\n  children: React.ReactNode;\n  defaultOpen?: boolean;\n  icon?: React.ReactNode;\n  className?: string;\n}\n\nexport function Sidebar001Group({\n  label,\n  children,\n  defaultOpen = false,\n  icon,\n  className,\n}: Sidebar001GroupProps) {\n  const [isOpen, setIsOpen] = useState(false);\n  const id = useId();\n  const { setHovered, containerRef } = useContext(HoverContext);\n  const buttonRef = useRef<HTMLButtonElement>(null);\n\n  useEffect(() => {\n    setIsOpen(defaultOpen);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const handleMouseEnter = useCallback(() => {\n    const el = buttonRef.current;\n    const container = containerRef.current;\n    if (el && container) {\n      const elRect = el.getBoundingClientRect();\n      const containerRect = container.getBoundingClientRect();\n      setHovered(id, {\n        top: elRect.top - containerRect.top,\n        height: elRect.height,\n        left: 0,\n      });\n    } else {\n      setHovered(id);\n    }\n  }, [id, setHovered, containerRef]);\n\n  const handleMouseLeave = useCallback(() => {\n    setHovered(null);\n  }, [setHovered]);\n\n  return (\n    <div className={cn(\"flex flex-col\", className)}>\n      <button\n        ref={buttonRef}\n        type=\"button\"\n        onClick={() => setIsOpen((v) => !v)}\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n        className=\"relative z-1 flex items-center gap-1.5 py-1.5 pr-2 select-none text-left w-full group\"\n      >\n        {icon ? (\n          <>\n            <span className=\"shrink-0 text-foreground/35 [&_svg]:size-3.5\">\n              {icon}\n            </span>\n            <span className=\"text-sm text-foreground/45 group-hover:text-foreground/70 transition-colors duration-150 flex-1\">\n              {label}\n            </span>\n            <MotionChevron\n              size={14}\n              strokeWidth={2.5}\n              className=\"shrink-0 text-foreground/25 mr-1\"\n              animate={{ rotate: isOpen ? 90 : 0 }}\n              transition={{ type: \"spring\", stiffness: 500, damping: 30 }}\n            />\n          </>\n        ) : (\n          <>\n            <MotionChevron\n              size={11}\n              strokeWidth={2.5}\n              className=\"shrink-0 text-foreground/35\"\n              animate={{ rotate: isOpen ? 90 : 0 }}\n              transition={{ type: \"spring\", stiffness: 500, damping: 30 }}\n            />\n            <span className=\"text-sm text-foreground/45 group-hover:text-foreground/70 transition-colors duration-150\">\n              {label}\n            </span>\n          </>\n        )}\n      </button>\n\n      <AnimatePresence initial={false}>\n        {isOpen && (\n          <motion.div\n            initial={{ height: 0, opacity: 0 }}\n            animate={{ height: \"auto\", opacity: 1 }}\n            exit={{ height: 0, opacity: 0 }}\n            transition={{ type: \"spring\", stiffness: 420, damping: 34 }}\n            style={{ overflow: \"hidden\" }}\n          >\n            <div className=\"flex flex-col pl-3\">{children}</div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\n// ─── Sidebar001Section ────────────────────────────────────────────────────────\n\nexport function Sidebar001Section({\n  label,\n  children,\n  className,\n}: {\n  label?: React.ReactNode;\n  children: React.ReactNode;\n  className?: string;\n}) {\n  return (\n    <div className={cn(\"flex flex-col\", className)}>\n      {label && <Sidebar001Separator>{label}</Sidebar001Separator>}\n      {children}\n    </div>\n  );\n}\n\n// ─── Sidebar001Content ────────────────────────────────────────────────────────\n\nexport function Sidebar001Content({\n  children,\n  className,\n}: {\n  children: React.ReactNode;\n  className?: string;\n}) {\n  const containerRef = useContext(HoverContext).containerRef;\n\n  return (\n    <div\n      className={cn(\"flex-1 overflow-y-auto py-4 no-scrollbar\", className)}\n      data-scroll-viewport\n    >\n      <div ref={containerRef} className=\"relative px-1\">\n        <HoverHighlight />\n        {children}\n      </div>\n    </div>\n  );\n}\n\n// ─── Sidebar001 (with resize) ─────────────────────────────────────────────────\n\nexport interface Sidebar001Props {\n  children: React.ReactNode;\n  className?: string;\n  defaultEffectsEnabled?: boolean;\n  /** Initial width in px. Default: 240 */\n  defaultWidth?: number;\n  /** Min resize width in px. Default: 160 */\n  minWidth?: number;\n  /** Max resize width in px. Default: 400 */\n  maxWidth?: number;\n}\n\nexport function Sidebar001({\n  children,\n  className,\n  defaultEffectsEnabled = true,\n  defaultWidth = 240,\n  minWidth = 160,\n  maxWidth = 400,\n}: Sidebar001Props) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [width, setWidth] = useState(defaultWidth);\n  const dragging = useRef(false);\n  const startX = useRef(0);\n  const startW = useRef(0);\n\n  const onPointerDown = useCallback(\n    (e: React.PointerEvent) => {\n      e.preventDefault();\n      dragging.current = true;\n      startX.current = e.clientX;\n      startW.current = width;\n      (e.target as HTMLElement).setPointerCapture(e.pointerId);\n    },\n    [width],\n  );\n\n  const onPointerMove = useCallback(\n    (e: React.PointerEvent) => {\n      if (!dragging.current) return;\n      const next = Math.min(\n        maxWidth,\n        Math.max(minWidth, startW.current + e.clientX - startX.current),\n      );\n      setWidth(next);\n    },\n    [minWidth, maxWidth],\n  );\n\n  const onPointerUp = useCallback(() => {\n    dragging.current = false;\n  }, []);\n\n  return (\n    <EffectsProvider defaultEnabled={defaultEffectsEnabled}>\n      <HoverProvider containerRef={containerRef}>\n        <aside\n          className={cn(\n            \"relative flex flex-col h-full shrink-0 bg-background\",\n            className,\n          )}\n          style={{ width }}\n        >\n          {children}\n\n          {/* Resize handle */}\n          <div\n            className=\"absolute top-0 right-0 h-full w-1 cursor-col-resize group/handle z-20\"\n            onPointerDown={onPointerDown}\n            onPointerMove={onPointerMove}\n            onPointerUp={onPointerUp}\n          >\n            <div className=\"absolute right-0 top-0 h-full w-px bg-border/50 group-hover/handle:bg-border transition-colors duration-150\" />\n          </div>\n        </aside>\n      </HoverProvider>\n    </EffectsProvider>\n  );\n}\n\n// ─── Sidebar001Header ─────────────────────────────────────────────────────────\n\nexport function Sidebar001Header({\n  children,\n  className,\n}: {\n  children?: React.ReactNode;\n  className?: string;\n}) {\n  return (\n    <div className={cn(\"shrink-0 px-3 pt-4 pb-2\", className)}>{children}</div>\n  );\n}\n\n// ─── Sidebar001Footer ─────────────────────────────────────────────────────────\n\nexport function Sidebar001Footer({\n  children,\n  className,\n}: {\n  children?: React.ReactNode;\n  className?: string;\n}) {\n  return (\n    <div\n      className={cn(\n        \"shrink-0 px-3 pb-4 pt-2 border-t border-border/50\",\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/sidebar-001.tsx"
    }
  ],
  "type": "registry:ui"
}