{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "side-sheet",
  "type": "registry:ui",
  "title": "Side Sheet",
  "description": "A customizable sheet component for creating elegant slide-out interfaces.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["motion", "clsx", "tailwind-merge"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/side-sheet.tsx",
      "content": "'use client';\nimport React, {\n  createContext,\n  useContext,\n  useState,\n  useRef,\n  useCallback,\n  useEffect,\n  forwardRef,\n  ReactElement,\n  useSyncExternalStore,\n} from 'react';\nimport { createPortal } from 'react-dom';\nimport {\n  motion,\n  useAnimation,\n  PanInfo,\n  useMotionValue,\n  useTransform,\n  HTMLMotionProps,\n} from 'motion/react';\nimport { cn } from '@/lib/utils';\n\ntype SheetSide = 'left' | 'right';\n\ninterface SideSheetContextValue {\n  isOpen: boolean;\n  onOpenChange: (open: boolean) => void;\n  contentProps: {\n    width: string;\n    className: string;\n    closeThreshold: number;\n    side: SheetSide;\n  };\n}\n\nconst SideSheetContext = createContext<SideSheetContextValue | null>(null);\n\nconst useSideSheetContext = () => {\n  const context = useContext(SideSheetContext);\n  if (!context) {\n    throw new Error(\n      'SideSheet compound components must be used within SideSheet',\n    );\n  }\n  return context;\n};\n\ninterface SideSheetRootProps {\n  children: React.ReactNode;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  defaultOpen?: boolean;\n  className?: string;\n  side?: SheetSide;\n  width?: string;\n  closeThreshold?: number;\n}\n\nconst SideSheetRoot = ({\n  children,\n  open,\n  onOpenChange,\n  defaultOpen,\n  className,\n  side = 'right',\n  width = '400px',\n  closeThreshold = 0.3,\n}: SideSheetRootProps) => {\n  const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);\n\n  const isControlled = open !== undefined;\n  const isOpen = isControlled ? open : internalOpen;\n\n  const handleOpenChange = useCallback(\n    (newOpen: boolean) => {\n      if (onOpenChange) {\n        onOpenChange(newOpen);\n      }\n      if (!isControlled) {\n        setInternalOpen(newOpen);\n      }\n    },\n    [onOpenChange, isControlled],\n  );\n\n  const contentProps = {\n    width,\n    className: className || '',\n    closeThreshold,\n    side,\n  };\n\n  return (\n    <SideSheetContext.Provider\n      value={{ isOpen, onOpenChange: handleOpenChange, contentProps }}\n    >\n      {children}\n    </SideSheetContext.Provider>\n  );\n};\n\ninterface SideSheetPortalProps {\n  children: React.ReactNode;\n  container?: HTMLElement;\n  className?: string;\n}\n\nconst SideSheetPortal = ({\n  children,\n  container,\n  className,\n}: SideSheetPortalProps) => {\n  const mounted = useSyncExternalStore(\n    () => () => {},\n    () => true,\n    () => false,\n  );\n\n  if (!mounted) {\n    return null;\n  }\n\n  const portalContent = className ? (\n    <div className={className}>{children}</div>\n  ) : (\n    children\n  );\n\n  return createPortal(portalContent, container || document.body);\n};\n\ninterface SideSheetOverlayProps extends HTMLMotionProps<'div'> {\n  className?: string;\n}\n\nconst SideSheetOverlay = forwardRef<HTMLDivElement, SideSheetOverlayProps>(\n  ({ className, ...props }, ref) => {\n    const { isOpen, onOpenChange } = useSideSheetContext();\n\n    const handleClick = useCallback(\n      (e: React.MouseEvent) => {\n        if (e.target === e.currentTarget) {\n          onOpenChange(false);\n        }\n      },\n      [onOpenChange],\n    );\n\n    return (\n      <motion.div\n        ref={ref}\n        initial={{ opacity: 0 }}\n        animate={{ opacity: isOpen ? 1 : 0 }}\n        transition={{ duration: 0.2, ease: 'easeOut' }}\n        onClick={handleClick}\n        className={cn(\n          'absolute inset-0 bg-black/20 backdrop-blur-xs',\n          className,\n        )}\n        style={{ pointerEvents: isOpen ? 'auto' : 'none' }}\n        {...props}\n      />\n    );\n  },\n);\nSideSheetOverlay.displayName = 'SideSheetOverlay';\n\ninterface SideSheetTriggerProps {\n  asChild?: boolean;\n  children: React.ReactNode;\n  className?: string;\n}\n\nconst SideSheetTrigger = ({\n  asChild,\n  children,\n  className,\n}: SideSheetTriggerProps) => {\n  const { onOpenChange } = useSideSheetContext();\n\n  const handleClick = () => {\n    onOpenChange(true);\n  };\n\n  if (asChild && React.isValidElement(children)) {\n    const child = children as ReactElement<{\n      className?: string;\n      onClick?: (e: React.MouseEvent) => void;\n    }>;\n    return React.cloneElement(child, {\n      className: cn(child.props.className, className),\n      onClick: (e: React.MouseEvent) => {\n        child.props.onClick?.(e);\n        handleClick();\n      },\n    });\n  }\n\n  return (\n    <div onClick={handleClick} className={cn('', className)}>\n      {children}\n    </div>\n  );\n};\n\ninterface SideSheetContentProps {\n  children?: React.ReactNode;\n  className?: string;\n}\n\nconst SideSheetContent = ({\n  children,\n  className = '',\n}: SideSheetContentProps) => {\n  const { isOpen, onOpenChange, contentProps } = useSideSheetContext();\n  const { width, closeThreshold, side } = contentProps;\n  const controls = useAnimation();\n  const x = useMotionValue(0);\n  useTransform(x, [-100, 0], [0, 1]);\n  const overlayRef = useRef<HTMLDivElement>(null);\n  const [sheetWidth, setSheetWidth] = useState(0);\n\n  const onClose = useCallback(() => onOpenChange(false), [onOpenChange]);\n\n  const calculateWidth = useCallback(() => {\n    if (typeof window !== 'undefined') {\n      const vw = window.innerWidth;\n\n      let calculatedWidth;\n      if (vw <= 640) {\n        calculatedWidth = vw * 0.9;\n      } else if (vw <= 1024) {\n        calculatedWidth = vw * 0.7;\n      } else {\n        if (width.includes('px')) {\n          calculatedWidth = parseInt(width);\n        } else if (width.includes('vw')) {\n          calculatedWidth = (parseInt(width) / 100) * vw;\n        } else if (width.includes('%')) {\n          calculatedWidth = (parseInt(width) / 100) * vw;\n        } else {\n          calculatedWidth = 400;\n        }\n      }\n\n      return Math.min(calculatedWidth, vw * 0.95);\n    }\n    return 400;\n  }, [width]);\n\n  useEffect(() => {\n    const updateWidth = () => {\n      setSheetWidth(calculateWidth());\n    };\n\n    updateWidth();\n    window.addEventListener('resize', updateWidth);\n\n    return () => window.removeEventListener('resize', updateWidth);\n  }, [calculateWidth]);\n\n  const getInitialX = useCallback(() => {\n    return side === 'left' ? -(sheetWidth + 50) : sheetWidth + 50;\n  }, [side, sheetWidth]);\n\n  const getPositionStyles = useCallback(() => {\n    if (side === 'left') {\n      return {\n        left: 0,\n        top: 0,\n        bottom: 0,\n      };\n    } else {\n      return {\n        right: 0,\n        top: 0,\n        bottom: 0,\n      };\n    }\n  }, [side]);\n\n  useEffect(() => {\n    if (isOpen) {\n      document.body.style.overflow = 'hidden';\n      controls.start({\n        x: 0,\n        transition: {\n          type: 'spring',\n          stiffness: 400,\n          damping: 40,\n          mass: 0.8,\n        },\n      });\n    } else {\n      document.body.style.overflow = '';\n      controls.start({\n        x: getInitialX(),\n        transition: {\n          type: 'tween',\n          ease: [0.25, 0.46, 0.45, 0.94],\n          duration: 0.3,\n        },\n      });\n    }\n    return () => {\n      document.body.style.overflow = '';\n    };\n  }, [isOpen, controls, getInitialX]);\n\n  useEffect(() => {\n    const handleEscape = (e: KeyboardEvent) => {\n      if (e.key === 'Escape' && isOpen) {\n        onClose();\n      }\n    };\n    if (isOpen) {\n      document.addEventListener('keydown', handleEscape);\n    }\n    return () => {\n      document.removeEventListener('keydown', handleEscape);\n    };\n  }, [isOpen, onClose]);\n\n  const handleDragEnd = useCallback(\n    (_event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => {\n      const threshold = sheetWidth * closeThreshold;\n      let shouldClose = false;\n\n      if (side === 'left') {\n        shouldClose = info.offset.x < -threshold || info.velocity.x < -800;\n      } else {\n        shouldClose = info.offset.x > threshold || info.velocity.x > 800;\n      }\n\n      if (shouldClose) {\n        onClose();\n      } else {\n        controls.start({\n          x: 0,\n          transition: {\n            type: 'spring',\n            stiffness: 500,\n            damping: 40,\n          },\n        });\n      }\n    },\n    [controls, onClose, closeThreshold, sheetWidth, side],\n  );\n\n  const handleOverlayClick = useCallback(\n    (e: React.MouseEvent) => {\n      if (e.target === overlayRef.current) {\n        onClose();\n      }\n    },\n    [onClose],\n  );\n\n  const getDragConstraints = useCallback(() => {\n    if (side === 'left') {\n      return { left: -sheetWidth, right: 0 };\n    } else {\n      return { left: 0, right: sheetWidth };\n    }\n  }, [side, sheetWidth]);\n\n  if (sheetWidth === 0) return null;\n\n  return (\n    <SideSheetPortal>\n      <div\n        className={cn('fixed inset-0 z-999', !isOpen && 'pointer-events-none')}\n      >\n        <motion.div\n          ref={overlayRef}\n          initial={{ opacity: 0 }}\n          animate={{ opacity: isOpen ? 1 : 0 }}\n          transition={{ duration: 0.2, ease: 'easeOut' }}\n          onClick={handleOverlayClick}\n          className='absolute inset-0 bg-black/20 backdrop-blur-xs'\n          style={{ pointerEvents: isOpen ? 'auto' : 'none' }}\n        />\n        <motion.div\n          drag='x'\n          dragConstraints={getDragConstraints()}\n          dragElastic={0}\n          dragMomentum={false}\n          onDragEnd={handleDragEnd}\n          animate={controls}\n          initial={{ x: getInitialX() }}\n          className={cn(\n            'absolute bg-white dark:bg-[#0A0A0A] shadow-2xl',\n            side === 'left' ? 'rounded-r-lg' : 'rounded-l-lg',\n            className,\n          )}\n          style={{\n            width: sheetWidth,\n            ...getPositionStyles(),\n          }}\n        >\n          <div className='h-full overflow-hidden'>\n            <div\n              className='h-full overflow-y-auto px-6 py-6 scrollbar-hide'\n              style={{\n                scrollbarWidth: 'none',\n                msOverflowStyle: 'none',\n              }}\n            >\n              {children}\n            </div>\n          </div>\n\n          <div\n            className={cn(\n              'absolute top-1/2 -translate-y-1/2 flex items-center',\n              side === 'left' ? 'right-0 pr-2' : 'left-0 pl-2',\n            )}\n          >\n            <div className='w-2 h-16 rounded-full bg-muted cursor-grab active:cursor-grabbing' />\n          </div>\n        </motion.div>\n      </div>\n    </SideSheetPortal>\n  );\n};\n\ninterface SideSheetHeaderProps {\n  children: React.ReactNode;\n  className?: string;\n}\n\nconst SideSheetHeader = ({ children, className }: SideSheetHeaderProps) => {\n  return (\n    <div className={cn('flex flex-col space-y-1.5 text-left pb-4', className)}>\n      {children}\n    </div>\n  );\n};\n\ninterface SideSheetTitleProps {\n  children: React.ReactNode;\n  className?: string;\n}\n\nconst SideSheetTitle = ({ children, className }: SideSheetTitleProps) => {\n  return (\n    <h3\n      className={cn(\n        'text-lg font-semibold leading-none tracking-tight',\n        className,\n      )}\n    >\n      {children}\n    </h3>\n  );\n};\n\ninterface SideSheetDescriptionProps {\n  children: React.ReactNode;\n  className?: string;\n}\n\nconst SideSheetDescription = ({\n  children,\n  className,\n}: SideSheetDescriptionProps) => {\n  return (\n    <p className={cn('text-sm text-gray-600 dark:text-gray-400', className)}>\n      {children}\n    </p>\n  );\n};\n\ninterface SideSheetFooterProps {\n  children: React.ReactNode;\n  className?: string;\n}\n\nconst SideSheetFooter = ({ children, className }: SideSheetFooterProps) => {\n  return (\n    <div\n      className={cn(\n        'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 pt-4',\n        className,\n      )}\n    >\n      {children}\n    </div>\n  );\n};\n\ninterface SideSheetCloseProps {\n  asChild?: boolean;\n  children: React.ReactNode;\n  className?: string;\n}\n\nconst SideSheetClose = ({\n  asChild,\n  children,\n  className,\n}: SideSheetCloseProps) => {\n  const { onOpenChange } = useSideSheetContext();\n\n  const handleClick = () => {\n    onOpenChange(false);\n  };\n\n  if (asChild && React.isValidElement(children)) {\n    const child = children as ReactElement<{\n      className?: string;\n      onClick?: (e: React.MouseEvent) => void;\n    }>;\n    return React.cloneElement(child, {\n      className: cn(child.props.className, className),\n      onClick: (e: React.MouseEvent) => {\n        child.props.onClick?.(e);\n        handleClick();\n      },\n    });\n  }\n\n  return (\n    <button onClick={handleClick} type='button' className={cn('', className)}>\n      {children}\n    </button>\n  );\n};\n\nconst SideSheet = SideSheetRoot;\n\nexport {\n  SideSheet,\n  SideSheetPortal,\n  SideSheetOverlay,\n  SideSheetTrigger,\n  SideSheetClose,\n  SideSheetContent,\n  SideSheetHeader,\n  SideSheetFooter,\n  SideSheetTitle,\n  SideSheetDescription,\n};\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}"
    }
  ]
}
