Todos os componentes

Responsive Modal Default

Responsive Modal Default é um componente de sobreposição para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.

UI Layouts MIT overlays

O que é

Responsive Modal Default é um componente de sobreposição para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.

Como usar

Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.

Abrir no catálogo

Cinco cópias grátis por mês. Sem cartão.

Precisa de

npm i motion lucide-react

Licença

Este componente vem de UI Layouts e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

Responsive Modal Default

./components/ui/responsive-modal.tsx
'use client';

import { cn } from '@/lib/utils';
import { X } from 'lucide-react';
import { AnimatePresence, motion } from 'motion/react';
import React, { createContext, type ReactNode, useContext, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { Drawer as VaulDrawer } from 'vaul';

interface DrawerContextProps {
  open: boolean;
  setOpen: (open: boolean) => void;
}

const DrawerContext = createContext<DrawerContextProps | undefined>(undefined);

export const useResponsiveModal = () => {
  const context = useContext(DrawerContext);
  if (!context) {
    throw new Error('useResponsiveModal must be used within a ResponsiveModalProvider');
  }
  return context;
};

interface ResponsiveModalProps {
  children: ReactNode;
  open?: boolean;
  setOpen?: (open: boolean) => void;
  classname?: string;
  clsBtnClassname?: string;
}

export function ResponsiveModal({
  children,
  open: controlledOpen,
  setOpen: controlledSetOpen,
  classname,
  clsBtnClassname,
}: ResponsiveModalProps) {
  const [internalOpen, setInternalOpen] = useState(false);
  const [isDesktop, setIsDesktop] = useState(false);
  const [mounted, setMounted] = useState(false);

  const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
  const setOpen = controlledSetOpen || setInternalOpen;

  useEffect(() => {
    setMounted(true);
    const mediaQuery = window.matchMedia('(min-width: 768px)');
    const handleMediaChange = (event: MediaQueryListEvent) => {
      setIsDesktop(event.matches);
    };

    setIsDesktop(mediaQuery.matches);
    mediaQuery.addEventListener('change', handleMediaChange);
    return () => mediaQuery.removeEventListener('change', handleMediaChange);
  }, []);

  const trigger = React.Children.toArray(children).find(
    (child: any) => child.type === ResponsiveModalTrigger
  );
  const content = React.Children.toArray(children).filter(
    (child: any) => child.type !== ResponsiveModalTrigger
  );

  const desktopModal =
    isDesktop && mounted
      ? createPortal(
          <AnimatePresence>
            {open && (
              <motion.div
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                exit={{ opacity: 0 }}
                className='fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-xs cursor-zoom-out'
                onClick={() => setOpen(false)}
              >
                <motion.div
                  initial={{ scale: 0.9, opacity: 0 }}
                  animate={{ scale: 1, opacity: 1 }}
                  exit={{ scale: 0.9, opacity: 0 }}
                  transition={{ type: 'spring', duration: 0.5 }}
                  onClick={(e) => e.stopPropagation()}
                  className={cn(
                    'relative w-full max-w-md border bg-white dark:bg-neutral-900 rounded-lg cursor-default',
                    classname
                  )}
                >
                  <button
                    className={cn(
                      'absolute top-2 right-2 bg-primary text-background p-2 border z-1 rounded-md',
                      clsBtnClassname
                    )}
                    onClick={() => setOpen(false)}
                    aria-label='Close'
                  >
                    <X />
                  </button>
                  {content}
                </motion.div>
              </motion.div>
            )}
          </AnimatePresence>,
          document.body
        )
      : null;

  return (
    <DrawerContext.Provider value={{ open, setOpen }}>
      {trigger}

      {desktopModal}

      {!isDesktop && (
        <VaulDrawer.Root shouldScaleBackground open={open} onOpenChange={setOpen}>
          <VaulDrawer.Portal>
            <VaulDrawer.Overlay className='fixed inset-0 z-50 bg-white/50 dark:bg-black/50 backdrop-blur-xs' />
            <VaulDrawer.Content className='fixed bottom-0 left-0 z-50 w-full max-h-[96%] bg-white dark:bg-neutral-900'>
              <div className='mx-auto w-16 h-[0.30rem] shrink-0 rounded-full bg-neutral-600 my-4' />
              <div className='w-full mx-auto max-h-[96vh] overflow-auto px-4 pb-2'>{content}</div>
            </VaulDrawer.Content>
          </VaulDrawer.Portal>
        </VaulDrawer.Root>
      )}
    </DrawerContext.Provider>
  );
}

export function ResponsiveModalContent({
  children,
  className,
}: {
  children: ReactNode;
  className?: string;
}) {
  return <div className={cn('', className)}>{children}</div>;
}

interface ResponsiveModalTriggerProps {
  children: ReactNode;
  asChild?: boolean;
}

export function ResponsiveModalTrigger({ children, asChild = false }: ResponsiveModalTriggerProps) {
  const { setOpen } = useResponsiveModal();

  if (asChild) {
    // Clone the child element and add onClick handler
    const child = React.Children.only(children) as React.ReactElement<any>;
    return React.cloneElement(child, {
      ...child.props,
      onClick: (e: React.MouseEvent) => {
        setOpen(true);
        // Call original onClick if it exists
        child.props.onClick?.(e);
      },
    } as any);
  }

  return <div onClick={() => setOpen(true)}>{children}</div>;
}
./registry/components/modal/responsive-modal.tsx
'use client';
import {
  ResponsiveModal,
  ResponsiveModalContent,
  ResponsiveModalTrigger,
} from '@/components/ui/responsive-modal';
import { useMediaQuery } from '@/hooks/use-media-query';
import { motion } from 'motion/react';
import { useState } from 'react';
import { toast } from 'sonner';

export default function MyDrawer() {
  // const [drawerOpen, setDrawerOpen] = useState(false);

  const handleSubmit = () => {
    toast.success('Profile image updated successfully');
    // setDrawerOpen(false);
  };
  return (
    <>
      {/* <div className='flex justify-between'>
        <motion.button
          onClick={() => setDrawerOpen(true)}
          whileTap={{ scale: 0.9 }}
          className='inline-flex h-12 w-fit mx-auto animate-background-shine items-center justify-center rounded-md  border-2 dark:border-[#656fe2] border-[#c0c6fc] dark:bg-[linear-gradient(110deg,#1e2a78,45%,#3749be,55%,#1e2a78)] bg-[linear-gradient(110deg,#3d5af1,45%,#5471ff,55%,#3d5af1)] bg-size-[200%_100%] dark:hover:border-white px-6 font-medium text-white dark:text-white transition-colors focus:outline-hidden focus:ring-2 dark:focus:ring-neutral-400 focus:ring-offset-2 focus:ring-offset-neutral-50'
        >
          Open Dialog
        </motion.button>
      </div> */}
      {/* <ResponsiveModal open={drawerOpen} setOpen={setDrawerOpen}> */}
      <div className='flex justify-center items-center'>
        <ResponsiveModal>
          <ResponsiveModalTrigger asChild>
            <motion.button
              whileTap={{ scale: 0.9 }}
              className='inline-flex h-12 w-fit mx-auto animate-background-shine items-center justify-center rounded-md  border-2 dark:border-[#656fe2] border-[#c0c6fc] dark:bg-[linear-gradient(110deg,#1e2a78,45%,#3749be,55%,#1e2a78)] bg-[linear-gradient(110deg,#3d5af1,45%,#5471ff,55%,#3d5af1)] bg-size-[200%_100%] dark:hover:border-white px-6 font-medium text-white dark:text-white transition-colors focus:outline-hidden focus:ring-2 dark:focus:ring-neutral-400 focus:ring-offset-2 focus:ring-offset-neutral-50'
            >
              Open Dialog
            </motion.button>
          </ResponsiveModalTrigger>
          <ResponsiveModalContent>
            <figure className='flex flex-col space-y-1.5 text-center  h-fit dark:bg-neutral-800 md:p-4 p-6'>
              <h1 className='font-medium text-2xl'>Update Profile Image</h1>
              <p className='text-sm text-muted-foreground w-4/5 mx-auto'>
                Upload a new profile image or remove the current one.
              </p>
              <div data-vaul-no-drag className='py-4 space-y-4'>
                <span className='relative flex justify-center overflow-hidden rounded-xl w-full '>
                  <span className='grid place-content-center h-40 w-40 rounded-xl dark:bg-neutral-950 bg-muted'>
                    JP
                  </span>
                </span>
                <div className='mb-3'>
                  <input
                    className='w-full border file:p-2 file:bg-black  file:border-none  file:text-white rounded-xs overflow-hidden'
                    type='file'
                    id='formFile'
                  />
                </div>
                <button
                  type='submit'
                  onClick={handleSubmit}
                  className='w-full rounded-xs dark:bg-white bg-black  p-2 dark:text-black text-white'
                >
                  Submit
                </button>
              </div>
            </figure>
          </ResponsiveModalContent>
        </ResponsiveModal>
      </div>
    </>
  );
}

Componentes parecidos