{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sidebar-rhs",
  "title": "Sidebar RHS",
  "description": "A right-hand side sidebar that can be collapsed",
  "dependencies": [
    "@mdi/js",
    "@dnd-kit/utilities"
  ],
  "registryDependencies": [
    "https://blok.sitecore.com/r/theme.json",
    "https://blok.sitecore.com/r/button.json",
    "https://blok.sitecore.com/r/draggable.json"
  ],
  "files": [
    {
      "path": "src/components/bloks/sidebar-rhs.tsx",
      "content": "\"use client\";\n\nimport { Icon } from \"@/lib/icon\";\nimport { mdiChevronRight, mdiClose, mdiWindowRestore } from \"@mdi/js\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DndContext,\n  type DragEndEvent,\n  type DragStartEvent,\n} from \"@/components/ui/dnd-context\";\nimport { useDraggable } from \"@/components/ui/draggable\";\nimport { cn } from \"@/lib/utils\";\nimport { CSS } from \"@dnd-kit/utilities\";\n\ntype SidebarRHSContextProps = {\n  isCollapsed: boolean;\n  isDocked: boolean;\n  toggleCollapse: () => void;\n  toggleDock: () => void;\n  open: boolean;\n  setOpen: (open: boolean) => void;\n};\n\nconst SidebarRHSContext = createContext<SidebarRHSContextProps | null>(null);\n\nexport function useSidebarRHS() {\n  const context = useContext(SidebarRHSContext);\n  if (!context) {\n    throw new Error(\"useSidebarRHS must be used within a SidebarRHSProvider.\");\n  }\n  return context;\n}\n\nexport interface SidebarRHSProviderProps {\n  /** Initial collapsed state */\n  defaultCollapsed?: boolean;\n  /** Initial docked state */\n  defaultDocked?: boolean;\n  /** Controlled collapsed state */\n  collapsed?: boolean;\n  /** Controlled docked state */\n  docked?: boolean;\n  /** Callback when collapsed state changes */\n  onCollapsedChange?: (collapsed: boolean) => void;\n  /** Callback when docked state changes */\n  onDockedChange?: (docked: boolean) => void;\n  /** Children */\n  children: ReactNode;\n}\n\nexport function SidebarRHSProvider({\n  defaultCollapsed = false,\n  defaultDocked = true,\n  collapsed: controlledCollapsed,\n  docked: controlledDocked,\n  onCollapsedChange,\n  onDockedChange,\n  children,\n}: SidebarRHSProviderProps) {\n  const [internalCollapsed, setInternalCollapsed] = useState(defaultCollapsed);\n  const [internalDocked, setInternalDocked] = useState(defaultDocked);\n\n  const isCollapsed = controlledCollapsed ?? internalCollapsed;\n  const isDocked = controlledDocked ?? internalDocked;\n  const open = !isCollapsed;\n\n  const toggleCollapse = useCallback(() => {\n    const newCollapsed = !isCollapsed;\n    if (controlledCollapsed === undefined) {\n      setInternalCollapsed(newCollapsed);\n    }\n    onCollapsedChange?.(newCollapsed);\n  }, [isCollapsed, controlledCollapsed, onCollapsedChange]);\n\n  const toggleDock = useCallback(() => {\n    const newDocked = !isDocked;\n    if (controlledDocked === undefined) {\n      setInternalDocked(newDocked);\n    }\n    onDockedChange?.(newDocked);\n  }, [isDocked, controlledDocked, onDockedChange]);\n\n  const setOpen = useCallback(\n    (open: boolean) => {\n      const newCollapsed = !open;\n      if (controlledCollapsed === undefined) {\n        setInternalCollapsed(newCollapsed);\n      }\n      onCollapsedChange?.(newCollapsed);\n    },\n    [controlledCollapsed, onCollapsedChange],\n  );\n\n  const contextValue = useMemo<SidebarRHSContextProps>(\n    () => ({\n      isCollapsed,\n      isDocked,\n      toggleCollapse,\n      toggleDock,\n      open,\n      setOpen,\n    }),\n    [isCollapsed, isDocked, toggleCollapse, toggleDock, open, setOpen],\n  );\n\n  return (\n    <SidebarRHSContext.Provider value={contextValue}>\n      {children}\n    </SidebarRHSContext.Provider>\n  );\n}\n\nexport interface SidebarRHSTriggerProps {\n  /** Additional className */\n  className?: string;\n  /** Additional style */\n  style?: React.CSSProperties;\n  /** Children (optional, defaults to chevron icon) */\n  children?: ReactNode;\n}\n\nexport function SidebarRHSTrigger({\n  className,\n  style,\n  children,\n  ...props\n}: SidebarRHSTriggerProps & ComponentProps<\"button\">) {\n  const { toggleCollapse, isCollapsed } = useSidebarRHS();\n\n  return (\n    <Button\n      {...props}\n      variant=\"outline\"\n      size=\"icon\"\n      className={cn(\n        \"h-8 w-8 rounded-full shadow-lg bg-white border-border hover:bg-gray-50\",\n        className,\n      )}\n      style={style}\n      aria-label={isCollapsed ? \"Expand sidebar\" : \"Collapse sidebar\"}\n      onClick={toggleCollapse}\n    >\n      {children || (\n        <Icon\n          path={mdiChevronRight}\n          size={0.8}\n          className={cn(\n            \"transition-transform text-black\",\n            isCollapsed && \"rotate-180\",\n          )}\n        />\n      )}\n    </Button>\n  );\n}\n\nexport interface SidebarRHSProps {\n  /** Title shown in the sidebar header */\n  title?: string;\n  /** Custom header content (overrides title if provided) */\n  header?: ReactNode;\n  /** Content to display in the sidebar body (below the header) */\n  children?: ReactNode;\n  /** Main content area (shown on the left) */\n  mainContent?: ReactNode;\n  /** Width of the sidebar when expanded */\n  width?: string;\n  /** Height of the container */\n  height?: string;\n  /** Minimum width of the sidebar */\n  minWidth?: string;\n  /** Maximum width of the sidebar */\n  maxWidth?: string;\n  /** Callback when width changes */\n  onWidthChange?: (width: string) => void;\n  /** Additional className on the sidebar root */\n  className?: string;\n  /** ClassName for the header region */\n  headerClassName?: string;\n  /** ClassName for the scrollable content region below the header */\n  contentClassName?: string;\n  /** Enable collapsible functionality */\n  collapsible?: boolean;\n  /**\n   * Shows dock/undock controls (pop-out to a floating, draggable panel).\n   * When false, pop-out UI is hidden only — docked vs undocked logic stays in the component.\n   * Set to true to re-enable without further changes.\n   */\n  dockable?: boolean;\n  /** Show left-edge resize handle and hover stroke (defaults to match collapsible) */\n  resizable?: boolean;\n}\n\nfunction SidebarResizeHandle({\n  onMouseDown,\n}: {\n  onMouseDown: (e: React.MouseEvent) => void;\n}) {\n  return (\n    <div\n      className=\"absolute left-0 top-0 bottom-0 w-2 cursor-ew-resize z-20 group/resize\"\n      onMouseDown={onMouseDown}\n      role=\"separator\"\n      aria-label=\"Resize sidebar\"\n      aria-orientation=\"vertical\"\n    >\n      <div className=\"absolute inset-y-0 left-0 w-px bg-transparent group-hover/resize:bg-primary transition-colors\" />\n    </div>\n  );\n}\n\nfunction SidebarRHSInner({\n  header,\n  title,\n  children,\n  dockable,\n  headerClassName,\n  contentClassName,\n  isStackedNavHeader,\n}: {\n  header?: ReactNode;\n  title?: string;\n  children?: ReactNode;\n  dockable?: boolean;\n  headerClassName?: string;\n  contentClassName?: string;\n  isStackedNavHeader: boolean;\n}) {\n  return (\n    <>\n      <div\n        className={cn(\n          \"shrink-0 flex items-center justify-between w-full gap-2\",\n          isStackedNavHeader\n            ? \"sticky top-0 z-10 bg-body-bg pt-4 pb-2 px-6\"\n            : \"h-12 px-4\",\n          headerClassName,\n        )}\n      >\n        <div className={cn(isStackedNavHeader && \"flex-1 min-w-0 w-full\")}>\n          {header ||\n            (title && <h2 className=\"text-lg font-semibold\">{title}</h2>)}\n        </div>\n        <DockButton show={dockable} />\n      </div>\n\n      <div\n        className={cn(\n          \"flex-1 min-h-0 overflow-auto\",\n          isStackedNavHeader ? \"px-6 py-4\" : \"p-4\",\n          contentClassName,\n        )}\n      >\n        {children}\n      </div>\n    </>\n  );\n}\n\n// Helper function to parse width string to pixels\nfunction parseWidth(width: string, containerWidth: number): number {\n  if (width.endsWith(\"px\")) {\n    return Number.parseInt(width, 10);\n  }\n  if (width.endsWith(\"%\")) {\n    return (Number.parseInt(width, 10) / 100) * containerWidth;\n  }\n  return Number.parseInt(width, 10) || 320;\n}\n\n// Helper function to format pixels to string\nfunction formatWidth(pixels: number): string {\n  return `${pixels}px`;\n}\n\nfunction DockButton({ show }: { show?: boolean }) {\n  const { toggleDock } = useSidebarRHS();\n  if (!show) return null;\n  return (\n    <div className=\"ml-auto flex items-center gap-2\">\n      <Button\n        variant=\"ghost\"\n        size=\"icon\"\n        colorScheme=\"neutral\"\n        aria-label=\"Undock sidebar\"\n        onClick={toggleDock}\n        className=\"h-7 w-7\"\n      >\n        <Icon path={mdiWindowRestore} size={0.9} />\n      </Button>\n    </div>\n  );\n}\n\nfunction UndockButton({ show }: { show?: boolean }) {\n  const { toggleDock } = useSidebarRHS();\n  if (!show) return null;\n  return (\n    <div className=\"ml-auto flex items-center gap-2\">\n      <Button\n        variant=\"ghost\"\n        size=\"icon\"\n        colorScheme=\"neutral\"\n        aria-label=\"Dock sidebar\"\n        onClick={toggleDock}\n        className=\"h-7 w-7\"\n      >\n        <Icon path={mdiClose} size={0.9} />\n      </Button>\n    </div>\n  );\n}\n\nexport function SidebarRHS({\n  title,\n  header,\n  children,\n  mainContent,\n  width = \"320px\",\n  height = \"600px\",\n  minWidth = \"200px\",\n  maxWidth = \"800px\",\n  onWidthChange,\n  className,\n  headerClassName,\n  contentClassName,\n  collapsible = false,\n  dockable = false,\n  resizable: resizableProp,\n  ...props\n}: SidebarRHSProps & ComponentProps<\"div\">) {\n  const isStackedNavHeader = !!header;\n  const resizable = resizableProp ?? collapsible;\n  const { isCollapsed, isDocked } = useSidebarRHS();\n  const [currentWidth, setCurrentWidth] = useState(width);\n  const [isResizing, setIsResizing] = useState(false);\n  const [transitionDuration, setTransitionDuration] = useState<\"200\" | \"500\">(\n    \"200\",\n  );\n  const [shouldHideContent, setShouldHideContent] = useState(isCollapsed);\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n  const containerRef = useRef<HTMLDivElement>(null);\n  const startXRef = useRef<number>(0);\n  const startWidthRef = useRef<number>(0);\n  const dragStartPositionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });\n  const draggableElementRef = useRef<HTMLDivElement | null>(null);\n\n  // Sync shouldHideContent with isCollapsed\n  useEffect(() => {\n    if (isCollapsed) {\n      // Collapsing - use slower animation (500ms)\n      setTransitionDuration(\"500\");\n      setTimeout(() => {\n        setShouldHideContent(true);\n      }, 500);\n    } else {\n      // Expanding - use faster animation (200ms)\n      setTransitionDuration(\"200\");\n      setShouldHideContent(false);\n    }\n\n    // Reset transition duration after animation completes\n    setTimeout(\n      () => {\n        setTransitionDuration(\"200\");\n      },\n      isCollapsed ? 500 : 200,\n    );\n  }, [isCollapsed]);\n\n  // Handle resize start\n  const handleResizeStart = useCallback(\n    (e: React.MouseEvent) => {\n      if (isCollapsed) return;\n      e.preventDefault();\n      setIsResizing(true);\n      startXRef.current = e.clientX;\n      if (containerRef.current) {\n        const containerWidth = containerRef.current.offsetWidth;\n        startWidthRef.current = parseWidth(currentWidth, containerWidth);\n      }\n    },\n    [isCollapsed, currentWidth],\n  );\n\n  // Handle resize move\n  useEffect(() => {\n    if (!isResizing) return;\n\n    const handleMouseMove = (e: MouseEvent) => {\n      if (!containerRef.current) return;\n\n      const containerWidth = containerRef.current.offsetWidth;\n      const deltaX = startXRef.current - e.clientX; // Negative because we're resizing from right\n      const newWidth = startWidthRef.current + deltaX;\n\n      const minPixels = parseWidth(minWidth, containerWidth);\n      const maxPixels = parseWidth(maxWidth, containerWidth);\n\n      const clampedWidth = Math.max(minPixels, Math.min(maxPixels, newWidth));\n      const newWidthString = formatWidth(clampedWidth);\n\n      setCurrentWidth(newWidthString);\n      onWidthChange?.(newWidthString);\n    };\n\n    const handleMouseUp = () => {\n      setIsResizing(false);\n    };\n\n    document.addEventListener(\"mousemove\", handleMouseMove);\n    document.addEventListener(\"mouseup\", handleMouseUp);\n    document.body.style.cursor = \"ew-resize\";\n    document.body.style.userSelect = \"none\";\n\n    return () => {\n      document.removeEventListener(\"mousemove\", handleMouseMove);\n      document.removeEventListener(\"mouseup\", handleMouseUp);\n      document.body.style.cursor = \"\";\n      document.body.style.userSelect = \"\";\n    };\n  }, [isResizing, minWidth, maxWidth, onWidthChange]);\n\n  // Helper to constrain position to viewport\n  const constrainPosition = useCallback(\n    (x: number, y: number) => {\n      const maxX =\n        window.innerWidth - parseWidth(currentWidth, window.innerWidth);\n      const maxY = window.innerHeight - 100; // Leave some space at bottom\n\n      return {\n        x: Math.max(0, Math.min(maxX, x)),\n        y: Math.max(0, Math.min(maxY, y)),\n      };\n    },\n    [currentWidth],\n  );\n\n  // Handle drag for undocked sidebar using @dnd-kit\n  const handleDragStartUndocked = useCallback(\n    // biome-ignore lint/correctness/noUnusedVariables: DragStartEvent param required by @dnd-kit; position read from DOM instead of event\n    (event: DragStartEvent) => {\n      // Get the actual element position from the DOM\n      const element = draggableElementRef.current;\n      if (element) {\n        const rect = element.getBoundingClientRect();\n        // Always capture the actual position from DOM to prevent glitch\n        // This ensures we're using left/top positioning even if element was using right/bottom\n        const left = rect.left;\n        const top = rect.top;\n\n        // Use the actual DOM position as the starting point for the drag\n        dragStartPositionRef.current = { x: left, y: top };\n\n        // Only update state if position wasn't already set (to avoid unnecessary updates)\n        const hasPosition = position.x !== 0 || position.y !== 0;\n        if (!hasPosition) {\n          setPosition({ x: left, y: top });\n        }\n      } else {\n        dragStartPositionRef.current = { ...position };\n      }\n    },\n    [position],\n  );\n\n  const handleDragEndUndocked = useCallback(\n    (event: DragEndEvent) => {\n      const { delta } = event;\n      if (delta) {\n        const newX = dragStartPositionRef.current.x + delta.x;\n        const newY = dragStartPositionRef.current.y + delta.y;\n\n        const constrained = constrainPosition(newX, newY);\n        setPosition(constrained);\n      }\n    },\n    [constrainPosition],\n  );\n\n  // When docked, render sidebar\n  if (isDocked) {\n    // If mainContent is provided, use the old pattern (backward compatible)\n    if (mainContent) {\n      return (\n        <div\n          ref={containerRef}\n          {...props}\n          className={cn(\n            \"relative w-full flex border border-border rounded-lg overflow-hidden bg-body-bg\",\n            className,\n          )}\n          style={{ height }}\n        >\n          {/* Main content area */}\n          <div className=\"flex-1 overflow-auto p-4\">{mainContent}</div>\n\n          {/* Sidebar */}\n          <div\n            className={cn(\n              \"relative bg-body-bg shrink-0 overflow-x-visible\",\n              !isResizing && \"transition-all ease-in-out\",\n              !isResizing && transitionDuration === \"500\" && \"duration-500\",\n              !isResizing && transitionDuration === \"200\" && \"duration-200\",\n            )}\n            style={{\n              width: isCollapsed ? \"0\" : currentWidth,\n            }}\n          >\n            {resizable && !shouldHideContent && (\n              <SidebarResizeHandle onMouseDown={handleResizeStart} />\n            )}\n\n            {collapsible && (\n              <SidebarRHSTrigger\n                className=\"absolute top-1/2 -translate-y-1/2 z-30\"\n                style={{\n                  left: isCollapsed ? \"-40px\" : \"-20px\",\n                }}\n              />\n            )}\n\n            <div\n              className={cn(\n                \"flex h-full flex-col w-full overflow-hidden\",\n                shouldHideContent && \"opacity-0 pointer-events-none\",\n                !shouldHideContent &&\n                  \"opacity-100 transition-opacity duration-200\",\n              )}\n            >\n              <SidebarRHSInner\n                header={header}\n                title={title}\n                dockable={dockable}\n                headerClassName={headerClassName}\n                contentClassName={contentClassName}\n                isStackedNavHeader={isStackedNavHeader}\n              >\n                {children}\n              </SidebarRHSInner>\n            </div>\n          </div>\n        </div>\n      );\n    }\n\n    // New pattern: just render the sidebar itself (no container)\n    return (\n      <div\n        ref={containerRef}\n        {...props}\n        className={cn(\n          \"relative bg-body-bg shrink-0 overflow-x-visible\",\n          !isResizing && \"transition-all ease-in-out\",\n          !isResizing && transitionDuration === \"500\" && \"duration-500\",\n          !isResizing && transitionDuration === \"200\" && \"duration-200\",\n          className,\n        )}\n        style={{\n          width: isCollapsed ? \"0\" : currentWidth,\n          height: height,\n        }}\n      >\n        {resizable && !shouldHideContent && (\n          <SidebarResizeHandle onMouseDown={handleResizeStart} />\n        )}\n\n        {collapsible && (\n          <SidebarRHSTrigger\n            className=\"absolute top-1/2 -translate-y-1/2 z-30\"\n            style={{\n              left: isCollapsed ? \"-40px\" : \"-20px\",\n            }}\n          />\n        )}\n\n        <div\n          className={cn(\n            \"flex h-full flex-col w-full overflow-hidden\",\n            shouldHideContent && \"opacity-0 pointer-events-none\",\n            !shouldHideContent && \"opacity-100 transition-opacity duration-200\",\n          )}\n        >\n          <SidebarRHSInner\n            header={header}\n            title={title}\n            dockable={dockable}\n            headerClassName={headerClassName}\n            contentClassName={contentClassName}\n            isStackedNavHeader={isStackedNavHeader}\n          >\n            {children}\n          </SidebarRHSInner>\n        </div>\n      </div>\n    );\n  }\n\n  // Draggable sidebar component for undocked state\n  function DraggableSidebarContent() {\n    const { attributes, listeners, setNodeRef, transform, isDragging } =\n      useDraggable({\n        id: \"sidebar-rhs-undocked\",\n        disabled: false,\n      });\n\n    // Store ref for drag start position calculation\n    const combinedRef = useCallback(\n      (node: HTMLDivElement | null) => {\n        draggableElementRef.current = node;\n        setNodeRef(node);\n      },\n      [setNodeRef],\n    );\n\n    // Use transform during drag for smooth, glitch-free movement\n    // Only update position state on dragEnd to avoid re-render glitches\n    const hasPosition = position.x !== 0 || position.y !== 0;\n\n    const style: React.CSSProperties = {\n      width: currentWidth,\n      height: \"calc(100vh - 2rem)\",\n      maxHeight: \"calc(100vh - 2rem)\",\n      left: hasPosition ? position.x : undefined,\n      top: hasPosition ? position.y : undefined,\n      right: !hasPosition ? \"1rem\" : undefined,\n      bottom: !hasPosition ? \"1rem\" : undefined,\n      // Transform is applied by @dnd-kit during drag for smooth movement\n      // This is GPU-accelerated and doesn't cause re-renders\n      transform: transform ? CSS.Translate.toString(transform) : undefined,\n    };\n\n    return (\n      <div\n        ref={combinedRef}\n        {...props}\n        className={cn(\n          \"fixed z-50 bg-body-bg border border-border rounded-lg shadow-lg overflow-hidden\",\n          // Disable transitions during drag and resize for instant movement\n          !isResizing && !isDragging && \"transition-all ease-in-out\",\n          !isResizing &&\n            !isDragging &&\n            transitionDuration === \"500\" &&\n            \"duration-500\",\n          !isResizing &&\n            !isDragging &&\n            transitionDuration === \"200\" &&\n            \"duration-200\",\n          isDragging && \"cursor-grabbing opacity-90\",\n          className,\n        )}\n        style={style}\n      >\n        <div className=\"flex h-full flex-col min-h-0\">\n          <div\n            className={cn(\n              \"shrink-0 flex items-center justify-between w-full cursor-grab active:cursor-grabbing\",\n              isStackedNavHeader\n                ? \"sticky top-0 z-10 bg-body-bg pt-4 pb-2 px-6\"\n                : \"h-12 px-4\",\n              headerClassName,\n            )}\n            {...listeners}\n            {...attributes}\n          >\n            {header ||\n              (title && (\n                <h2 className=\"text-lg font-semibold select-none\">{title}</h2>\n              ))}\n            <UndockButton show={dockable} />\n          </div>\n\n          <div\n            className={cn(\n              \"flex-1 min-h-0 overflow-auto\",\n              isStackedNavHeader ? \"px-6 py-4\" : \"p-4\",\n              contentClassName,\n            )}\n          >\n            {children}\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  // When undocked, render as floating draggable sidebar\n  if (!isDocked) {\n    return typeof window !== \"undefined\"\n      ? createPortal(\n          <DndContext\n            onDragStart={handleDragStartUndocked}\n            onDragEnd={handleDragEndUndocked}\n          >\n            <DraggableSidebarContent />\n          </DndContext>,\n          document.body,\n        )\n      : null;\n  }\n\n  return null;\n}\n\n// Default export for backward compatibility\nexport default function SidebarRHSWrapper(\n  props: SidebarRHSProps & {\n    defaultCollapsed?: boolean;\n    defaultDocked?: boolean;\n    onCollapsedChange?: (collapsed: boolean) => void;\n    onDockedChange?: (docked: boolean) => void;\n  },\n) {\n  const {\n    defaultCollapsed,\n    defaultDocked,\n    onCollapsedChange,\n    onDockedChange,\n    ...sidebarProps\n  } = props;\n\n  return (\n    <SidebarRHSProvider\n      defaultCollapsed={defaultCollapsed}\n      defaultDocked={defaultDocked}\n      onCollapsedChange={onCollapsedChange}\n      onDockedChange={onDockedChange}\n    >\n      <SidebarRHS {...sidebarProps} />\n    </SidebarRHSProvider>\n  );\n}\n",
      "type": "registry:block",
      "target": "src/components/bloks/sidebar-rhs.tsx"
    }
  ],
  "type": "registry:block"
}