{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "splitter",
  "type": "registry:ui",
  "title": "Splitter",
  "description": "A versatile splitter component for creating resizable panels with smooth animations.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["motion", "lucide-react", "clsx", "tailwind-merge"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/splitter.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { motion, AnimatePresence, HTMLMotionProps } from 'motion/react';\n\nimport { cn } from '@/lib/utils';\nimport { PanelLeftClose, PanelRightClose } from 'lucide-react';\n\ninterface SplitterProps extends Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  'onResize'\n> {\n  onResize?: (leftSize: number, rightSize: number) => void;\n  allowFullCollapse?: boolean;\n  minSize?: number;\n  maxSize?: number;\n  defaultSize?: number;\n  snapThreshold?: number;\n  smoothTransition?: boolean;\n}\n\ntype SplitterPanelProps = React.HTMLAttributes<HTMLDivElement>;\n\ninterface SplitterHandleProps {\n  className?: string;\n  withHandle?: boolean;\n  onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;\n  onTouchStart?: (e: React.TouchEvent<HTMLDivElement>) => void;\n  isDragging?: boolean;\n  isLeftCollapsed?: boolean;\n  isRightCollapsed?: boolean;\n  isLeftNearCollapse?: boolean;\n  isRightNearCollapse?: boolean;\n}\n\nfunction Splitter({\n  className,\n  children,\n  onResize,\n  style,\n  allowFullCollapse = true,\n  minSize = 0,\n  maxSize = 100,\n  defaultSize = 50,\n  snapThreshold = 5,\n  smoothTransition = true,\n  ...props\n}: SplitterProps) {\n  const [leftWidth, setLeftWidth] = React.useState<number>(defaultSize);\n  const [isDragging, setIsDragging] = React.useState<boolean>(false);\n  const [startPosition, setStartPosition] = React.useState<number>(0);\n  const [startWidth, setStartWidth] = React.useState<number>(0);\n  const containerRef = React.useRef<HTMLDivElement | null>(null);\n  const animationFrameRef = React.useRef<number | undefined>(undefined);\n\n  React.useEffect(() => {\n    if (containerRef.current) {\n      const computedStyle = getComputedStyle(containerRef.current);\n      const cssDefaultSize = parseFloat(\n        computedStyle.getPropertyValue('--splitter-default-size'),\n      );\n      const initialSize = !isNaN(cssDefaultSize) ? cssDefaultSize : defaultSize;\n      setLeftWidth(initialSize);\n    }\n  }, [defaultSize]);\n\n  const handleInteractionStart = React.useCallback(\n    (clientX: number): void => {\n      if (!containerRef.current) return;\n\n      setIsDragging(true);\n      setStartPosition(clientX);\n      setStartWidth(leftWidth);\n\n      document.body.style.userSelect = 'none';\n      document.body.style.webkitUserSelect = 'none';\n      document.body.style.touchAction = 'none';\n      document.body.style.cursor = 'col-resize';\n\n      containerRef.current.classList.add('splitter-active');\n    },\n    [leftWidth],\n  );\n\n  const handleMouseDown = React.useCallback(\n    (e: React.MouseEvent<HTMLDivElement>): void => {\n      e.preventDefault();\n      e.stopPropagation();\n      handleInteractionStart(e.clientX);\n    },\n    [handleInteractionStart],\n  );\n\n  const handleTouchStart = React.useCallback(\n    (e: React.TouchEvent<HTMLDivElement>): void => {\n      e.preventDefault();\n      e.stopPropagation();\n      if (e.touches.length === 1) {\n        handleInteractionStart(e.touches[0].clientX);\n      }\n    },\n    [handleInteractionStart],\n  );\n\n  const handleMove = React.useCallback(\n    (clientX: number): void => {\n      if (!isDragging || !containerRef.current) return;\n\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current);\n      }\n\n      animationFrameRef.current = requestAnimationFrame(() => {\n        if (!containerRef.current) return;\n\n        const rect = containerRef.current.getBoundingClientRect();\n        const deltaX = clientX - startPosition;\n        const deltaPercent = (deltaX / rect.width) * 100;\n        const newLeftWidth = startWidth + deltaPercent;\n\n        const effectiveMinSize = allowFullCollapse ? 0 : minSize;\n        const effectiveMaxSize = allowFullCollapse ? 100 : maxSize;\n\n        let clampedWidth = Math.min(\n          Math.max(newLeftWidth, effectiveMinSize),\n          effectiveMaxSize,\n        );\n\n        if (allowFullCollapse) {\n          if (clampedWidth <= snapThreshold) {\n            clampedWidth = 0;\n          } else if (clampedWidth >= 100 - snapThreshold) {\n            clampedWidth = 100;\n          }\n        }\n\n        setLeftWidth(clampedWidth);\n        onResize?.(clampedWidth, 100 - clampedWidth);\n      });\n    },\n    [\n      isDragging,\n      startPosition,\n      startWidth,\n      onResize,\n      allowFullCollapse,\n      minSize,\n      maxSize,\n      snapThreshold,\n    ],\n  );\n\n  const handleMouseMove = React.useCallback(\n    (e: MouseEvent): void => {\n      handleMove(e.clientX);\n    },\n    [handleMove],\n  );\n\n  const handleTouchMove = React.useCallback(\n    (e: TouchEvent): void => {\n      e.preventDefault();\n      if (e.touches.length === 1) {\n        handleMove(e.touches[0].clientX);\n      }\n    },\n    [handleMove],\n  );\n\n  const handleInteractionEnd = React.useCallback((): void => {\n    setIsDragging(false);\n\n    document.body.style.userSelect = '';\n    document.body.style.webkitUserSelect = '';\n    document.body.style.touchAction = '';\n    document.body.style.cursor = '';\n\n    if (containerRef.current) {\n      containerRef.current.classList.remove('splitter-active');\n    }\n\n    if (animationFrameRef.current !== undefined) {\n      cancelAnimationFrame(animationFrameRef.current);\n      animationFrameRef.current = undefined;\n    }\n  }, []);\n\n  const handleMouseUp = React.useCallback(\n    (e: MouseEvent): void => {\n      handleInteractionEnd();\n    },\n    [handleInteractionEnd],\n  );\n\n  const handleTouchEnd = React.useCallback(\n    (e: TouchEvent): void => {\n      handleInteractionEnd();\n    },\n    [handleInteractionEnd],\n  );\n\n  React.useEffect(() => {\n    if (isDragging) {\n      const options = { passive: false, capture: true };\n\n      document.addEventListener('mousemove', handleMouseMove, options);\n      document.addEventListener('mouseup', handleMouseUp, options);\n\n      document.addEventListener('touchmove', handleTouchMove, options);\n      document.addEventListener('touchend', handleTouchEnd, options);\n      document.addEventListener('touchcancel', handleTouchEnd, options);\n\n      document.addEventListener(\n        'selectstart',\n        (e) => e.preventDefault(),\n        options,\n      );\n      window.addEventListener('blur', handleInteractionEnd);\n    }\n\n    return () => {\n      document.removeEventListener('mousemove', handleMouseMove, {\n        capture: true,\n      });\n      document.removeEventListener('mouseup', handleMouseUp, { capture: true });\n      document.removeEventListener('touchmove', handleTouchMove, {\n        capture: true,\n      });\n      document.removeEventListener('touchend', handleTouchEnd, {\n        capture: true,\n      });\n      document.removeEventListener('touchcancel', handleTouchEnd, {\n        capture: true,\n      });\n      document.removeEventListener('selectstart', (e) => e.preventDefault(), {\n        capture: true,\n      });\n      window.removeEventListener('blur-sm', handleInteractionEnd);\n    };\n  }, [\n    isDragging,\n    handleMouseMove,\n    handleMouseUp,\n    handleTouchMove,\n    handleTouchEnd,\n    handleInteractionEnd,\n  ]);\n\n  React.useEffect(() => {\n    return () => {\n      if (animationFrameRef.current !== undefined) {\n        cancelAnimationFrame(animationFrameRef.current);\n      }\n    };\n  }, []);\n\n  const childrenArray = React.Children.toArray(children);\n  const panels = childrenArray.filter(\n    (child): child is React.ReactElement =>\n      React.isValidElement(child) && typeof child.type !== 'string',\n  );\n\n  const leftPanel = panels[0];\n  const rightPanel = panels[1];\n  const handle = panels.find(\n    (child) => React.isValidElement(child) && child.type === SplitterHandle,\n  );\n\n  const isLeftCollapsed = leftWidth <= 0.1;\n  const isRightCollapsed = leftWidth >= 99.9;\n  const isLeftNearCollapse = leftWidth <= snapThreshold && leftWidth > 0.1;\n  const isRightNearCollapse =\n    leftWidth >= 100 - snapThreshold && leftWidth < 99.9;\n\n  // collect derived flags into a stable object\n  const handleProps = React.useMemo(\n    () => ({\n      onMouseDown: handleMouseDown,\n      onTouchStart: handleTouchStart,\n      isDragging,\n      isLeftCollapsed,\n      isRightCollapsed,\n      isLeftNearCollapse,\n      isRightNearCollapse,\n    }),\n    [\n      handleMouseDown,\n      handleTouchStart,\n      isDragging,\n      isLeftCollapsed,\n      isRightCollapsed,\n      isLeftNearCollapse,\n      isRightNearCollapse,\n    ],\n  );\n\n  return (\n    <div\n      data-slot='splitter-root'\n      ref={containerRef}\n      className={cn(\n        'flex h-full w-full overflow-hidden relative',\n        `[--splitter-default-size:${defaultSize}]`,\n        `[--splitter-min-size:${allowFullCollapse ? 0 : minSize}]`,\n        `[--splitter-max-size:${allowFullCollapse ? 100 : maxSize}]`,\n        smoothTransition &&\n          !isDragging &&\n          'transition-all duration-200 ease-out',\n        className,\n      )}\n      style={{\n        ...style,\n        touchAction: 'none',\n      }}\n      {...props}\n    >\n      <motion.div\n        style={{\n          width: `${leftWidth}%`,\n        }}\n        className={cn(\n          'overflow-hidden relative',\n          isLeftCollapsed && 'pointer-events-none',\n        )}\n        animate={{\n          opacity: isLeftCollapsed ? 0 : 1,\n          scale: isLeftCollapsed ? 0.98 : 1,\n        }}\n        transition={{\n          duration: smoothTransition ? 0.2 : 0,\n          ease: 'easeOut',\n        }}\n      >\n        <div\n          className={cn(\n            'w-full h-full',\n            (isLeftNearCollapse || isLeftCollapsed) && 'blur-[1px]',\n            smoothTransition && 'transition-all duration-200',\n          )}\n        >\n          {leftPanel}\n        </div>\n      </motion.div>\n\n      {handle ? (\n        React.cloneElement(\n          handle as React.ReactElement<SplitterHandleProps>,\n          // eslint-disable-next-line react-hooks/refs\n          handleProps,\n        )\n      ) : (\n        <SplitterHandle {...handleProps} />\n      )}\n\n      <motion.div\n        style={{\n          width: `${100 - leftWidth}%`,\n        }}\n        className={cn(\n          'overflow-hidden relative',\n          isRightCollapsed && 'pointer-events-none',\n        )}\n        animate={{\n          opacity: isRightCollapsed ? 0 : 1,\n          scale: isRightCollapsed ? 0.98 : 1,\n        }}\n        transition={{\n          duration: smoothTransition ? 0.2 : 0,\n          ease: 'easeOut',\n        }}\n      >\n        <div\n          className={cn(\n            'w-full h-full',\n            (isRightNearCollapse || isRightCollapsed) && 'blur-[1px]',\n            smoothTransition && 'transition-all duration-200',\n          )}\n        >\n          {rightPanel}\n        </div>\n      </motion.div>\n    </div>\n  );\n}\n\nfunction SplitterPanel({ className, children, ...props }: SplitterPanelProps) {\n  return (\n    <div\n      data-slot='splitter-panel'\n      className={cn('h-full w-full', className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nfunction SplitterHandle({\n  className,\n  withHandle = true,\n  onMouseDown,\n  onTouchStart,\n  isDragging,\n  isLeftCollapsed,\n  isRightCollapsed,\n  isLeftNearCollapse,\n  isRightNearCollapse,\n}: SplitterHandleProps) {\n  const motionProps: HTMLMotionProps<'div'> = {\n    className: cn(\n      'relative flex items-center justify-center cursor-col-resize select-none z-20',\n      'bg-border/80 backdrop-blur-xs',\n      'hover:bg-accent/80 active:bg-accent',\n      'dark:bg-border/80 dark:hover:bg-accent/80 dark:active:bg-accent',\n      isDragging && 'bg-accent dark:bg-accent shadow-lg scale-110',\n      (isLeftCollapsed || isRightCollapsed) &&\n        'bg-accent/90 dark:bg-accent/90 shadow-md',\n      (isLeftNearCollapse || isRightNearCollapse) &&\n        'bg-warning/60 dark:bg-warning/60',\n      'transition-all duration-200 ease-out',\n      'touch-manipulation',\n      className,\n    ),\n    style: {\n      width: isDragging ? '10px' : '8px',\n      minWidth: '8px',\n      cursor: 'col-resize',\n    },\n    onMouseDown,\n    onTouchStart,\n    animate: {\n      width: isDragging ? '10px' : '8px',\n      boxShadow: isDragging\n        ? '0 0 20px rgba(0,0,0,0.2), 0 0 40px rgba(59,130,246,0.3)'\n        : '0 2px 8px rgba(0,0,0,0.1)',\n    },\n    whileHover: {\n      width: '10px',\n      boxShadow: '0 4px 12px rgba(0,0,0,0.15)',\n    },\n    transition: { duration: 0.15, ease: 'easeOut' },\n  };\n\n  return (\n    <motion.div data-slot='splitter-handle' {...motionProps}>\n      {withHandle && !isLeftCollapsed && !isRightCollapsed && (\n        <motion.div\n          className={cn(\n            'absolute inset-y-0 w-full flex items-center justify-center',\n          )}\n          animate={{\n            opacity: isDragging ? 1 : 0.7,\n          }}\n        >\n          <div\n            className={cn(\n              'flex flex-col gap-1',\n              'transition-colors duration-200',\n            )}\n          >\n            {[...Array(3)].map((_, i) => (\n              <motion.div\n                key={i}\n                className={cn(\n                  'w-1 h-1 rounded-full bg-muted-foreground/60',\n                  isDragging && 'bg-muted-foreground',\n                  (isLeftCollapsed || isRightCollapsed) &&\n                    'bg-muted-foreground/80',\n                )}\n                animate={{\n                  scale: isDragging ? 1.2 : 1,\n                  opacity: isDragging ? 1 : 0.6,\n                }}\n                transition={{ delay: i * 0.05 }}\n              />\n            ))}\n          </div>\n        </motion.div>\n      )}\n\n      <AnimatePresence>\n        {isLeftCollapsed && (\n          <motion.div\n            initial={{ opacity: 0, scale: 0.8 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.8 }}\n            className='absolute inset-0 flex items-center justify-center text-accent-foreground pointer-events-none'\n          >\n            <div className='bg-accent rounded-full w-6 h-6 flex items-center justify-center shadow-lg'>\n              <PanelRightClose className='w-4 h-4' />\n            </div>\n          </motion.div>\n        )}\n        {isRightCollapsed && (\n          <motion.div\n            initial={{ opacity: 0, scale: 0.8 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.8 }}\n            className='absolute inset-0 flex items-center justify-center text-accent-foreground pointer-events-none'\n          >\n            <div className='bg-accent rounded-full w-6 h-6 flex items-center justify-center shadow-lg'>\n              <PanelLeftClose className='w-4 h-4' />\n            </div>\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </motion.div>\n  );\n}\n\nfunction SplitterResizer({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      data-slot='splitter-resizer'\n      className={cn(\n        'group flex h-full w-px items-center justify-center bg-border',\n        'hover:w-2 hover:bg-accent transition-all duration-150',\n        'dark:bg-border dark:hover:bg-accent',\n        className,\n      )}\n      {...props}\n    >\n      <div className='h-4 w-0.75 rounded-sm bg-muted-foreground/30 group-hover:bg-muted-foreground/60 transition-colors' />\n    </div>\n  );\n}\n\nconst addGlobalStyles = (() => {\n  let styleElement: HTMLStyleElement | null = null;\n\n  return () => {\n    if (typeof document !== 'undefined' && !styleElement) {\n      styleElement = document.createElement('style');\n      styleElement.textContent = `\n        .splitter-active {\n          user-select: none !important;\n          -webkit-user-select: none !important;\n          touch-action: none !important;\n        }\n        .splitter-active * {\n          pointer-events: none !important;\n        }\n        .splitter-active [data-slot=\"splitter-handle\"] {\n          pointer-events: auto !important;\n        }\n      `;\n      document.head.appendChild(styleElement);\n    }\n  };\n})();\n\nif (typeof document !== 'undefined') {\n  addGlobalStyles();\n}\n\nSplitter.displayName = 'Splitter';\nSplitterPanel.displayName = 'SplitterPanel';\nSplitterHandle.displayName = 'SplitterHandle';\nSplitterResizer.displayName = 'SplitterResizer';\n\nexport { Splitter, SplitterPanel, SplitterHandle, SplitterResizer };\n"
    },
    {
      "type": "registry:lib",
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}"
    }
  ]
}
