{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dialog-stack","type":"registry:ui","title":"dialog-stack","description":"Composable stacked dialogs, useful for creating a wizard, nested form or multi-step process. It provides a consistent layout and styling for each dialog, and includes navigation component.","author":"Hayden Bleasel <hello@haydenbleasel.com>","dependencies":["@radix-ui/react-use-controllable-state","radix-ui"],"devDependencies":[],"registryDependencies":[],"files":[{"type":"registry:ui","path":"index.tsx","content":"\"use client\";\n\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\";\nimport { Portal } from \"radix-ui\";\nimport type {\n  ButtonHTMLAttributes,\n  Dispatch,\n  HTMLAttributes,\n  MouseEvent,\n  MouseEventHandler,\n  ReactElement,\n  SetStateAction,\n} from \"react\";\nimport {\n  Children,\n  cloneElement,\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype DialogStackContextType = {\n  activeIndex: number;\n  setActiveIndex: Dispatch<SetStateAction<number>>;\n  totalDialogs: number;\n  setTotalDialogs: Dispatch<SetStateAction<number>>;\n  isOpen: boolean;\n  setIsOpen: Dispatch<SetStateAction<boolean>>;\n  clickable: boolean;\n};\n\nconst DialogStackContext = createContext<DialogStackContextType>({\n  activeIndex: 0,\n  setActiveIndex: () => {},\n  totalDialogs: 0,\n  setTotalDialogs: () => {},\n  isOpen: false,\n  setIsOpen: () => {},\n  clickable: false,\n});\n\ntype DialogStackChildProps = {\n  index?: number;\n};\n\nexport type DialogStackProps = HTMLAttributes<HTMLDivElement> & {\n  open?: boolean;\n  clickable?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  defaultOpen?: boolean;\n};\n\nexport const DialogStack = ({\n  children,\n  className,\n  open,\n  defaultOpen = false,\n  onOpenChange,\n  clickable = false,\n  ...props\n}: DialogStackProps) => {\n  const [activeIndex, setActiveIndex] = useState(0);\n  const [isOpen, setIsOpen] = useControllableState({\n    defaultProp: defaultOpen,\n    prop: open,\n    onChange: onOpenChange,\n  });\n\n  useEffect(() => {\n    if (onOpenChange && isOpen !== undefined) {\n      onOpenChange(isOpen);\n    }\n  }, [isOpen, onOpenChange]);\n\n  return (\n    <DialogStackContext.Provider\n      value={{\n        activeIndex,\n        setActiveIndex,\n        totalDialogs: 0,\n        setTotalDialogs: () => {},\n        isOpen: isOpen ?? false,\n        setIsOpen: (value) => setIsOpen(Boolean(value)),\n        clickable,\n      }}\n    >\n      <div className={className} {...props}>\n        {children}\n      </div>\n    </DialogStackContext.Provider>\n  );\n};\n\nexport type DialogStackTriggerProps =\n  ButtonHTMLAttributes<HTMLButtonElement> & {\n    asChild?: boolean;\n  };\n\nexport const DialogStackTrigger = ({\n  children,\n  className,\n  onClick,\n  asChild,\n  ...props\n}: DialogStackTriggerProps) => {\n  const context = useContext(DialogStackContext);\n\n  if (!context) {\n    throw new Error(\"DialogStackTrigger must be used within a DialogStack\");\n  }\n\n  const handleClick: MouseEventHandler<HTMLButtonElement> = (e) => {\n    context.setIsOpen(true);\n    onClick?.(e);\n  };\n\n  if (asChild && children) {\n    const child = children as ReactElement<{\n      onClick: MouseEventHandler<HTMLButtonElement>;\n      className?: string;\n    }>;\n    return cloneElement(child, {\n      onClick: (e: MouseEvent<HTMLButtonElement>) => {\n        handleClick(e);\n        child.props.onClick?.(e);\n      },\n      className: cn(className, child.props.className),\n      ...props,\n    });\n  }\n\n  return (\n    <button\n      className={cn(\n        \"inline-flex items-center justify-center whitespace-nowrap rounded-md font-medium text-sm\",\n        \"ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2\",\n        \"focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n        \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        \"h-10 px-4 py-2\",\n        className\n      )}\n      onClick={handleClick}\n      {...props}\n    >\n      {children}\n    </button>\n  );\n};\n\nexport type DialogStackOverlayProps = HTMLAttributes<HTMLDivElement>;\n\nexport const DialogStackOverlay = ({\n  className,\n  ...props\n}: DialogStackOverlayProps) => {\n  const context = useContext(DialogStackContext);\n\n  if (!context) {\n    throw new Error(\"DialogStackOverlay must be used within a DialogStack\");\n  }\n\n  const handleClick = useCallback(() => {\n    context.setIsOpen(false);\n  }, [context.setIsOpen]);\n\n  if (!context.isOpen) {\n    return null;\n  }\n\n  return (\n    // biome-ignore lint/a11y/noStaticElementInteractions: \"This is a clickable overlay\"\n    // biome-ignore lint/a11y/useKeyWithClickEvents: \"This is a clickable overlay\"\n    <div\n      className={cn(\n        \"fixed inset-0 z-50 bg-black/80\",\n        \"data-[state=closed]:animate-out data-[state=open]:animate-in\",\n        \"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n        className\n      )}\n      onClick={handleClick}\n      {...props}\n    />\n  );\n};\n\nexport type DialogStackBodyProps = HTMLAttributes<HTMLDivElement> & {\n  children:\n    | ReactElement<DialogStackChildProps>[]\n    | ReactElement<DialogStackChildProps>;\n};\n\nexport const DialogStackBody = ({\n  children,\n  className,\n  ...props\n}: DialogStackBodyProps) => {\n  const context = useContext(DialogStackContext);\n  const [totalDialogs, setTotalDialogs] = useState(Children.count(children));\n\n  if (!context) {\n    throw new Error(\"DialogStackBody must be used within a DialogStack\");\n  }\n\n  if (!context.isOpen) {\n    return null;\n  }\n\n  return (\n    <DialogStackContext.Provider\n      value={{\n        ...context,\n        totalDialogs,\n        setTotalDialogs,\n      }}\n    >\n      <Portal.Root>\n        <div\n          className={cn(\n            \"pointer-events-none fixed inset-0 z-50 mx-auto flex w-full max-w-lg flex-col items-center justify-center\",\n            className\n          )}\n          {...props}\n        >\n          <div className=\"pointer-events-auto relative flex w-full flex-col items-center justify-center\">\n            {Children.map(children, (child, index) => {\n              const childElement = child as ReactElement<{\n                index: number;\n                onClick: MouseEventHandler<HTMLButtonElement>;\n                className?: string;\n              }>;\n\n              return cloneElement(childElement, {\n                ...childElement.props,\n                index,\n              });\n            })}\n          </div>\n        </div>\n      </Portal.Root>\n    </DialogStackContext.Provider>\n  );\n};\n\nexport type DialogStackContentProps = HTMLAttributes<HTMLDivElement> & {\n  index?: number;\n  offset?: number;\n};\n\nexport const DialogStackContent = ({\n  children,\n  className,\n  index = 0,\n  offset = 10,\n  ...props\n}: DialogStackContentProps) => {\n  const context = useContext(DialogStackContext);\n\n  if (!context) {\n    throw new Error(\"DialogStackContent must be used within a DialogStack\");\n  }\n\n  if (!context.isOpen) {\n    return null;\n  }\n\n  const handleClick = () => {\n    if (context.clickable && context.activeIndex > index) {\n      context.setActiveIndex(index ?? 0);\n    }\n  };\n\n  const distanceFromActive = index - context.activeIndex;\n  const translateY =\n    distanceFromActive < 0\n      ? `-${Math.abs(distanceFromActive) * offset}px`\n      : `${Math.abs(distanceFromActive) * offset}px`;\n\n  return (\n    // biome-ignore lint/a11y/noStaticElementInteractions: \"This is a clickable dialog\"\n    // biome-ignore lint/a11y/useKeyWithClickEvents: \"This is a clickable dialog\"\n    <div\n      className={cn(\n        \"h-auto w-full rounded-lg border bg-background p-6 shadow-lg transition-all duration-300\",\n        className\n      )}\n      onClick={handleClick}\n      style={{\n        top: 0,\n        transform: `translateY(${translateY})`,\n        width: `calc(100% - ${Math.abs(distanceFromActive) * 10}px)`,\n        zIndex: 50 - Math.abs(context.activeIndex - (index ?? 0)),\n        position: distanceFromActive ? \"absolute\" : \"relative\",\n        opacity: distanceFromActive > 0 ? 0 : 1,\n        cursor:\n          context.clickable && context.activeIndex > index\n            ? \"pointer\"\n            : \"default\",\n      }}\n      {...props}\n    >\n      <div\n        className={cn(\n          \"h-full w-full transition-all duration-300\",\n          context.activeIndex !== index &&\n            \"pointer-events-none select-none opacity-0\"\n        )}\n      >\n        {children}\n      </div>\n    </div>\n  );\n};\n\nexport type DialogStackTitleProps = HTMLAttributes<HTMLHeadingElement>;\n\nexport const DialogStackTitle = ({\n  children,\n  className,\n  ...props\n}: DialogStackTitleProps) => (\n  <h2\n    className={cn(\n      \"font-semibold text-lg leading-none tracking-tight\",\n      className\n    )}\n    {...props}\n  >\n    {children}\n  </h2>\n);\n\nexport type DialogStackDescriptionProps = HTMLAttributes<HTMLParagraphElement>;\n\nexport const DialogStackDescription = ({\n  children,\n  className,\n  ...props\n}: DialogStackDescriptionProps) => (\n  <p className={cn(\"text-muted-foreground text-sm\", className)} {...props}>\n    {children}\n  </p>\n);\n\nexport type DialogStackHeaderProps = HTMLAttributes<HTMLDivElement>;\n\nexport const DialogStackHeader = ({\n  className,\n  ...props\n}: DialogStackHeaderProps) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-1.5 text-center sm:text-left\",\n      className\n    )}\n    {...props}\n  />\n);\n\nexport type DialogStackFooterProps = HTMLAttributes<HTMLDivElement>;\n\nexport const DialogStackFooter = ({\n  children,\n  className,\n  ...props\n}: DialogStackFooterProps) => (\n  <div\n    className={cn(\"flex items-center justify-end space-x-2 pt-4\", className)}\n    {...props}\n  >\n    {children}\n  </div>\n);\n\nexport type DialogStackNextProps = ButtonHTMLAttributes<HTMLButtonElement> & {\n  asChild?: boolean;\n};\n\nexport const DialogStackNext = ({\n  children,\n  className,\n  asChild,\n  ...props\n}: DialogStackNextProps) => {\n  const context = useContext(DialogStackContext);\n\n  if (!context) {\n    throw new Error(\"DialogStackNext must be used within a DialogStack\");\n  }\n\n  const handleNext = () => {\n    if (context.activeIndex < context.totalDialogs - 1) {\n      context.setActiveIndex(context.activeIndex + 1);\n    }\n  };\n\n  if (asChild && children) {\n    const child = children as ReactElement<{\n      onClick: MouseEventHandler<HTMLButtonElement>;\n      className?: string;\n    }>;\n\n    return cloneElement(child, {\n      onClick: (e: MouseEvent<HTMLButtonElement>) => {\n        handleNext();\n        child.props.onClick?.(e);\n      },\n      className: cn(className, child.props.className),\n      ...props,\n    });\n  }\n\n  return (\n    <button\n      className={cn(\n        \"inline-flex items-center justify-center whitespace-nowrap rounded-md font-medium text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n        className\n      )}\n      disabled={context.activeIndex >= context.totalDialogs - 1}\n      onClick={handleNext}\n      type=\"button\"\n      {...props}\n    >\n      {children || \"Next\"}\n    </button>\n  );\n};\n\nexport type DialogStackPreviousProps =\n  ButtonHTMLAttributes<HTMLButtonElement> & {\n    asChild?: boolean;\n  };\n\nexport const DialogStackPrevious = ({\n  children,\n  className,\n  asChild,\n  ...props\n}: DialogStackPreviousProps) => {\n  const context = useContext(DialogStackContext);\n\n  if (!context) {\n    throw new Error(\"DialogStackPrevious must be used within a DialogStack\");\n  }\n\n  const handlePrevious = () => {\n    if (context.activeIndex > 0) {\n      context.setActiveIndex(context.activeIndex - 1);\n    }\n  };\n\n  if (asChild && children) {\n    const child = children as ReactElement<{\n      onClick: MouseEventHandler<HTMLButtonElement>;\n      className?: string;\n    }>;\n\n    return cloneElement(child, {\n      onClick: (e: MouseEvent<HTMLButtonElement>) => {\n        handlePrevious();\n        child.props.onClick?.(e);\n      },\n      className: cn(className, child.props.className),\n      ...props,\n    });\n  }\n\n  return (\n    <button\n      className={cn(\n        \"inline-flex items-center justify-center whitespace-nowrap rounded-md font-medium text-sm ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n        className\n      )}\n      disabled={context.activeIndex <= 0}\n      onClick={handlePrevious}\n      type=\"button\"\n      {...props}\n    >\n      {children || \"Previous\"}\n    </button>\n  );\n};\n","target":"components/kibo-ui/dialog-stack/index.tsx"}],"css":{}}