Full Code of emilkowalski/sonner for AI

main 45d894085af8 cached
64 files
156.4 KB
44.4k tokens
48 symbols
1 requests
Download .txt
Repository: emilkowalski/sonner
Branch: main
Commit: 45d894085af8
Files: 64
Total size: 156.4 KB

Directory structure:
gitextract_cl8qn5z4/

├── .github/
│   └── workflows/
│       └── playwright.yml
├── .gitignore
├── .prettierrc.js
├── FUNDING.yml
├── LICENSE.md
├── README.md
├── package.json
├── playwright.config.ts
├── pnpm-workspace.yaml
├── src/
│   ├── assets.tsx
│   ├── hooks.tsx
│   ├── index.tsx
│   ├── state.ts
│   ├── styles.css
│   └── types.ts
├── test/
│   ├── .eslintrc.json
│   ├── .gitignore
│   ├── .npmrc
│   ├── .vscode/
│   │   └── settings.json
│   ├── README.md
│   ├── next.config.js
│   ├── package.json
│   ├── src/
│   │   └── app/
│   │       ├── action.tsx
│   │       ├── layout.tsx
│   │       └── page.tsx
│   ├── tests/
│   │   └── basic.spec.ts
│   └── tsconfig.json
├── tsconfig.json
├── turbo.json
└── website/
    ├── .eslintrc.json
    ├── .gitignore
    ├── .vscode/
    │   └── settings.json
    ├── README.md
    ├── next.config.js
    ├── package.json
    ├── postcss.config.js
    ├── src/
    │   ├── components/
    │   │   ├── CodeBlock/
    │   │   │   ├── code-block.module.css
    │   │   │   └── index.tsx
    │   │   ├── ExpandModes/
    │   │   │   └── index.tsx
    │   │   ├── Footer/
    │   │   │   ├── footer.module.css
    │   │   │   └── index.tsx
    │   │   ├── Head/
    │   │   │   └── index.tsx
    │   │   ├── Hero/
    │   │   │   ├── hero.module.css
    │   │   │   └── index.tsx
    │   │   ├── How/
    │   │   │   └── How.tsx
    │   │   ├── Installation/
    │   │   │   ├── index.tsx
    │   │   │   └── installation.module.css
    │   │   ├── Other/
    │   │   │   ├── Other.tsx
    │   │   │   └── other.module.css
    │   │   ├── Position/
    │   │   │   └── index.tsx
    │   │   ├── Types/
    │   │   │   └── Types.tsx
    │   │   └── Usage/
    │   │       └── index.tsx
    │   ├── globals.css
    │   ├── pages/
    │   │   ├── _app.tsx
    │   │   ├── _meta.json
    │   │   ├── getting-started.mdx
    │   │   ├── index.tsx
    │   │   ├── styling.mdx
    │   │   ├── toast.mdx
    │   │   └── toaster.mdx
    │   └── style.css
    ├── tailwind.config.js
    ├── theme.config.jsx
    └── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/playwright.yml
================================================
name: Playwright Tests
on:
  push:
    branches: [main, master]
  pull_request:
    branches: [main, master]
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 20
      - run: npm install pnpm@9.15.9 -g
      - run: pnpm install --no-frozen-lockfile
      - run: pnpm build
      - run: npx playwright install --with-deps
      - run: pnpm test || exit 1
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30


================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
dist


# dependencies
node_modules
.pnp
.pnp.js

# testing
coverage

# next.js
.next/
out/
build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local

# turbo
.turbo
/test-results/
/playwright-report/
/playwright/.cache/


================================================
FILE: .prettierrc.js
================================================
module.exports = {
  semi: true,
  singleQuote: true,
  tabWidth: 2,
  trailingComma: 'all',
  printWidth: 120,
};


================================================
FILE: FUNDING.yml
================================================
github: emilkowalski


================================================
FILE: LICENSE.md
================================================
MIT License

Copyright (c) 2023 Emil Kowalski

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
https://github.com/vallezw/sonner/assets/50796600/59b95cb7-9068-4f3e-8469-0b35d9de5cf0

[Sonner](https://sonner.emilkowal.ski/) is an opinionated toast component for React. You can read more about why and how it was built [here](https://emilkowal.ski/ui/building-a-toast-component).

## Usage

To start using the library, install it in your project:

```bash
npm install sonner
```

Add `<Toaster />` to your app, it will be the place where all your toasts will be rendered.
After that you can use `toast()` from anywhere in your app.

```jsx
import { Toaster, toast } from 'sonner';

// ...

function App() {
  return (
    <div>
      <Toaster />
      <button onClick={() => toast('My first toast')}>Give me a toast</button>
    </div>
  );
}
```

## Documentation

Find the full API reference in the [documentation](https://sonner.emilkowal.ski/getting-started).


================================================
FILE: package.json
================================================
{
  "name": "sonner",
  "version": "2.0.7",
  "description": "An opinionated toast component for React.",
  "exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.mts",
        "default": "./dist/index.mjs"
      },
      "require": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.js"
      },
      "default": "./dist/index.js"
    },
    "./dist/styles.css": "./dist/styles.css"
  },
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": [
    "dist"
  ],
  "scripts": {
    "dev": "bunchee --watch",
    "build": "bunchee && cp src/styles.css dist/styles.css",
    "type-check": "tsc --noEmit",
    "dev:website": "turbo run dev --filter=website...",
    "dev:test": "turbo run dev --filter=test...",
    "format": "prettier --write .",
    "test": "playwright test"
  },
  "keywords": [
    "react",
    "notifications",
    "toast",
    "snackbar",
    "message"
  ],
  "author": "Emil Kowalski <e@emilkowal.ski>",
  "license": "MIT",
  "homepage": "https://sonner.emilkowal.ski/",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/emilkowalski/sonner.git"
  },
  "bugs": {
    "url": "https://github.com/emilkowalski/sonner/issues"
  },
  "devDependencies": {
    "@playwright/test": "^1.49.1",
    "@types/node": "^18.11.13",
    "@types/react": "^18.0.26",
    "bunchee": "6.3.3",
    "prettier": "^2.8.4",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "turbo": "1.6",
    "typescript": "^4.8.4"
  },
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
    "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
  },
  "packageManager": "pnpm@9.15.9"
}


================================================
FILE: playwright.config.ts
================================================
import { defineConfig, devices } from '@playwright/test';

/**
 * Read environment variables from file.
 * https://github.com/motdotla/dotenv
 */
// require('dotenv').config();

/**
 * See https://playwright.dev/docs/test-configuration.
 */
export default defineConfig({
  testDir: './test',
  /* Maximum time one test can run for. */
  timeout: 30 * 1000,
  expect: {
    /**
     * Maximum time expect() should wait for the condition to be met.
     * For example in `await expect(locator).toHaveText();`
     */
    timeout: 5000,
  },
  /* Run tests in files in parallel */
  fullyParallel: true,
  /* Fail the build on CI if you accidentally left test.only in the source code. */
  forbidOnly: !!process.env.CI,
  /* Retry on CI only */
  retries: process.env.CI ? 2 : 0,
  /* Opt out of parallel tests on CI. */
  workers: process.env.CI ? 1 : undefined,
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
  reporter: 'html',
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
  use: {
    trace: 'on-first-retry',
    baseURL: 'http://localhost:3000',
  },
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    cwd: './test',
    reuseExistingServer: !process.env.CI,
  },
  /* Configure projects for major browsers */
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },

    // {
    //   name: 'firefox',
    //   use: { ...devices['Desktop Firefox'] },
    // },

    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },

    /* Test against mobile viewports. */
    // {
    //   name: 'Mobile Chrome',
    //   use: { ...devices['Pixel 5'] },
    // },
    // {
    //   name: 'Mobile Safari',
    //   use: { ...devices['iPhone 12'] },
    // },

    /* Test against branded browsers. */
    // {
    //   name: 'Microsoft Edge',
    //   use: { channel: 'msedge' },
    // },
    // {
    //   name: 'Google Chrome',
    //   use: { channel: 'chrome' },
    // },
  ],
});


================================================
FILE: pnpm-workspace.yaml
================================================
packages:
  - 'website'
  - '.'
  - 'test'


================================================
FILE: src/assets.tsx
================================================
'use client';
import React from 'react';
import type { ToastTypes } from './types';

export const getAsset = (type: ToastTypes): JSX.Element | null => {
  switch (type) {
    case 'success':
      return SuccessIcon;

    case 'info':
      return InfoIcon;

    case 'warning':
      return WarningIcon;

    case 'error':
      return ErrorIcon;

    default:
      return null;
  }
};

const bars = Array(12).fill(0);

export const Loader = ({ visible, className }: { visible: boolean; className?: string }) => {
  return (
    <div className={['sonner-loading-wrapper', className].filter(Boolean).join(' ')} data-visible={visible}>
      <div className="sonner-spinner">
        {bars.map((_, i) => (
          <div className="sonner-loading-bar" key={`spinner-bar-${i}`} />
        ))}
      </div>
    </div>
  );
};

const SuccessIcon = (
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" height="20" width="20">
    <path
      fillRule="evenodd"
      d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
      clipRule="evenodd"
    />
  </svg>
);

const WarningIcon = (
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" height="20" width="20">
    <path
      fillRule="evenodd"
      d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z"
      clipRule="evenodd"
    />
  </svg>
);

const InfoIcon = (
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" height="20" width="20">
    <path
      fillRule="evenodd"
      d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
      clipRule="evenodd"
    />
  </svg>
);

const ErrorIcon = (
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" height="20" width="20">
    <path
      fillRule="evenodd"
      d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-5a.75.75 0 01.75.75v4.5a.75.75 0 01-1.5 0v-4.5A.75.75 0 0110 5zm0 10a1 1 0 100-2 1 1 0 000 2z"
      clipRule="evenodd"
    />
  </svg>
);

export const CloseIcon = (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="12"
    height="12"
    viewBox="0 0 24 24"
    fill="none"
    stroke="currentColor"
    strokeWidth="1.5"
    strokeLinecap="round"
    strokeLinejoin="round"
  >
    <line x1="18" y1="6" x2="6" y2="18"></line>
    <line x1="6" y1="6" x2="18" y2="18"></line>
  </svg>
);


================================================
FILE: src/hooks.tsx
================================================
import React from 'react';

export const useIsDocumentHidden = () => {
  const [isDocumentHidden, setIsDocumentHidden] = React.useState(document.hidden);

  React.useEffect(() => {
    const callback = () => {
      setIsDocumentHidden(document.hidden);
    };
    document.addEventListener('visibilitychange', callback);
    return () => document.removeEventListener('visibilitychange', callback);
  }, []);

  return isDocumentHidden;
};


================================================
FILE: src/index.tsx
================================================
'use client';

import React from 'react';
import ReactDOM from 'react-dom';

import { CloseIcon, getAsset, Loader } from './assets';
import { useIsDocumentHidden } from './hooks';
import { toast, ToastState } from './state';
import './styles.css';
import {
  isAction,
  SwipeDirection,
  type ExternalToast,
  type HeightT,
  type ToasterProps,
  type ToastProps,
  type ToastT,
  type ToastToDismiss,
} from './types';

// Visible toasts amount
const VISIBLE_TOASTS_AMOUNT = 3;

// Viewport padding
const VIEWPORT_OFFSET = '24px';

// Mobile viewport padding
const MOBILE_VIEWPORT_OFFSET = '16px';

// Default lifetime of a toasts (in ms)
const TOAST_LIFETIME = 4000;

// Default toast width
const TOAST_WIDTH = 356;

// Default gap between toasts
const GAP = 14;

// Threshold to dismiss a toast
const SWIPE_THRESHOLD = 45;

// Equal to exit animation duration
const TIME_BEFORE_UNMOUNT = 200;

function cn(...classes: (string | undefined)[]) {
  return classes.filter(Boolean).join(' ');
}

function getDefaultSwipeDirections(position: string): Array<SwipeDirection> {
  const [y, x] = position.split('-');
  const directions: Array<SwipeDirection> = [];

  if (y) {
    directions.push(y as SwipeDirection);
  }

  if (x) {
    directions.push(x as SwipeDirection);
  }

  return directions;
}

const Toast = (props: ToastProps) => {
  const {
    invert: ToasterInvert,
    toast,
    unstyled,
    interacting,
    setHeights,
    visibleToasts,
    heights,
    index,
    toasts,
    expanded,
    removeToast,
    defaultRichColors,
    closeButton: closeButtonFromToaster,
    style,
    cancelButtonStyle,
    actionButtonStyle,
    className = '',
    descriptionClassName = '',
    duration: durationFromToaster,
    position,
    gap,
    expandByDefault,
    classNames,
    icons,
    closeButtonAriaLabel = 'Close toast',
  } = props;
  const [swipeDirection, setSwipeDirection] = React.useState<'x' | 'y' | null>(null);
  const [swipeOutDirection, setSwipeOutDirection] = React.useState<'left' | 'right' | 'up' | 'down' | null>(null);
  const [mounted, setMounted] = React.useState(false);
  const [removed, setRemoved] = React.useState(false);
  const [swiping, setSwiping] = React.useState(false);
  const [swipeOut, setSwipeOut] = React.useState(false);
  const [isSwiped, setIsSwiped] = React.useState(false);
  const [offsetBeforeRemove, setOffsetBeforeRemove] = React.useState(0);
  const [initialHeight, setInitialHeight] = React.useState(0);
  const remainingTime = React.useRef(toast.duration || durationFromToaster || TOAST_LIFETIME);
  const dragStartTime = React.useRef<Date | null>(null);
  const toastRef = React.useRef<HTMLLIElement>(null);
  const isFront = index === 0;
  const isVisible = index + 1 <= visibleToasts;
  const toastType = toast.type;
  const dismissible = toast.dismissible !== false;
  const toastClassname = toast.className || '';
  const toastDescriptionClassname = toast.descriptionClassName || '';
  // Height index is used to calculate the offset as it gets updated before the toast array, which means we can calculate the new layout faster.
  const heightIndex = React.useMemo(
    () => heights.findIndex((height) => height.toastId === toast.id) || 0,
    [heights, toast.id],
  );
  const closeButton = React.useMemo(
    () => toast.closeButton ?? closeButtonFromToaster,
    [toast.closeButton, closeButtonFromToaster],
  );
  const duration = React.useMemo(
    () => toast.duration || durationFromToaster || TOAST_LIFETIME,
    [toast.duration, durationFromToaster],
  );
  const closeTimerStartTimeRef = React.useRef(0);
  const offset = React.useRef(0);
  const lastCloseTimerStartTimeRef = React.useRef(0);
  const pointerStartRef = React.useRef<{ x: number; y: number } | null>(null);
  const [y, x] = position.split('-');
  const toastsHeightBefore = React.useMemo(() => {
    return heights.reduce((prev, curr, reducerIndex) => {
      // Calculate offset up until current toast
      if (reducerIndex >= heightIndex) {
        return prev;
      }

      return prev + curr.height;
    }, 0);
  }, [heights, heightIndex]);
  const isDocumentHidden = useIsDocumentHidden();

  const invert = toast.invert || ToasterInvert;
  const disabled = toastType === 'loading';

  offset.current = React.useMemo(() => heightIndex * gap + toastsHeightBefore, [heightIndex, toastsHeightBefore]);

  React.useEffect(() => {
    remainingTime.current = duration;
  }, [duration]);

  React.useEffect(() => {
    // Trigger enter animation without using CSS animation
    setMounted(true);
  }, []);

  React.useEffect(() => {
    const toastNode = toastRef.current;
    if (toastNode) {
      const height = toastNode.getBoundingClientRect().height;
      // Add toast height to heights array after the toast is mounted
      setInitialHeight(height);
      setHeights((h) => [{ toastId: toast.id, height, position: toast.position }, ...h]);
      return () => setHeights((h) => h.filter((height) => height.toastId !== toast.id));
    }
  }, [setHeights, toast.id]);

  React.useLayoutEffect(() => {
    // Keep height up to date with the content in case it updates
    if (!mounted) return;
    const toastNode = toastRef.current;
    const originalHeight = toastNode.style.height;
    toastNode.style.height = 'auto';
    const newHeight = toastNode.getBoundingClientRect().height;
    toastNode.style.height = originalHeight;

    setInitialHeight(newHeight);

    setHeights((heights) => {
      const alreadyExists = heights.find((height) => height.toastId === toast.id);
      if (!alreadyExists) {
        return [{ toastId: toast.id, height: newHeight, position: toast.position }, ...heights];
      } else {
        return heights.map((height) => (height.toastId === toast.id ? { ...height, height: newHeight } : height));
      }
    });
  }, [mounted, toast.title, toast.description, setHeights, toast.id, toast.jsx, toast.action, toast.cancel]);

  const deleteToast = React.useCallback(() => {
    // Save the offset for the exit swipe animation
    setRemoved(true);
    setOffsetBeforeRemove(offset.current);
    setHeights((h) => h.filter((height) => height.toastId !== toast.id));

    setTimeout(() => {
      removeToast(toast);
    }, TIME_BEFORE_UNMOUNT);
  }, [toast, removeToast, setHeights, offset]);

  React.useEffect(() => {
    if ((toast.promise && toastType === 'loading') || toast.duration === Infinity || toast.type === 'loading') return;
    let timeoutId: NodeJS.Timeout;

    // Pause the timer on each hover
    const pauseTimer = () => {
      if (lastCloseTimerStartTimeRef.current < closeTimerStartTimeRef.current) {
        // Get the elapsed time since the timer started
        const elapsedTime = new Date().getTime() - closeTimerStartTimeRef.current;

        remainingTime.current = remainingTime.current - elapsedTime;
      }

      lastCloseTimerStartTimeRef.current = new Date().getTime();
    };

    const startTimer = () => {
      // setTimeout(, Infinity) behaves as if the delay is 0.
      // As a result, the toast would be closed immediately, giving the appearance that it was never rendered.
      // See: https://github.com/denysdovhan/wtfjs?tab=readme-ov-file#an-infinite-timeout
      if (remainingTime.current === Infinity) return;

      closeTimerStartTimeRef.current = new Date().getTime();

      // Let the toast know it has started
      timeoutId = setTimeout(() => {
        toast.onAutoClose?.(toast);
        deleteToast();
      }, remainingTime.current);
    };

    if (expanded || interacting || isDocumentHidden) {
      pauseTimer();
    } else {
      startTimer();
    }

    return () => clearTimeout(timeoutId);
  }, [expanded, interacting, toast, toastType, isDocumentHidden, deleteToast]);

  React.useEffect(() => {
    if (toast.delete) {
      deleteToast();
      toast.onDismiss?.(toast);
    }
  }, [deleteToast, toast.delete]);

  function getLoadingIcon() {
    if (icons?.loading) {
      return (
        <div
          className={cn(classNames?.loader, toast?.classNames?.loader, 'sonner-loader')}
          data-visible={toastType === 'loading'}
        >
          {icons.loading}
        </div>
      );
    }

    return <Loader className={cn(classNames?.loader, toast?.classNames?.loader)} visible={toastType === 'loading'} />;
  }

  const icon = toast.icon || icons?.[toastType] || getAsset(toastType);

  return (
    <li
      tabIndex={0}
      ref={toastRef}
      className={cn(
        className,
        toastClassname,
        classNames?.toast,
        toast?.classNames?.toast,
        classNames?.default,
        classNames?.[toastType],
        toast?.classNames?.[toastType],
      )}
      data-sonner-toast=""
      data-rich-colors={toast.richColors ?? defaultRichColors}
      data-styled={!Boolean(toast.jsx || toast.unstyled || unstyled)}
      data-mounted={mounted}
      data-promise={Boolean(toast.promise)}
      data-swiped={isSwiped}
      data-removed={removed}
      data-visible={isVisible}
      data-y-position={y}
      data-x-position={x}
      data-index={index}
      data-front={isFront}
      data-swiping={swiping}
      data-dismissible={dismissible}
      data-type={toastType}
      data-invert={invert}
      data-swipe-out={swipeOut}
      data-swipe-direction={swipeOutDirection}
      data-expanded={Boolean(expanded || (expandByDefault && mounted))}
      data-testid={toast.testId}
      style={
        {
          '--index': index,
          '--toasts-before': index,
          '--z-index': toasts.length - index,
          '--offset': `${removed ? offsetBeforeRemove : offset.current}px`,
          '--initial-height': expandByDefault ? 'auto' : `${initialHeight}px`,
          ...style,
          ...toast.style,
        } as React.CSSProperties
      }
      onDragEnd={() => {
        setSwiping(false);
        setSwipeDirection(null);
        pointerStartRef.current = null;
      }}
      onPointerDown={(event) => {
        if (event.button === 2) return; // Return early on right click
        if (disabled || !dismissible) return;
        dragStartTime.current = new Date();
        setOffsetBeforeRemove(offset.current);
        // Ensure we maintain correct pointer capture even when going outside of the toast (e.g. when swiping)
        (event.target as HTMLElement).setPointerCapture(event.pointerId);
        if ((event.target as HTMLElement).tagName === 'BUTTON') return;
        setSwiping(true);
        pointerStartRef.current = { x: event.clientX, y: event.clientY };
      }}
      onPointerUp={() => {
        if (swipeOut || !dismissible) return;

        pointerStartRef.current = null;
        const swipeAmountX = Number(
          toastRef.current?.style.getPropertyValue('--swipe-amount-x').replace('px', '') || 0,
        );
        const swipeAmountY = Number(
          toastRef.current?.style.getPropertyValue('--swipe-amount-y').replace('px', '') || 0,
        );
        const timeTaken = new Date().getTime() - dragStartTime.current?.getTime();

        const swipeAmount = swipeDirection === 'x' ? swipeAmountX : swipeAmountY;
        const velocity = Math.abs(swipeAmount) / timeTaken;

        if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
          setOffsetBeforeRemove(offset.current);

          toast.onDismiss?.(toast);

          if (swipeDirection === 'x') {
            setSwipeOutDirection(swipeAmountX > 0 ? 'right' : 'left');
          } else {
            setSwipeOutDirection(swipeAmountY > 0 ? 'down' : 'up');
          }

          deleteToast();
          setSwipeOut(true);

          return;
        } else {
          toastRef.current?.style.setProperty('--swipe-amount-x', `0px`);
          toastRef.current?.style.setProperty('--swipe-amount-y', `0px`);
        }
        setIsSwiped(false);
        setSwiping(false);
        setSwipeDirection(null);
      }}
      onPointerMove={(event) => {
        if (!pointerStartRef.current || !dismissible) return;

        const isHighlighted = window.getSelection()?.toString().length > 0;
        if (isHighlighted) return;

        const yDelta = event.clientY - pointerStartRef.current.y;
        const xDelta = event.clientX - pointerStartRef.current.x;

        const swipeDirections = props.swipeDirections ?? getDefaultSwipeDirections(position);

        // Determine swipe direction if not already locked
        if (!swipeDirection && (Math.abs(xDelta) > 1 || Math.abs(yDelta) > 1)) {
          setSwipeDirection(Math.abs(xDelta) > Math.abs(yDelta) ? 'x' : 'y');
        }

        let swipeAmount = { x: 0, y: 0 };

        const getDampening = (delta: number) => {
          const factor = Math.abs(delta) / 20;

          return 1 / (1.5 + factor);
        };

        // Only apply swipe in the locked direction
        if (swipeDirection === 'y') {
          // Handle vertical swipes
          if (swipeDirections.includes('top') || swipeDirections.includes('bottom')) {
            if ((swipeDirections.includes('top') && yDelta < 0) || (swipeDirections.includes('bottom') && yDelta > 0)) {
              swipeAmount.y = yDelta;
            } else {
              // Smoothly transition to dampened movement
              const dampenedDelta = yDelta * getDampening(yDelta);
              // Ensure we don't jump when transitioning to dampened movement
              swipeAmount.y = Math.abs(dampenedDelta) < Math.abs(yDelta) ? dampenedDelta : yDelta;
            }
          }
        } else if (swipeDirection === 'x') {
          // Handle horizontal swipes
          if (swipeDirections.includes('left') || swipeDirections.includes('right')) {
            if ((swipeDirections.includes('left') && xDelta < 0) || (swipeDirections.includes('right') && xDelta > 0)) {
              swipeAmount.x = xDelta;
            } else {
              // Smoothly transition to dampened movement
              const dampenedDelta = xDelta * getDampening(xDelta);
              // Ensure we don't jump when transitioning to dampened movement
              swipeAmount.x = Math.abs(dampenedDelta) < Math.abs(xDelta) ? dampenedDelta : xDelta;
            }
          }
        }

        if (Math.abs(swipeAmount.x) > 0 || Math.abs(swipeAmount.y) > 0) {
          setIsSwiped(true);
        }

        // Apply transform using both x and y values
        toastRef.current?.style.setProperty('--swipe-amount-x', `${swipeAmount.x}px`);
        toastRef.current?.style.setProperty('--swipe-amount-y', `${swipeAmount.y}px`);
      }}
    >
      {closeButton && !toast.jsx && toastType !== 'loading' ? (
        <button
          aria-label={closeButtonAriaLabel}
          data-disabled={disabled}
          data-close-button
          onClick={
            disabled || !dismissible
              ? () => {}
              : () => {
                  deleteToast();
                  toast.onDismiss?.(toast);
                }
          }
          className={cn(classNames?.closeButton, toast?.classNames?.closeButton)}
        >
          {icons?.close ?? CloseIcon}
        </button>
      ) : null}
      {/* TODO: This can be cleaner */}
      {(toastType || toast.icon || toast.promise) &&
      toast.icon !== null &&
      (icons?.[toastType] !== null || toast.icon) ? (
        <div data-icon="" className={cn(classNames?.icon, toast?.classNames?.icon)}>
          {toast.promise || (toast.type === 'loading' && !toast.icon) ? toast.icon || getLoadingIcon() : null}
          {toast.type !== 'loading' ? icon : null}
        </div>
      ) : null}

      <div data-content="" className={cn(classNames?.content, toast?.classNames?.content)}>
        <div data-title="" className={cn(classNames?.title, toast?.classNames?.title)}>
          {toast.jsx ? toast.jsx : typeof toast.title === 'function' ? toast.title() : toast.title}
        </div>
        {toast.description ? (
          <div
            data-description=""
            className={cn(
              descriptionClassName,
              toastDescriptionClassname,
              classNames?.description,
              toast?.classNames?.description,
            )}
          >
            {typeof toast.description === 'function' ? toast.description() : toast.description}
          </div>
        ) : null}
      </div>
      {React.isValidElement(toast.cancel) ? (
        toast.cancel
      ) : toast.cancel && isAction(toast.cancel) ? (
        <button
          data-button
          data-cancel
          style={toast.cancelButtonStyle || cancelButtonStyle}
          onClick={(event) => {
            // We need to check twice because typescript
            if (!isAction(toast.cancel)) return;
            if (!dismissible) return;
            toast.cancel.onClick?.(event);
            deleteToast();
          }}
          className={cn(classNames?.cancelButton, toast?.classNames?.cancelButton)}
        >
          {toast.cancel.label}
        </button>
      ) : null}
      {React.isValidElement(toast.action) ? (
        toast.action
      ) : toast.action && isAction(toast.action) ? (
        <button
          data-button
          data-action
          style={toast.actionButtonStyle || actionButtonStyle}
          onClick={(event) => {
            // We need to check twice because typescript
            if (!isAction(toast.action)) return;
            toast.action.onClick?.(event);
            if (event.defaultPrevented) return;
            deleteToast();
          }}
          className={cn(classNames?.actionButton, toast?.classNames?.actionButton)}
        >
          {toast.action.label}
        </button>
      ) : null}
    </li>
  );
};

function getDocumentDirection(): ToasterProps['dir'] {
  if (typeof window === 'undefined') return 'ltr';
  if (typeof document === 'undefined') return 'ltr'; // For Fresh purpose

  const dirAttribute = document.documentElement.getAttribute('dir');

  if (dirAttribute === 'auto' || !dirAttribute) {
    return window.getComputedStyle(document.documentElement).direction as ToasterProps['dir'];
  }

  return dirAttribute as ToasterProps['dir'];
}

function assignOffset(defaultOffset: ToasterProps['offset'], mobileOffset: ToasterProps['mobileOffset']) {
  const styles = {} as React.CSSProperties;

  [defaultOffset, mobileOffset].forEach((offset, index) => {
    const isMobile = index === 1;
    const prefix = isMobile ? '--mobile-offset' : '--offset';
    const defaultValue = isMobile ? MOBILE_VIEWPORT_OFFSET : VIEWPORT_OFFSET;

    function assignAll(offset: string | number) {
      ['top', 'right', 'bottom', 'left'].forEach((key) => {
        styles[`${prefix}-${key}`] = typeof offset === 'number' ? `${offset}px` : offset;
      });
    }

    if (typeof offset === 'number' || typeof offset === 'string') {
      assignAll(offset);
    } else if (typeof offset === 'object') {
      ['top', 'right', 'bottom', 'left'].forEach((key) => {
        if (offset[key] === undefined) {
          styles[`${prefix}-${key}`] = defaultValue;
        } else {
          styles[`${prefix}-${key}`] = typeof offset[key] === 'number' ? `${offset[key]}px` : offset[key];
        }
      });
    } else {
      assignAll(defaultValue);
    }
  });

  return styles;
}

function useSonner() {
  const [activeToasts, setActiveToasts] = React.useState<ToastT[]>([]);

  React.useEffect(() => {
    return ToastState.subscribe((toast) => {
      if ((toast as ToastToDismiss).dismiss) {
        setTimeout(() => {
          ReactDOM.flushSync(() => {
            setActiveToasts((toasts) => toasts.filter((t) => t.id !== toast.id));
          });
        });
        return;
      }

      // Prevent batching, temp solution.
      setTimeout(() => {
        ReactDOM.flushSync(() => {
          setActiveToasts((toasts) => {
            const indexOfExistingToast = toasts.findIndex((t) => t.id === toast.id);

            // Update the toast if it already exists
            if (indexOfExistingToast !== -1) {
              return [
                ...toasts.slice(0, indexOfExistingToast),
                { ...toasts[indexOfExistingToast], ...toast },
                ...toasts.slice(indexOfExistingToast + 1),
              ];
            }

            return [toast, ...toasts];
          });
        });
      });
    });
  }, []);

  return {
    toasts: activeToasts,
  };
}

const Toaster = React.forwardRef<HTMLElement, ToasterProps>(function Toaster(props, ref) {
  const {
    id,
    invert,
    position = 'bottom-right',
    hotkey = ['altKey', 'KeyT'],
    expand,
    closeButton,
    className,
    offset,
    mobileOffset,
    theme = 'light',
    richColors,
    duration,
    style,
    visibleToasts = VISIBLE_TOASTS_AMOUNT,
    toastOptions,
    dir = getDocumentDirection(),
    gap = GAP,
    icons,
    customAriaLabel,
    containerAriaLabel = 'Notifications',
  } = props;
  const [toasts, setToasts] = React.useState<ToastT[]>([]);
  const filteredToasts = React.useMemo(() => {
    if (id) {
      return toasts.filter((toast) => toast.toasterId === id);
    }
    return toasts.filter((toast) => !toast.toasterId);
  }, [toasts, id]);
  const possiblePositions = React.useMemo(() => {
    return Array.from(
      new Set([position].concat(filteredToasts.filter((toast) => toast.position).map((toast) => toast.position))),
    );
  }, [filteredToasts, position]);
  const [heights, setHeights] = React.useState<HeightT[]>([]);
  const [expanded, setExpanded] = React.useState(false);
  const [interacting, setInteracting] = React.useState(false);
  const [actualTheme, setActualTheme] = React.useState(
    theme !== 'system'
      ? theme
      : typeof window !== 'undefined'
      ? window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
        ? 'dark'
        : 'light'
      : 'light',
  );

  const listRef = React.useRef<HTMLOListElement>(null);
  const hotkeyLabel = hotkey.join('+').replace(/Key/g, '').replace(/Digit/g, '');
  const lastFocusedElementRef = React.useRef<HTMLElement>(null);
  const isFocusWithinRef = React.useRef(false);

  const removeToast = React.useCallback((toastToRemove: ToastT) => {
    setToasts((toasts) => {
      if (!toasts.find((toast) => toast.id === toastToRemove.id)?.delete) {
        ToastState.dismiss(toastToRemove.id);
      }

      return toasts.filter(({ id }) => id !== toastToRemove.id);
    });
  }, []);

  React.useEffect(() => {
    return ToastState.subscribe((toast) => {
      if ((toast as ToastToDismiss).dismiss) {
        // Prevent batching of other state updates
        requestAnimationFrame(() => {
          setToasts((toasts) => toasts.map((t) => (t.id === toast.id ? { ...t, delete: true } : t)));
        });
        return;
      }

      // Prevent batching, temp solution.
      setTimeout(() => {
        ReactDOM.flushSync(() => {
          setToasts((toasts) => {
            const indexOfExistingToast = toasts.findIndex((t) => t.id === toast.id);

            // Update the toast if it already exists
            if (indexOfExistingToast !== -1) {
              return [
                ...toasts.slice(0, indexOfExistingToast),
                { ...toasts[indexOfExistingToast], ...toast },
                ...toasts.slice(indexOfExistingToast + 1),
              ];
            }

            return [toast, ...toasts];
          });
        });
      });
    });
  }, [toasts]);

  React.useEffect(() => {
    if (theme !== 'system') {
      setActualTheme(theme);
      return;
    }

    if (theme === 'system') {
      // check if current preference is dark
      if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
        // it's currently dark
        setActualTheme('dark');
      } else {
        // it's not dark
        setActualTheme('light');
      }
    }

    if (typeof window === 'undefined') return;
    const darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');

    try {
      // Chrome & Firefox
      darkMediaQuery.addEventListener('change', ({ matches }) => {
        if (matches) {
          setActualTheme('dark');
        } else {
          setActualTheme('light');
        }
      });
    } catch (error) {
      // Safari < 14
      darkMediaQuery.addListener(({ matches }) => {
        try {
          if (matches) {
            setActualTheme('dark');
          } else {
            setActualTheme('light');
          }
        } catch (e) {
          console.error(e);
        }
      });
    }
  }, [theme]);

  React.useEffect(() => {
    // Ensure expanded is always false when no toasts are present / only one left
    if (toasts.length <= 1) {
      setExpanded(false);
    }
  }, [toasts]);

  React.useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      const isHotkeyPressed = hotkey.length > 0 && hotkey.every((key) => (event as any)[key] || event.code === key);

      if (isHotkeyPressed) {
        setExpanded(true);
        listRef.current?.focus();
      }

      if (
        event.code === 'Escape' &&
        (document.activeElement === listRef.current || listRef.current?.contains(document.activeElement))
      ) {
        setExpanded(false);
      }
    };
    document.addEventListener('keydown', handleKeyDown);

    return () => document.removeEventListener('keydown', handleKeyDown);
  }, [hotkey]);

  React.useEffect(() => {
    if (listRef.current) {
      return () => {
        if (lastFocusedElementRef.current) {
          lastFocusedElementRef.current.focus({ preventScroll: true });
          lastFocusedElementRef.current = null;
          isFocusWithinRef.current = false;
        }
      };
    }
  }, [listRef.current]);

  return (
    // Remove item from normal navigation flow, only available via hotkey
    <section
      ref={ref}
      aria-label={customAriaLabel ?? `${containerAriaLabel} ${hotkeyLabel}`}
      tabIndex={-1}
      aria-live="polite"
      aria-relevant="additions text"
      aria-atomic="false"
      suppressHydrationWarning
      data-react-aria-top-layer
    >
      {possiblePositions.map((position, index) => {
        const [y, x] = position.split('-');

        if (!filteredToasts.length) return null;

        return (
          <ol
            key={position}
            dir={dir === 'auto' ? getDocumentDirection() : dir}
            tabIndex={-1}
            ref={listRef}
            className={className}
            data-sonner-toaster
            data-sonner-theme={actualTheme}
            data-y-position={y}
            data-x-position={x}
            style={
              {
                '--front-toast-height': `${heights[0]?.height || 0}px`,
                '--width': `${TOAST_WIDTH}px`,
                '--gap': `${gap}px`,
                ...style,
                ...assignOffset(offset, mobileOffset),
              } as React.CSSProperties
            }
            onBlur={(event) => {
              if (isFocusWithinRef.current && !event.currentTarget.contains(event.relatedTarget)) {
                isFocusWithinRef.current = false;
                if (lastFocusedElementRef.current) {
                  lastFocusedElementRef.current.focus({ preventScroll: true });
                  lastFocusedElementRef.current = null;
                }
              }
            }}
            onFocus={(event) => {
              const isNotDismissible =
                event.target instanceof HTMLElement && event.target.dataset.dismissible === 'false';

              if (isNotDismissible) return;

              if (!isFocusWithinRef.current) {
                isFocusWithinRef.current = true;
                lastFocusedElementRef.current = event.relatedTarget as HTMLElement;
              }
            }}
            onMouseEnter={() => setExpanded(true)}
            onMouseMove={() => setExpanded(true)}
            onMouseLeave={() => {
              // Avoid setting expanded to false when interacting with a toast, e.g. swiping
              if (!interacting) {
                setExpanded(false);
              }
            }}
            onDragEnd={() => setExpanded(false)}
            onPointerDown={(event) => {
              const isNotDismissible =
                event.target instanceof HTMLElement && event.target.dataset.dismissible === 'false';

              if (isNotDismissible) return;
              setInteracting(true);
            }}
            onPointerUp={() => setInteracting(false)}
          >
            {filteredToasts
              .filter((toast) => (!toast.position && index === 0) || toast.position === position)
              .map((toast, index) => (
                <Toast
                  key={toast.id}
                  icons={icons}
                  index={index}
                  toast={toast}
                  defaultRichColors={richColors}
                  duration={toastOptions?.duration ?? duration}
                  className={toastOptions?.className}
                  descriptionClassName={toastOptions?.descriptionClassName}
                  invert={invert}
                  visibleToasts={visibleToasts}
                  closeButton={toastOptions?.closeButton ?? closeButton}
                  interacting={interacting}
                  position={position}
                  style={toastOptions?.style}
                  unstyled={toastOptions?.unstyled}
                  classNames={toastOptions?.classNames}
                  cancelButtonStyle={toastOptions?.cancelButtonStyle}
                  actionButtonStyle={toastOptions?.actionButtonStyle}
                  closeButtonAriaLabel={toastOptions?.closeButtonAriaLabel}
                  removeToast={removeToast}
                  toasts={filteredToasts.filter((t) => t.position == toast.position)}
                  heights={heights.filter((h) => h.position == toast.position)}
                  setHeights={setHeights}
                  expandByDefault={expand}
                  gap={gap}
                  expanded={expanded}
                  swipeDirections={props.swipeDirections}
                />
              ))}
          </ol>
        );
      })}
    </section>
  );
});

export { toast, Toaster, type ExternalToast, type ToastT, type ToasterProps, useSonner };
export { type ToastClassnames, type ToastToDismiss, type Action } from './types';


================================================
FILE: src/state.ts
================================================
import type {
  ExternalToast,
  PromiseData,
  PromiseIExtendedResult,
  PromiseT,
  ToastT,
  ToastToDismiss,
  ToastTypes,
} from './types';

import React from 'react';

let toastsCounter = 1;

type titleT = (() => React.ReactNode) | React.ReactNode;

class Observer {
  subscribers: Array<(toast: ExternalToast | ToastToDismiss) => void>;
  toasts: Array<ToastT | ToastToDismiss>;
  dismissedToasts: Set<string | number>;

  constructor() {
    this.subscribers = [];
    this.toasts = [];
    this.dismissedToasts = new Set();
  }

  // We use arrow functions to maintain the correct `this` reference
  subscribe = (subscriber: (toast: ToastT | ToastToDismiss) => void) => {
    this.subscribers.push(subscriber);

    return () => {
      const index = this.subscribers.indexOf(subscriber);
      this.subscribers.splice(index, 1);
    };
  };

  publish = (data: ToastT) => {
    this.subscribers.forEach((subscriber) => subscriber(data));
  };

  addToast = (data: ToastT) => {
    this.publish(data);
    this.toasts = [...this.toasts, data];
  };

  create = (
    data: ExternalToast & {
      message?: titleT;
      type?: ToastTypes;
      promise?: PromiseT;
      jsx?: React.ReactElement;
    },
  ) => {
    const { message, ...rest } = data;
    const id = typeof data?.id === 'number' || data.id?.length > 0 ? data.id : toastsCounter++;
    const alreadyExists = this.toasts.find((toast) => {
      return toast.id === id;
    });
    const dismissible = data.dismissible === undefined ? true : data.dismissible;

    if (this.dismissedToasts.has(id)) {
      this.dismissedToasts.delete(id);
    }

    if (alreadyExists) {
      this.toasts = this.toasts.map((toast) => {
        if (toast.id === id) {
          this.publish({ ...toast, ...data, id, title: message });
          return {
            ...toast,
            ...data,
            id,
            dismissible,
            title: message,
          };
        }

        return toast;
      });
    } else {
      this.addToast({ title: message, ...rest, dismissible, id });
    }

    return id;
  };

  dismiss = (id?: number | string) => {
    if (id) {
      this.dismissedToasts.add(id);
      requestAnimationFrame(() => this.subscribers.forEach((subscriber) => subscriber({ id, dismiss: true })));
    } else {
      this.toasts.forEach((toast) => {
        this.subscribers.forEach((subscriber) => subscriber({ id: toast.id, dismiss: true }));
      });
    }

    return id;
  };

  message = (message: titleT | React.ReactNode, data?: ExternalToast) => {
    return this.create({ ...data, message });
  };

  error = (message: titleT | React.ReactNode, data?: ExternalToast) => {
    return this.create({ ...data, message, type: 'error' });
  };

  success = (message: titleT | React.ReactNode, data?: ExternalToast) => {
    return this.create({ ...data, type: 'success', message });
  };

  info = (message: titleT | React.ReactNode, data?: ExternalToast) => {
    return this.create({ ...data, type: 'info', message });
  };

  warning = (message: titleT | React.ReactNode, data?: ExternalToast) => {
    return this.create({ ...data, type: 'warning', message });
  };

  loading = (message: titleT | React.ReactNode, data?: ExternalToast) => {
    return this.create({ ...data, type: 'loading', message });
  };

  promise = <ToastData>(promise: PromiseT<ToastData>, data?: PromiseData<ToastData>) => {
    if (!data) {
      // Nothing to show
      return;
    }

    let id: string | number | undefined = undefined;
    if (data.loading !== undefined) {
      id = this.create({
        ...data,
        promise,
        type: 'loading',
        message: data.loading,
        description: typeof data.description !== 'function' ? data.description : undefined,
      });
    }

    const p = Promise.resolve(promise instanceof Function ? promise() : promise);

    let shouldDismiss = id !== undefined;
    let result: ['resolve', ToastData] | ['reject', unknown];

    const originalPromise = p
      .then(async (response) => {
        result = ['resolve', response];
        const isReactElementResponse = React.isValidElement(response);
        if (isReactElementResponse) {
          shouldDismiss = false;
          this.create({ id, type: 'default', message: response });
        } else if (isHttpResponse(response) && !response.ok) {
          shouldDismiss = false;

          const promiseData =
            typeof data.error === 'function' ? await data.error(`HTTP error! status: ${response.status}`) : data.error;

          const description =
            typeof data.description === 'function'
              ? await data.description(`HTTP error! status: ${response.status}`)
              : data.description;

          const isExtendedResult = typeof promiseData === 'object' && !React.isValidElement(promiseData);

          const toastSettings: PromiseIExtendedResult = isExtendedResult
            ? (promiseData as PromiseIExtendedResult)
            : { message: promiseData };

          this.create({ id, type: 'error', description, ...toastSettings });
        } else if (response instanceof Error) {
          shouldDismiss = false;

          const promiseData = typeof data.error === 'function' ? await data.error(response) : data.error;

          const description =
            typeof data.description === 'function' ? await data.description(response) : data.description;

          const isExtendedResult = typeof promiseData === 'object' && !React.isValidElement(promiseData);

          const toastSettings: PromiseIExtendedResult = isExtendedResult
            ? (promiseData as PromiseIExtendedResult)
            : { message: promiseData };

          this.create({ id, type: 'error', description, ...toastSettings });
        } else if (data.success !== undefined) {
          shouldDismiss = false;
          const promiseData = typeof data.success === 'function' ? await data.success(response) : data.success;

          const description =
            typeof data.description === 'function' ? await data.description(response) : data.description;

          const isExtendedResult = typeof promiseData === 'object' && !React.isValidElement(promiseData);

          const toastSettings: PromiseIExtendedResult = isExtendedResult
            ? (promiseData as PromiseIExtendedResult)
            : { message: promiseData };

          this.create({ id, type: 'success', description, ...toastSettings });
        }
      })
      .catch(async (error) => {
        result = ['reject', error];
        if (data.error !== undefined) {
          shouldDismiss = false;
          const promiseData = typeof data.error === 'function' ? await data.error(error) : data.error;

          const description = typeof data.description === 'function' ? await data.description(error) : data.description;

          const isExtendedResult = typeof promiseData === 'object' && !React.isValidElement(promiseData);

          const toastSettings: PromiseIExtendedResult = isExtendedResult
            ? (promiseData as PromiseIExtendedResult)
            : { message: promiseData };

          this.create({ id, type: 'error', description, ...toastSettings });
        }
      })
      .finally(() => {
        if (shouldDismiss) {
          // Toast is still in load state (and will be indefinitely — dismiss it)
          this.dismiss(id);
          id = undefined;
        }

        data.finally?.();
      });

    const unwrap = () =>
      new Promise<ToastData>((resolve, reject) =>
        originalPromise.then(() => (result[0] === 'reject' ? reject(result[1]) : resolve(result[1]))).catch(reject),
      );

    if (typeof id !== 'string' && typeof id !== 'number') {
      // cannot Object.assign on undefined
      return { unwrap };
    } else {
      return Object.assign(id, { unwrap });
    }
  };

  custom = (jsx: (id: number | string) => React.ReactElement, data?: ExternalToast) => {
    const id = data?.id || toastsCounter++;
    this.create({ jsx: jsx(id), ...data, id });
    return id;
  };

  getActiveToasts = () => {
    return this.toasts.filter((toast) => !this.dismissedToasts.has(toast.id));
  };
}

export const ToastState = new Observer();

// bind this to the toast function
const toastFunction = (message: titleT, data?: ExternalToast) => {
  const id = data?.id || toastsCounter++;

  ToastState.addToast({
    title: message,
    ...data,
    id,
  });
  return id;
};

const isHttpResponse = (data: any): data is Response => {
  return (
    data &&
    typeof data === 'object' &&
    'ok' in data &&
    typeof data.ok === 'boolean' &&
    'status' in data &&
    typeof data.status === 'number'
  );
};

const basicToast = toastFunction;

const getHistory = () => ToastState.toasts;
const getToasts = () => ToastState.getActiveToasts();

// We use `Object.assign` to maintain the correct types as we would lose them otherwise
export const toast = Object.assign(
  basicToast,
  {
    success: ToastState.success,
    info: ToastState.info,
    warning: ToastState.warning,
    error: ToastState.error,
    custom: ToastState.custom,
    message: ToastState.message,
    promise: ToastState.promise,
    dismiss: ToastState.dismiss,
    loading: ToastState.loading,
  },
  { getHistory, getToasts },
);


================================================
FILE: src/styles.css
================================================
html[dir='ltr'],
[data-sonner-toaster][dir='ltr'] {
  --toast-icon-margin-start: -3px;
  --toast-icon-margin-end: 4px;
  --toast-svg-margin-start: -1px;
  --toast-svg-margin-end: 0px;
  --toast-button-margin-start: auto;
  --toast-button-margin-end: 0;
  --toast-close-button-start: 0;
  --toast-close-button-end: unset;
  --toast-close-button-transform: translate(-35%, -35%);
}

html[dir='rtl'],
[data-sonner-toaster][dir='rtl'] {
  --toast-icon-margin-start: 4px;
  --toast-icon-margin-end: -3px;
  --toast-svg-margin-start: 0px;
  --toast-svg-margin-end: -1px;
  --toast-button-margin-start: 0;
  --toast-button-margin-end: auto;
  --toast-close-button-start: unset;
  --toast-close-button-end: 0;
  --toast-close-button-transform: translate(35%, -35%);
}

[data-sonner-toaster] {
  position: fixed;
  width: var(--width);
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial,
    Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
  --gray1: hsl(0, 0%, 99%);
  --gray2: hsl(0, 0%, 97.3%);
  --gray3: hsl(0, 0%, 95.1%);
  --gray4: hsl(0, 0%, 93%);
  --gray5: hsl(0, 0%, 90.9%);
  --gray6: hsl(0, 0%, 88.7%);
  --gray7: hsl(0, 0%, 85.8%);
  --gray8: hsl(0, 0%, 78%);
  --gray9: hsl(0, 0%, 56.1%);
  --gray10: hsl(0, 0%, 52.3%);
  --gray11: hsl(0, 0%, 43.5%);
  --gray12: hsl(0, 0%, 9%);
  --border-radius: 8px;
  box-sizing: border-box;
  padding: 0;
  margin: 0;
  list-style: none;
  outline: none;
  z-index: 999999999;
  transition: transform 400ms ease;
}

@media (hover: none) and (pointer: coarse) {
  [data-sonner-toaster][data-lifted='true'] {
    transform: none;
  }
}

[data-sonner-toaster][data-x-position='right'] {
  right: var(--offset-right);
}

[data-sonner-toaster][data-x-position='left'] {
  left: var(--offset-left);
}

[data-sonner-toaster][data-x-position='center'] {
  left: 50%;
  transform: translateX(-50%);
}

[data-sonner-toaster][data-y-position='top'] {
  top: var(--offset-top);
}

[data-sonner-toaster][data-y-position='bottom'] {
  bottom: var(--offset-bottom);
}

[data-sonner-toast] {
  --y: translateY(100%);
  --lift-amount: calc(var(--lift) * var(--gap));
  z-index: var(--z-index);
  position: absolute;
  opacity: 0;
  transform: var(--y);
  touch-action: none;
  transition: transform 400ms, opacity 400ms, height 400ms, box-shadow 200ms;
  box-sizing: border-box;
  outline: none;
  overflow-wrap: anywhere;
}

[data-sonner-toast][data-styled='true'] {
  padding: 16px;
  background: var(--normal-bg);
  border: 1px solid var(--normal-border);
  color: var(--normal-text);
  border-radius: var(--border-radius);
  box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
  width: var(--width);
  font-size: 13px;
  display: flex;
  align-items: center;
  gap: 6px;
}

[data-sonner-toast]:focus-visible {
  box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2);
}

[data-sonner-toast][data-y-position='top'] {
  top: 0;
  --y: translateY(-100%);
  --lift: 1;
  --lift-amount: calc(1 * var(--gap));
}

[data-sonner-toast][data-y-position='bottom'] {
  bottom: 0;
  --y: translateY(100%);
  --lift: -1;
  --lift-amount: calc(var(--lift) * var(--gap));
}

[data-sonner-toast][data-styled='true'] [data-description] {
  font-weight: 400;
  line-height: 1.4;
  color: #3f3f3f;
}

[data-rich-colors='true'][data-sonner-toast][data-styled='true'] [data-description] {
  color: inherit;
}

[data-sonner-toaster][data-sonner-theme='dark'] [data-description] {
  color: hsl(0, 0%, 91%);
}

[data-sonner-toast][data-styled='true'] [data-title] {
  font-weight: 500;
  line-height: 1.5;
  color: inherit;
}

[data-sonner-toast][data-styled='true'] [data-icon] {
  display: flex;
  height: 16px;
  width: 16px;
  position: relative;
  justify-content: flex-start;
  align-items: center;
  flex-shrink: 0;
  margin-left: var(--toast-icon-margin-start);
  margin-right: var(--toast-icon-margin-end);
}

[data-sonner-toast][data-promise='true'] [data-icon] > svg {
  opacity: 0;
  transform: scale(0.8);
  transform-origin: center;
  animation: sonner-fade-in 300ms ease forwards;
}

[data-sonner-toast][data-styled='true'] [data-icon] > * {
  flex-shrink: 0;
}

[data-sonner-toast][data-styled='true'] [data-icon] svg {
  margin-left: var(--toast-svg-margin-start);
  margin-right: var(--toast-svg-margin-end);
}

[data-sonner-toast][data-styled='true'] [data-content] {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

[data-sonner-toast][data-styled='true'] [data-button] {
  border-radius: 4px;
  padding-left: 8px;
  padding-right: 8px;
  height: 24px;
  font-size: 12px;
  color: var(--normal-bg);
  background: var(--normal-text);
  margin-left: var(--toast-button-margin-start);
  margin-right: var(--toast-button-margin-end);
  border: none;
  font-weight: 500;
  cursor: pointer;
  outline: none;
  display: flex;
  align-items: center;
  flex-shrink: 0;
  transition: opacity 400ms, box-shadow 200ms;
}

[data-sonner-toast][data-styled='true'] [data-button]:focus-visible {
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4);
}

[data-sonner-toast][data-styled='true'] [data-button]:first-of-type {
  margin-left: var(--toast-button-margin-start);
  margin-right: var(--toast-button-margin-end);
}

[data-sonner-toast][data-styled='true'] [data-cancel] {
  color: var(--normal-text);
  background: rgba(0, 0, 0, 0.08);
}

[data-sonner-toaster][data-sonner-theme='dark'] [data-sonner-toast][data-styled='true'] [data-cancel] {
  background: rgba(255, 255, 255, 0.3);
}

[data-sonner-toast][data-styled='true'] [data-close-button] {
  position: absolute;
  left: var(--toast-close-button-start);
  right: var(--toast-close-button-end);
  top: 0;
  height: 20px;
  width: 20px;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 0;
  color: var(--normal-text);
  background: var(--normal-bg);
  border: 1px solid var(--normal-border);
  transform: var(--toast-close-button-transform);
  border-radius: 50%;
  cursor: pointer;
  z-index: 1;
  transition: opacity 100ms, background 200ms, border-color 200ms;
}

[data-sonner-toast][data-styled='true'] [data-close-button]:focus-visible {
  box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2);
}

[data-sonner-toast][data-styled='true'] [data-disabled='true'] {
  cursor: not-allowed;
}

[data-sonner-toast][data-styled='true']:hover [data-close-button]:hover {
  background: var(--gray2);
  border-color: var(--gray5);
}

[data-sonner-toast][data-swiping='true']::before {
  content: '';
  position: absolute;
  left: -100%;
  right: -100%;
  height: 100%;
  z-index: -1;
}

[data-sonner-toast][data-y-position='top'][data-swiping='true']::before {
  bottom: 50%;
  transform: scaleY(3) translateY(50%);
}

[data-sonner-toast][data-y-position='bottom'][data-swiping='true']::before {
  top: 50%;
  transform: scaleY(3) translateY(-50%);
}

[data-sonner-toast][data-swiping='false'][data-removed='true']::before {
  content: '';
  position: absolute;
  inset: 0;
  transform: scaleY(2);
}

[data-sonner-toast][data-expanded='true']::after {
  content: '';
  position: absolute;
  left: 0;
  height: calc(var(--gap) + 1px);
  bottom: 100%;
  width: 100%;
}

[data-sonner-toast][data-mounted='true'] {
  --y: translateY(0);
  opacity: 1;
}

[data-sonner-toast][data-expanded='false'][data-front='false'] {
  --scale: var(--toasts-before) * 0.05 + 1;
  --y: translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale)));
  height: var(--front-toast-height);
}

[data-sonner-toast] > * {
  transition: opacity 400ms;
}

[data-sonner-toast][data-x-position='right'] {
  right: 0;
}

[data-sonner-toast][data-x-position='left'] {
  left: 0;
}

[data-sonner-toast][data-expanded='false'][data-front='false'][data-styled='true'] > * {
  opacity: 0;
}

[data-sonner-toast][data-visible='false'] {
  opacity: 0;
  pointer-events: none;
}

[data-sonner-toast][data-mounted='true'][data-expanded='true'] {
  --y: translateY(calc(var(--lift) * var(--offset)));
  height: var(--initial-height);
}

[data-sonner-toast][data-removed='true'][data-front='true'][data-swipe-out='false'] {
  --y: translateY(calc(var(--lift) * -100%));
  opacity: 0;
}

[data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='true'] {
  --y: translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%));
  opacity: 0;
}

[data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='false'] {
  --y: translateY(40%);
  opacity: 0;
  transition: transform 500ms, opacity 200ms;
}

[data-sonner-toast][data-removed='true'][data-front='false']::before {
  height: calc(var(--initial-height) + 20%);
}

[data-sonner-toast][data-swiping='true'] {
  transform: var(--y) translateY(var(--swipe-amount-y, 0px)) translateX(var(--swipe-amount-x, 0px));
  transition: none;
}

[data-sonner-toast][data-swiped='true'] {
  -webkit-user-select: none; /* Safari 3+ */
  user-select: none;
}

[data-sonner-toast][data-swipe-out='true'][data-y-position='bottom'],
[data-sonner-toast][data-swipe-out='true'][data-y-position='top'] {
  animation-duration: 200ms;
  animation-timing-function: ease-out;
  animation-fill-mode: forwards;
}

[data-sonner-toast][data-swipe-out='true'][data-swipe-direction='left'] {
  animation-name: swipe-out-left;
}

[data-sonner-toast][data-swipe-out='true'][data-swipe-direction='right'] {
  animation-name: swipe-out-right;
}

[data-sonner-toast][data-swipe-out='true'][data-swipe-direction='up'] {
  animation-name: swipe-out-up;
}

[data-sonner-toast][data-swipe-out='true'][data-swipe-direction='down'] {
  animation-name: swipe-out-down;
}

@keyframes swipe-out-left {
  from {
    transform: var(--y) translateX(var(--swipe-amount-x));
    opacity: 1;
  }

  to {
    transform: var(--y) translateX(calc(var(--swipe-amount-x) - 100%));
    opacity: 0;
  }
}

@keyframes swipe-out-right {
  from {
    transform: var(--y) translateX(var(--swipe-amount-x));
    opacity: 1;
  }

  to {
    transform: var(--y) translateX(calc(var(--swipe-amount-x) + 100%));
    opacity: 0;
  }
}

@keyframes swipe-out-up {
  from {
    transform: var(--y) translateY(var(--swipe-amount-y));
    opacity: 1;
  }

  to {
    transform: var(--y) translateY(calc(var(--swipe-amount-y) - 100%));
    opacity: 0;
  }
}

@keyframes swipe-out-down {
  from {
    transform: var(--y) translateY(var(--swipe-amount-y));
    opacity: 1;
  }

  to {
    transform: var(--y) translateY(calc(var(--swipe-amount-y) + 100%));
    opacity: 0;
  }
}

@media (max-width: 600px) {
  [data-sonner-toaster] {
    position: fixed;
    right: var(--mobile-offset-right);
    left: var(--mobile-offset-left);
    width: 100%;
  }

  [data-sonner-toaster][dir='rtl'] {
    left: calc(var(--mobile-offset-left) * -1);
  }

  [data-sonner-toaster] [data-sonner-toast] {
    left: 0;
    right: 0;
    width: calc(100% - var(--mobile-offset-left) * 2);
  }

  [data-sonner-toaster][data-x-position='left'] {
    left: var(--mobile-offset-left);
  }

  [data-sonner-toaster][data-y-position='bottom'] {
    bottom: var(--mobile-offset-bottom);
  }

  [data-sonner-toaster][data-y-position='top'] {
    top: var(--mobile-offset-top);
  }

  [data-sonner-toaster][data-x-position='center'] {
    left: var(--mobile-offset-left);
    right: var(--mobile-offset-right);
    transform: none;
  }
}

[data-sonner-toaster][data-sonner-theme='light'] {
  --normal-bg: #fff;
  --normal-border: var(--gray4);
  --normal-text: var(--gray12);

  --success-bg: hsl(143, 85%, 96%);
  --success-border: hsl(145, 92%, 87%);
  --success-text: hsl(140, 100%, 27%);

  --info-bg: hsl(208, 100%, 97%);
  --info-border: hsl(221, 91%, 93%);
  --info-text: hsl(210, 92%, 45%);

  --warning-bg: hsl(49, 100%, 97%);
  --warning-border: hsl(49, 91%, 84%);
  --warning-text: hsl(31, 92%, 45%);

  --error-bg: hsl(359, 100%, 97%);
  --error-border: hsl(359, 100%, 94%);
  --error-text: hsl(360, 100%, 45%);
}

[data-sonner-toaster][data-sonner-theme='light'] [data-sonner-toast][data-invert='true'] {
  --normal-bg: #000;
  --normal-border: hsl(0, 0%, 20%);
  --normal-text: var(--gray1);
}

[data-sonner-toaster][data-sonner-theme='dark'] [data-sonner-toast][data-invert='true'] {
  --normal-bg: #fff;
  --normal-border: var(--gray3);
  --normal-text: var(--gray12);
}

[data-sonner-toaster][data-sonner-theme='dark'] {
  --normal-bg: #000;
  --normal-bg-hover: hsl(0, 0%, 12%);
  --normal-border: hsl(0, 0%, 20%);
  --normal-border-hover: hsl(0, 0%, 25%);
  --normal-text: var(--gray1);

  --success-bg: hsl(150, 100%, 6%);
  --success-border: hsl(147, 100%, 12%);
  --success-text: hsl(150, 86%, 65%);

  --info-bg: hsl(215, 100%, 6%);
  --info-border: hsl(223, 43%, 17%);
  --info-text: hsl(216, 87%, 65%);

  --warning-bg: hsl(64, 100%, 6%);
  --warning-border: hsl(60, 100%, 9%);
  --warning-text: hsl(46, 87%, 65%);

  --error-bg: hsl(358, 76%, 10%);
  --error-border: hsl(357, 89%, 16%);
  --error-text: hsl(358, 100%, 81%);
}

[data-sonner-toaster][data-sonner-theme='dark'] [data-sonner-toast] [data-close-button] {
  background: var(--normal-bg);
  border-color: var(--normal-border);
  color: var(--normal-text);
}

[data-sonner-toaster][data-sonner-theme='dark'] [data-sonner-toast] [data-close-button]:hover {
  background: var(--normal-bg-hover);
  border-color: var(--normal-border-hover);
}

[data-rich-colors='true'][data-sonner-toast][data-type='success'] {
  background: var(--success-bg);
  border-color: var(--success-border);
  color: var(--success-text);
}

[data-rich-colors='true'][data-sonner-toast][data-type='success'] [data-close-button] {
  background: var(--success-bg);
  border-color: var(--success-border);
  color: var(--success-text);
}

[data-rich-colors='true'][data-sonner-toast][data-type='info'] {
  background: var(--info-bg);
  border-color: var(--info-border);
  color: var(--info-text);
}

[data-rich-colors='true'][data-sonner-toast][data-type='info'] [data-close-button] {
  background: var(--info-bg);
  border-color: var(--info-border);
  color: var(--info-text);
}

[data-rich-colors='true'][data-sonner-toast][data-type='warning'] {
  background: var(--warning-bg);
  border-color: var(--warning-border);
  color: var(--warning-text);
}

[data-rich-colors='true'][data-sonner-toast][data-type='warning'] [data-close-button] {
  background: var(--warning-bg);
  border-color: var(--warning-border);
  color: var(--warning-text);
}

[data-rich-colors='true'][data-sonner-toast][data-type='error'] {
  background: var(--error-bg);
  border-color: var(--error-border);
  color: var(--error-text);
}

[data-rich-colors='true'][data-sonner-toast][data-type='error'] [data-close-button] {
  background: var(--error-bg);
  border-color: var(--error-border);
  color: var(--error-text);
}

.sonner-loading-wrapper {
  --size: 16px;
  height: var(--size);
  width: var(--size);
  position: absolute;
  inset: 0;
  z-index: 10;
}

.sonner-loading-wrapper[data-visible='false'] {
  transform-origin: center;
  animation: sonner-fade-out 0.2s ease forwards;
}

.sonner-spinner {
  position: relative;
  top: 50%;
  left: 50%;
  height: var(--size);
  width: var(--size);
}

.sonner-loading-bar {
  animation: sonner-spin 1.2s linear infinite;
  background: var(--gray11);
  border-radius: 6px;
  height: 8%;
  left: -10%;
  position: absolute;
  top: -3.9%;
  width: 24%;
}

.sonner-loading-bar:nth-child(1) {
  animation-delay: -1.2s;
  transform: rotate(0.0001deg) translate(146%);
}

.sonner-loading-bar:nth-child(2) {
  animation-delay: -1.1s;
  transform: rotate(30deg) translate(146%);
}

.sonner-loading-bar:nth-child(3) {
  animation-delay: -1s;
  transform: rotate(60deg) translate(146%);
}

.sonner-loading-bar:nth-child(4) {
  animation-delay: -0.9s;
  transform: rotate(90deg) translate(146%);
}

.sonner-loading-bar:nth-child(5) {
  animation-delay: -0.8s;
  transform: rotate(120deg) translate(146%);
}

.sonner-loading-bar:nth-child(6) {
  animation-delay: -0.7s;
  transform: rotate(150deg) translate(146%);
}

.sonner-loading-bar:nth-child(7) {
  animation-delay: -0.6s;
  transform: rotate(180deg) translate(146%);
}

.sonner-loading-bar:nth-child(8) {
  animation-delay: -0.5s;
  transform: rotate(210deg) translate(146%);
}

.sonner-loading-bar:nth-child(9) {
  animation-delay: -0.4s;
  transform: rotate(240deg) translate(146%);
}

.sonner-loading-bar:nth-child(10) {
  animation-delay: -0.3s;
  transform: rotate(270deg) translate(146%);
}

.sonner-loading-bar:nth-child(11) {
  animation-delay: -0.2s;
  transform: rotate(300deg) translate(146%);
}

.sonner-loading-bar:nth-child(12) {
  animation-delay: -0.1s;
  transform: rotate(330deg) translate(146%);
}

@keyframes sonner-fade-in {
  0% {
    opacity: 0;
    transform: scale(0.8);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes sonner-fade-out {
  0% {
    opacity: 1;
    transform: scale(1);
  }
  100% {
    opacity: 0;
    transform: scale(0.8);
  }
}

@keyframes sonner-spin {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0.15;
  }
}

@media (prefers-reduced-motion) {
  [data-sonner-toast],
  [data-sonner-toast] > *,
  .sonner-loading-bar {
    transition: none !important;
    animation: none !important;
  }
}

.sonner-loader {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  transform-origin: center;
  transition: opacity 200ms, transform 200ms;
}

.sonner-loader[data-visible='false'] {
  opacity: 0;
  transform: scale(0.8) translate(-50%, -50%);
}


================================================
FILE: src/types.ts
================================================
import React from 'react';

export type ToastTypes = 'normal' | 'action' | 'success' | 'info' | 'warning' | 'error' | 'loading' | 'default';

export type PromiseT<Data = any> = Promise<Data> | (() => Promise<Data>);

export interface PromiseIExtendedResult extends ExternalToast {
  message: React.ReactNode;
}

export type PromiseTExtendedResult<Data = any> =
  | PromiseIExtendedResult
  | ((data: Data) => PromiseIExtendedResult | Promise<PromiseIExtendedResult>);

export type PromiseTResult<Data = any> =
  | string
  | React.ReactNode
  | ((data: Data) => React.ReactNode | string | Promise<React.ReactNode | string>);

export type PromiseExternalToast = Omit<ExternalToast, 'description'>;

export type PromiseData<ToastData = any> = PromiseExternalToast & {
  loading?: string | React.ReactNode;
  success?: PromiseTResult<ToastData> | PromiseTExtendedResult<ToastData>;
  error?: PromiseTResult | PromiseTExtendedResult;
  description?: PromiseTResult;
  finally?: () => void | Promise<void>;
};

export interface ToastClassnames {
  toast?: string;
  title?: string;
  description?: string;
  loader?: string;
  closeButton?: string;
  cancelButton?: string;
  actionButton?: string;
  success?: string;
  error?: string;
  info?: string;
  warning?: string;
  loading?: string;
  default?: string;
  content?: string;
  icon?: string;
}

export interface ToastIcons {
  success?: React.ReactNode;
  info?: React.ReactNode;
  warning?: React.ReactNode;
  error?: React.ReactNode;
  loading?: React.ReactNode;
  close?: React.ReactNode;
}

export interface Action {
  label: React.ReactNode;
  onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
  actionButtonStyle?: React.CSSProperties;
}

export interface ToastT {
  id: number | string;
  toasterId?: string;
  title?: (() => React.ReactNode) | React.ReactNode;
  type?: ToastTypes;
  icon?: React.ReactNode;
  jsx?: React.ReactNode;
  richColors?: boolean;
  invert?: boolean;
  closeButton?: boolean;
  dismissible?: boolean;
  description?: (() => React.ReactNode) | React.ReactNode;
  duration?: number;
  delete?: boolean;
  action?: Action | React.ReactNode;
  cancel?: Action | React.ReactNode;
  onDismiss?: (toast: ToastT) => void;
  onAutoClose?: (toast: ToastT) => void;
  promise?: PromiseT;
  cancelButtonStyle?: React.CSSProperties;
  actionButtonStyle?: React.CSSProperties;
  style?: React.CSSProperties;
  unstyled?: boolean;
  className?: string;
  classNames?: ToastClassnames;
  descriptionClassName?: string;
  position?: Position;
  testId?: string;
}

export function isAction(action: Action | React.ReactNode): action is Action {
  return (action as Action).label !== undefined;
}

export type Position = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center';
export interface HeightT {
  height: number;
  toastId: number | string;
  position: Position;
}

interface ToastOptions {
  className?: string;
  closeButton?: boolean;
  descriptionClassName?: string;
  style?: React.CSSProperties;
  cancelButtonStyle?: React.CSSProperties;
  actionButtonStyle?: React.CSSProperties;
  duration?: number;
  unstyled?: boolean;
  classNames?: ToastClassnames;
  closeButtonAriaLabel?: string;
  toasterId?: string;
}

type Offset =
  | {
      top?: string | number;
      right?: string | number;
      bottom?: string | number;
      left?: string | number;
    }
  | string
  | number;

export interface ToasterProps {
  id?: string;
  invert?: boolean;
  theme?: 'light' | 'dark' | 'system';
  position?: Position;
  hotkey?: string[];
  richColors?: boolean;
  expand?: boolean;
  duration?: number;
  gap?: number;
  visibleToasts?: number;
  closeButton?: boolean;
  toastOptions?: ToastOptions;
  className?: string;
  style?: React.CSSProperties;
  offset?: Offset;
  mobileOffset?: Offset;
  dir?: 'rtl' | 'ltr' | 'auto';
  swipeDirections?: SwipeDirection[];
  icons?: ToastIcons;
  customAriaLabel?: string;
  containerAriaLabel?: string;
}

export type SwipeDirection = 'top' | 'right' | 'bottom' | 'left';

export interface ToastProps {
  toast: ToastT;
  toasts: ToastT[];
  index: number;
  swipeDirections?: SwipeDirection[];
  expanded: boolean;
  invert: boolean;
  heights: HeightT[];
  setHeights: React.Dispatch<React.SetStateAction<HeightT[]>>;
  removeToast: (toast: ToastT) => void;
  gap?: number;
  position: Position;
  visibleToasts: number;
  expandByDefault: boolean;
  closeButton: boolean;
  interacting: boolean;
  style?: React.CSSProperties;
  cancelButtonStyle?: React.CSSProperties;
  actionButtonStyle?: React.CSSProperties;
  duration?: number;
  className?: string;
  unstyled?: boolean;
  descriptionClassName?: string;
  loadingIcon?: React.ReactNode;
  classNames?: ToastClassnames;
  icons?: ToastIcons;
  closeButtonAriaLabel?: string;
  defaultRichColors?: boolean;
}

export enum SwipeStateTypes {
  SwipedOut = 'SwipedOut',
  SwipedBack = 'SwipedBack',
  NotSwiped = 'NotSwiped',
}

export type Theme = 'light' | 'dark';

export interface ToastToDismiss {
  id: number | string;
  dismiss: boolean;
}

export type ExternalToast = Omit<ToastT, 'id' | 'type' | 'title' | 'jsx' | 'delete' | 'promise'> & {
  id?: number | string;
  toasterId?: string;
};


================================================
FILE: test/.eslintrc.json
================================================
{
  "extends": "next/core-web-vitals"
}


================================================
FILE: test/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
/test-results/
/playwright-report/
/playwright/.cache/


================================================
FILE: test/.npmrc
================================================
package-manager-strict=false


================================================
FILE: test/.vscode/settings.json
================================================
{
  "typescript.tsdk": "../node_modules/.pnpm/typescript@4.9.5/node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true
}


================================================
FILE: test/README.md
================================================
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.

The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.

This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.


================================================
FILE: test/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {};

module.exports = nextConfig;


================================================
FILE: test/package.json
================================================
{
  "name": "test",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@types/node": "18.15.0",
    "@types/react": "18.0.28",
    "@types/react-dom": "18.0.11",
    "ai": "^3.4.9",
    "eslint": "8.35.0",
    "eslint-config-next": "13.2.4",
    "next": "14.2.15",
    "react": "18.3.1",
    "react-dom": "18.3.1",
    "sonner": "workspace:*",
    "typescript": "4.9.5"
  }
}


================================================
FILE: test/src/app/action.tsx
================================================
'use server';
import { createStreamableUI } from 'ai/rsc';

export async function action() {
  'use server';
  let progress = 0;
  const ui = createStreamableUI('loading 0%');
  const interval = setInterval(() => {
    progress += 10;
    ui.update('loading ' + progress + '%');
    if (progress >= 100) {
      clearInterval(interval);
      ui.update('load complete');
    }
  }, 100);
  return ui.value;
}


================================================
FILE: test/src/app/layout.tsx
================================================
export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}


================================================
FILE: test/src/app/page.tsx
================================================
'use client';

import React from 'react';
import { Toaster, toast } from 'sonner';
import { action } from '@/app/action';

const promise = () => new Promise((resolve) => setTimeout(resolve, 2000));

export default function Home({ searchParams }: any) {
  const [showAutoClose, setShowAutoClose] = React.useState(false);
  const [showDismiss, setShowDismiss] = React.useState(false);
  const [theme, setTheme] = React.useState(searchParams.theme || 'light');
  const [isFinally, setIsFinally] = React.useState(false);
  const [showAriaLabels, setShowAriaLabels] = React.useState(false);

  return (
    <>
      <button data-testid="theme-button" className="button" onClick={() => setTheme('dark')}>
        Change theme
      </button>
      <button data-testid="default-button" className="button" onClick={() => toast('My Toast')}>
        Render Toast
      </button>
      <button data-testid="default-button-top" className="button" onClick={() => toast('My Toast')}>
        Render Toast Top
      </button>
      <button data-testid="success" className="button" onClick={() => toast.success('My Success Toast')}>
        Render Success Toast
      </button>
      <button data-testid="error" className="button" onClick={() => toast.error('My Error Toast')}>
        Render Error Toast
      </button>
      <button
        data-testid="action"
        className="button"
        onClick={() =>
          toast('My Message', {
            action: {
              label: 'Action',
              onClick: () => console.log('Action'),
            },
          })
        }
      >
        Render Action Toast
      </button>
      <button
        data-testid="action-prevent"
        className="button"
        onClick={() =>
          toast('My Message', {
            action: {
              label: 'Action',
              onClick: (event) => {
                event.preventDefault();
                console.log('Action');
              },
            },
          })
        }
      >
        Render Action Toast
      </button>
      <button
        data-testid="promise"
        data-finally={isFinally ? '1' : '0'}
        className="button"
        onClick={() =>
          toast.promise(promise, {
            loading: 'Loading...',
            success: 'Loaded',
            error: 'Error',
            finally: () => setIsFinally(true),
          })
        }
      >
        Render Promise Toast
      </button>
      <button
        data-testid="rsf-promise"
        data-finally={isFinally ? '1' : '0'}
        className="button"
        onClick={() =>
          toast.promise(action(), {
            loading: 'Loading...',
            success: 'Loaded',
            error: 'Error',
            finally: () => setIsFinally(true),
          })
        }
      >
        Render React Server Function Toast
      </button>
      <button
        data-testid="custom"
        className="button"
        onClick={() =>
          toast.custom((t) => (
            <div>
              <h1>jsx</h1>
              <button data-testid="dismiss-button" onClick={() => toast.dismiss(t)}>
                Dismiss
              </button>
            </div>
          ))
        }
      >
        Render Custom Toast
      </button>
      <button
        data-testid="custom-cancel-button-toast"
        className="button"
        onClick={() =>
          toast('My Custom Cancel Button', {
            cancel: {
              label: 'Cancel',
              onClick: () => console.log('Cancel'),
            },
          })
        }
      >
        Render Custom Cancel Button
      </button>
      <button
        data-testid="custom-with-empty-id"
        className="button"
        onClick={() =>
          toast.custom(
            (t) => (
              <div>
                <h1>jsx</h1>
                <button data-testid="dismiss-button" onClick={() => toast.dismiss(t)}>
                  Dismiss
                </button>
              </div>
            ),
            {
              id: undefined,
            },
          )
        }
      >
        Render Custom Toast with empty id
      </button>
      <button data-testid="infinity-toast" className="button" onClick={() => toast('My Toast', { duration: Infinity })}>
        Render Infinity Toast
      </button>
      <button
        data-testid="auto-close-toast-callback"
        className="button"
        onClick={() =>
          toast('My Toast', {
            onAutoClose: () => setShowAutoClose(true),
          })
        }
      >
        Render Toast With onAutoClose callback
      </button>
      <button
        data-testid="dismiss-toast-callback"
        className="button"
        onClick={() =>
          toast('My Toast', {
            onDismiss: () => {
              setShowDismiss(true);
            },
          })
        }
      >
        Dismiss toast callback
      </button>
      <button
        data-testid="non-dismissible-toast"
        className="button"
        onClick={() =>
          toast('My Toast', {
            dismissible: false,
          })
        }
      >
        Non-dismissible Toast
      </button>
      <button
        data-testid="update-toast"
        className="button"
        onClick={() => {
          const toastId = toast('My Unupdated Toast', {
            duration: 10000,
          });
          toast('My Updated Toast', {
            id: toastId,
            duration: 10000,
          });
        }}
      >
        Updated Toast
      </button>
      <button
        data-testid="update-toast-duration"
        className="button"
        onClick={() => {
          const toastId = toast('My Unupdated Toast, Updated After 3 Seconds', {
            duration: 10000,
          });
          setTimeout(() => {
            toast('My Updated Toast, Close After 1 Second', {
              id: toastId,
              duration: 1000,
            });
          }, 3000);
        }}
      >
        Updated Toast Duration
      </button>
      <button
        data-testid="string-description"
        className="button"
        onClick={() => toast('Custom Description', { description: 'string description' })}
      >
        String Description
      </button>
      <button
        data-testid="react-node-description"
        className="button"
        onClick={() => toast('Custom Description', { description: <div>This is my custom ReactNode description</div> })}
      >
        ReactNode Description
      </button>
      <button
        data-testid="close-button"
        className="button"
        onClick={() => toast('Toast with close button', { closeButton: true })}
      >
        Render close button
      </button>
      <button
        data-testid="extended-promise"
        className="button"
        onClick={() =>
          toast.promise(
            new Promise((resolve) => {
              setTimeout(() => {
                resolve({ name: 'Sonner' });
              }, 2000);
            }),
            {
              loading: 'Loading...',
              success: (data: any) => ({
                message: `${data.name} toast has been added`,
                description: 'Custom description for the Success state',
              }),
              error: {
                message: 'An error occurred',
                description: undefined,
                action: {
                  label: 'Retry',
                  onClick: () => {
                    console.log('retrying');
                  },
                },
              },
              description: 'Global description',
            },
          )
        }
      >
        Extended Promise Toast
      </button>

      <button
        data-testid="extended-promise-error"
        className="button"
        onClick={() =>
          toast.promise(
            new Promise((_, reject) => {
              setTimeout(() => {
                reject(new Error('Simulated error'));
              }, 2000);
            }),
            {
              loading: 'Loading...',
              success: (data: any) => ({
                message: `${data.name} toast has been added`,
                description: 'Custom description for the Success state',
              }),
              error: {
                message: 'An error occurred',
                description: undefined,
                action: {
                  label: 'Retry',
                  onClick: (event) => {
                    event.preventDefault();
                    console.log('retrying');
                  },
                },
              },
              description: 'Global description',
            },
          )
        }
      >
        Extended Promise Error Toast
      </button>
      <button
        data-testid="error-promise"
        className="button"
        onClick={() => {
          const whatWillHappen = async () => {
            throw new Error('Not implemented');
          };

          toast.promise(whatWillHappen, {
            loading: 'Saving project...',
            success: (result: any) => {
              if (result?.ok) {
                return 'Project saved';
              } else {
                return `${result?.error}`;
              }
            },
            error: (e) => `Error Raise: ${e}`,
          });
        }}
      >
        Error Promise Toast
      </button>
      <button
        className="button"
        onClick={() => {
          setShowAriaLabels(true);
          toast('Toast with custom ARIA labels', { closeButton: true, onAutoClose: () => setShowAriaLabels(false) });
        }}
      >
        With custom ARIA labels
      </button>
      <button
        data-testid="toast-secondary"
        className="button"
        onClick={() => toast('Secondary Toaster Toast', { toasterId: 'secondary' })}
      >
        Render Toast in Secondary Toaster
      </button>
      <button data-testid="toast-global" className="button" onClick={() => toast('Global Toaster Toast')}>
        Render Toast in Global Toaster
      </button>
      <button
        data-testid="testid-toast-button"
        className="button"
        onClick={() => toast('Toast with test ID', { testId: 'my-test-toast' })}
      >
        Toast with testId
      </button>
      <button
        data-testid="testid-promise-toast-button"
        className="button"
        onClick={() =>
          toast.promise(promise, {
            loading: 'Loading...',
            success: 'Loaded',
            error: 'Error',
            testId: 'promise-test-toast',
          })
        }
      >
        Promise Toast with testId
      </button>
      {showAutoClose ? <div data-testid="auto-close-el" /> : null}
      {showDismiss ? <div data-testid="dismiss-el" /> : null}
      <Toaster
        offset={32}
        position={searchParams.position || 'bottom-right'}
        toastOptions={{
          actionButtonStyle: { backgroundColor: 'rgb(219, 239, 255)' },
          cancelButtonStyle: { backgroundColor: 'rgb(254, 226, 226)' },
          closeButtonAriaLabel: showAriaLabels ? 'Yeet the notice' : undefined,
        }}
        theme={theme}
        dir={searchParams.dir || 'auto'}
        containerAriaLabel={showAriaLabels ? 'Notices' : undefined}
        icons={{
          close:
            searchParams.customCloseIcon === '' ? (
              <svg
                xmlns="http://www.w3.org/2000/svg"
                width="12"
                height="12"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                strokeWidth="3"
                strokeLinecap="round"
                strokeLinejoin="round"
              >
                <line x1="18" y1="6" x2="6" y2="18"></line>
                <line x1="6" y1="6" x2="18" y2="18"></line>
              </svg>
            ) : undefined,
        }}
      />
      <Toaster
        id="secondary"
        position="top-left"
        toastOptions={{
          className: 'secondary-toaster',
        }}
      />
    </>
  );
}

Home.theme = 'light';


================================================
FILE: test/tests/basic.spec.ts
================================================
import { expect, test } from '@playwright/test';
import { toast } from 'sonner';

test.beforeEach(async ({ page }) => {
  await page.goto('/');
});

test.describe('Basic functionality', () => {
  test('toast is rendered and disappears after the default timeout', async ({ page }) => {
    await page.getByTestId('default-button').click();
    await expect(page.locator('[data-sonner-toast]')).toHaveCount(1);
    await expect(page.locator('[data-sonner-toast]')).toHaveCount(0);
  });

  test('various toast types are rendered correctly', async ({ page }) => {
    await page.getByTestId('success').click();
    await expect(page.getByText('My Success Toast', { exact: true })).toHaveCount(1);

    await page.getByTestId('error').click();
    await expect(page.getByText('My Error Toast', { exact: true })).toHaveCount(1);

    await page.getByTestId('action').click();
    await expect(page.locator('[data-button]')).toHaveCount(1);
  });

  test('show correct toast content based on promise state', async ({ page }) => {
    await page.getByTestId('promise').click();
    await expect(page.getByText('Loading...')).toHaveCount(1);
    await expect(page.getByText('Loaded')).toHaveCount(1);
  });

  test('handle toast promise rejections', async ({ page }) => {
    const rejectedPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Promise rejected')), 100));
    try {
      toast.promise(rejectedPromise, {});
    } catch {
      throw new Error('Promise should not have rejected without unwrap');
    }

    await expect(toast.promise(rejectedPromise, {}).unwrap()).rejects.toThrow('Promise rejected');
  });

  test('promise toast with extended configuration', async ({ page }) => {
    await page.getByTestId('extended-promise').click();

    // Check loading state
    await expect(page.getByText('Loading...')).toHaveCount(1);

    // Check success state with custom message and description
    await expect(page.getByText('Sonner toast has been added')).toHaveCount(1);
    await expect(page.getByText('Custom description for the Success state')).toHaveCount(1);

    // Verify global description is not shown (overridden by success description)
    await expect(page.getByText('Global description')).toHaveCount(0);
  });

  test('promise toast with extended error configuration', async ({ page }) => {
    await page.getByTestId('extended-promise-error').click();

    // Check loading state
    await expect(page.getByText('Loading...')).toHaveCount(1);

    // Check error state
    await expect(page.getByText('An error occurred')).toHaveCount(1);

    // Verify action button is present
    const actionButton = page.getByText('Retry');
    await expect(actionButton).toHaveCount(1);

    // Click retry button and verify it doesn't close the toast (due to preventDefault)
    await actionButton.click();
    await expect(page.getByText('An error occurred')).toHaveCount(1);
  });

  test('promise toast with Error object rejection', async ({ page }) => {
    await page.getByTestId('error-promise').click();

    // Check error state shows the error message correctly
    await expect(page.getByText('Error Raise: Error: Not implemented')).toHaveCount(1);
  });

  test('render custom jsx in toast', async ({ page }) => {
    await page.getByTestId('custom').click();
    await expect(page.getByText('jsx')).toHaveCount(1);
  });

  test('toast is removed after swiping down', async ({ page }) => {
    await page.getByTestId('default-button').click();
    await page.hover('[data-sonner-toast]');
    await page.mouse.down();
    await page.mouse.move(0, 800);
    await page.mouse.up();
    await expect(page.locator('[data-sonner-toast]')).toHaveCount(0);
  });

  test('dismissible toast is not removed when dragged', async ({ page }) => {
    await page.getByTestId('non-dismissible-toast').click();
    const toast = page.locator('[data-sonner-toast]');
    const dragBoundingBox = await toast.boundingBox();

    if (!dragBoundingBox) return;
    await page.mouse.move(dragBoundingBox.x + dragBoundingBox.width / 2, dragBoundingBox.y);

    await page.mouse.down();
    await page.mouse.move(0, dragBoundingBox.y + 300);

    await page.mouse.up();
    await expect(page.getByTestId('non-dismissible-toast')).toHaveCount(1);
  });

  test('toast is removed after swiping up', async ({ page }) => {
    await page.goto('/?position=top-left');
    await page.getByTestId('default-button').click();
    await page.hover('[data-sonner-toast]');
    await page.mouse.down();
    await page.mouse.move(0, -800);
    await page.mouse.up();
    await expect(page.locator('[data-sonner-toast]')).toHaveCount(0);
  });

  test('toast is not removed when hovered', async ({ page }) => {
    await page.getByTestId('default-button').click();

    // Wait for toast to be visible first
    await expect(page.locator('[data-sonner-toast]')).toBeVisible();

    // Hover the toast
    await page.hover('[data-sonner-toast]');

    // Wait a bit to ensure hover is registered
    await page.waitForTimeout(100);

    // Create a longer timeout to verify toast persists
    await page.waitForTimeout(5000);

    // Verify toast is still visible
    await expect(page.locator('[data-sonner-toast]')).toBeVisible();
    await expect(page.locator('[data-sonner-toast]')).toHaveCount(1);
  });

  test('toast is not removed if duration is set to infinity', async ({ page }) => {
    await page.getByTestId('infinity-toast').click();

    await expect(page.locator('[data-sonner-toast]')).toBeVisible();

    const toast = page.locator('[data-sonner-toast]');
    await toast.hover({ force: true });

    await page.waitForTimeout(100);

    await page.waitForTimeout(5000);

    await expect(toast).toBeVisible();
    await expect(toast).toHaveCount(1);
  });

  test('toast is not removed when event prevented in action', async ({ page }) => {
    await page.getByTestId('action-prevent').click();
    await page.locator('[data-button]').click();
    await expect(page.locator('[data-sonner-toast]')).toHaveCount(1);
  });

  test("toast's auto close callback gets executed correctly", async ({ page }) => {
    await page.getByTestId('auto-close-toast-callback').click();
    await expect(page.getByTestId('auto-close-el')).toHaveCount(1);
  });

  test("toast's dismiss callback gets executed correctly", async ({ page }) => {
    await page.getByTestId('dismiss-toast-callback').click();
    const toast = page.locator('[data-sonner-toast]');

    await toast.waitFor({ state: 'visible' });

    const box = await toast.boundingBox();
    if (!box) return;

    const startX = box.x + box.width / 2;
    const startY = box.y + box.height / 2;

    await page.mouse.move(startX, startY);
    await page.mouse.down();

    // Small initial movement to trigger drag
    await page.mouse.move(startX, startY + 20, { steps: 5 });

    // Main swipe movement
    await page.mouse.move(startX, startY + 300, { steps: 10 });
    await page.mouse.up();

    await expect(page.getByTestId('dismiss-el')).toHaveCount(1);
  });

  test("toaster's theme should be light", async ({ page }) => {
    await page.getByTestId('infinity-toast').click();
    await expect(page.locator('[data-sonner-toaster]')).toHaveAttribute('data-sonner-theme', 'light');
  });

  test("toaster's theme should be dark", async ({ page }) => {
    await page.goto('/?theme=dark');
    await page.getByTestId('infinity-toast').click();
    await expect(page.locator('[data-sonner-toaster]')).toHaveAttribute('data-sonner-theme', 'dark');
  });

  test("toaster's theme should be changed", async ({ page }) => {
    await page.getByTestId('infinity-toast').click();
    await expect(page.locator('[data-sonner-toaster]')).toHaveAttribute('data-sonner-theme', 'light');
    await page.getByTestId('theme-button').click();
    await expect(page.locator('[data-sonner-toaster]')).toHaveAttribute('data-sonner-theme', 'dark');
  });

  test('return focus to the previous focused element', async ({ page }) => {
    await page.getByTestId('custom').focus();
    await page.keyboard.press('Enter');
    await expect(page.locator('[data-sonner-toast]')).toHaveCount(1);
    await page.getByTestId('dismiss-button').focus();
    await page.keyboard.press('Enter');
    await expect(page.locator('[data-sonner-toast]')).toHaveCount(0);
    await expect(page.getByTestId('custom')).toBeFocused();
  });

  test("toaster's dir prop is reflected correctly", async ({ page }) => {
    await page.goto('/?dir=rtl');
    await page.getByTestId('default-button').click();
    await expect(page.locator('[data-sonner-toaster]')).toHaveAttribute('dir', 'rtl');
  });

  test("toaster respects the HTML's dir attribute", async ({ page }) => {
    await page.evaluate(() => {
      document.documentElement.setAttribute('dir', 'rtl');
    });
    await page.getByTestId('default-button').click();
    await expect(page.locator('[data-sonner-toaster]')).toHaveAttribute('dir', 'rtl');
  });

  test("toaster respects its own dir attribute over HTML's", async ({ page }) => {
    await page.goto('/?dir=ltr');
    await page.evaluate(() => {
      document.documentElement.setAttribute('dir', 'rtl');
    });
    await page.getByTestId('default-button').click();
    await expect(page.locator('[data-sonner-toaster]')).toHaveAttribute('dir', 'ltr');
  });

  test('show correct toast content when updating', async ({ page }) => {
    await page.getByTestId('update-toast').click();
    await expect(page.getByText('My Unupdated Toast')).toHaveCount(0);
    await expect(page.getByText('My Updated Toast')).toHaveCount(1);
  });

  test('should update toast content and duration after 3 seconds', async ({ page }) => {
    await page.getByTestId('update-toast-duration').click();

    const initialToast = page.getByText('My Unupdated Toast, Updated After 3 Seconds');
    await expect(initialToast).toBeVisible();

    await page.waitForTimeout(3000);
    const updatedToast = page.getByText('My Updated Toast, Close After 1 Second');
    await expect(updatedToast).toBeVisible();

    await expect(initialToast).not.toBeVisible();

    await page.waitForTimeout(1200);
    await expect(updatedToast).not.toBeVisible();
  });

  test('cancel button is rendered with custom styles', async ({ page }) => {
    await page.getByTestId('custom-cancel-button-toast').click();
    const button = await page.locator('[data-cancel]');

    await expect(button).toHaveCSS('background-color', 'rgb(254, 226, 226)');
  });

  test('cancel button dismisses the custom toast with empty id', async ({ page }) => {
    await page.getByTestId('custom-with-empty-id').click();

    await expect(page.locator('[data-sonner-toast]')).toHaveCount(1);
    await page.locator('[data-dismiss]').click();
    await expect(page.locator('[data-sonner-toast]')).toHaveCount(0);
  });

  test('action button is rendered with custom styles', async ({ page }) => {
    await page.getByTestId('action').click();
    const button = await page.locator('[data-button]');

    await expect(button).toHaveCSS('background-color', 'rgb(219, 239, 255)');
  });

  test('string description is rendered', async ({ page }) => {
    await page.getByTestId('string-description').click();
    await expect(page.getByText('string description')).toHaveCount(1);
  });

  test('ReactNode description is rendered', async ({ page }) => {
    await page.getByTestId('react-node-description').click();
    await expect(page.getByText('This is my custom ReactNode description')).toHaveCount(1);
  });

  test('aria labels are custom', async ({ page }) => {
    await page.getByRole('button', { name: 'With custom ARIA labels' }).click();
    await expect(page.getByText('Toast with custom ARIA labels')).toHaveCount(1);
    await expect(page.getByLabel('Notices')).toHaveCount(1);
    await expect(page.getByLabel('Yeet the notice', { exact: true })).toHaveCount(1);
  });

  test('toast with toasterId only appears in the correct Toaster', async ({ page }) => {
    await page.getByTestId('toast-secondary').click();
    const secondaryToaster = page.locator('[data-sonner-toaster][data-x-position="left"][data-y-position="top"]');
    await expect(secondaryToaster.getByText('Secondary Toaster Toast')).toHaveCount(1);
    const globalToaster = page.locator('[data-sonner-toaster][data-x-position="right"][data-y-position="bottom"]');
    await expect(globalToaster.getByText('Secondary Toaster Toast')).toHaveCount(0);
  });

  test('toast without toasterId only appears in the global Toaster', async ({ page }) => {
    await page.getByTestId('toast-global').click();
    const globalToaster = page.locator('[data-sonner-toaster][data-x-position="right"][data-y-position="bottom"]');
    await expect(globalToaster.getByText('Global Toaster Toast')).toHaveCount(1);
    const secondaryToaster = page.locator('[data-sonner-toaster][data-x-position="left"][data-y-position="top"]');
    await expect(secondaryToaster.getByText('Global Toaster Toast')).toHaveCount(0);
  });

  test('toast with testId renders data-testid attribute correctly', async ({ page }) => {
    await page.getByTestId('testid-toast-button').click();
    await expect(page.getByTestId('my-test-toast')).toBeVisible();
    await expect(page.getByTestId('my-test-toast')).toHaveText('Toast with test ID');
  });

  test('toast without testId does not have data-testid attribute', async ({ page }) => {
    await page.getByTestId('default-button').click();
    const toast = page.locator('[data-sonner-toast]');
    await expect(toast).toBeVisible();
    await expect(toast).not.toHaveAttribute('data-testid');
  });

  test('promise toast with testId maintains testId through state changes', async ({ page }) => {
    await page.getByTestId('testid-promise-toast-button').click();
    await expect(page.getByTestId('promise-test-toast')).toBeVisible();
    await expect(page.getByTestId('promise-test-toast')).toHaveText('Loading...');
    await expect(page.getByTestId('promise-test-toast')).toHaveText('Loaded');
  });
});


================================================
FILE: test/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../playwright.config.ts"],
  "exclude": ["node_modules"]
}


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "jsx": "react",
    "target": "ES2018",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "lib": ["es2015", "dom"]
  },
  "include": ["src"]
}


================================================
FILE: turbo.json
================================================
{
  "$schema": "https://turbo.build/schema.json",
  "extends": ["//"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "dev": {
      "cache": false
    }
  }
}


================================================
FILE: website/.eslintrc.json
================================================
{
  "extends": "next/core-web-vitals"
}


================================================
FILE: website/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts


================================================
FILE: website/.vscode/settings.json
================================================
{
  "typescript.tsdk": "../node_modules/.pnpm/typescript@4.9.5/node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true
}


================================================
FILE: website/README.md
================================================
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.

The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.

This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.


================================================
FILE: website/next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: true,
  },
};

const withNextra = require('nextra')({
  title: 'Sonner',
  theme: 'nextra-theme-docs',
  themeConfig: './theme.config.jsx',
  defaultShowCopyCode: true,
});

module.exports = withNextra(nextConfig);


================================================
FILE: website/package.json
================================================
{
  "name": "website",
  "version": "0.1.0",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@types/node": "18.11.18",
    "@types/react": "18.2.0",
    "@types/react-dom": "18.0.10",
    "@vercel/analytics": "^1.1.0",
    "clsx": "^2.0.0",
    "copy-to-clipboard": "^3.3.3",
    "eslint-config-next": "^13.2.3",
    "framer-motion": "^9.0.1",
    "next": "13.4.19",
    "next-mdx-remote": "^4.3.0",
    "nextra": "^2.12.3",
    "nextra-theme-docs": "^2.12.3",
    "prism-react-renderer": "^1.3.5",
    "react": "^18.2.0",
    "react-dom": "18.2.0",
    "react-use-measure": "^2.1.1",
    "sonner": "workspace:*",
    "typescript": "4.9.5"
  },
  "devDependencies": {
    "autoprefixer": "^10.4.15",
    "postcss": "^8.4.29",
    "tailwindcss": "^3.3.3"
  }
}


================================================
FILE: website/postcss.config.js
================================================
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};


================================================
FILE: website/src/components/CodeBlock/code-block.module.css
================================================
.root {
  padding: 16px;
  margin: 0;
  background: var(--gray1);
  border-radius: 0;
  position: relative;
  line-height: 17px;
  white-space: pre-wrap;
  background: linear-gradient(to top, var(--gray2), var(--gray1) 16px);
}

.wrapper {
  overflow: hidden;
  margin: 0;
  position: relative;
  border-radius: 6px;
  margin-top: 16px;
  border: 1px solid var(--gray3);
  padding: 0 !important;
}

.copyButton {
  position: absolute;
  top: 12px;
  right: 12px;
  z-index: 1;
  width: 26px;
  height: 26px;
  border: 1px solid var(--gray4);
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--gray0);
  cursor: pointer;
  opacity: 0;
  color: var(--gray12);
  transition: background 200ms, box-shadow 200ms, opacity 200ms;
}

.copyButton:focus-visible {
  opacity: 1;
}

.copyButton:hover {
  background: var(--gray1);
}

.copyButton:focus-visible {
  box-shadow: 0 0 0 1px var(--gray4);
}

.copyButton > div {
  display: flex;
}

.outerWrapper {
  position: relative;
}

.outerWrapper:hover .copyButton {
  opacity: 1;
}


================================================
FILE: website/src/components/CodeBlock/index.tsx
================================================
import React from 'react';
import Highlight, { defaultProps } from 'prism-react-renderer';
import useMeasure from 'react-use-measure';
import copy from 'copy-to-clipboard';
import { AnimatePresence, motion, MotionConfig } from 'framer-motion';

import styles from './code-block.module.css';

const variants = {
  visible: { opacity: 1, scale: 1 },
  hidden: { opacity: 0, scale: 0.5 },
};

const theme = {
  plain: {
    color: 'var(--gray12)',
    fontSize: 12,
    fontFamily: 'var(--font-mono)',
  },
  styles: [
    {
      types: ['comment'],
      style: {
        color: 'var(--gray9)',
      },
    },
    {
      types: ['atrule', 'keyword', 'attr-name', 'selector', 'string'],
      style: {
        color: 'var(--gray11)',
      },
    },
    {
      types: ['punctuation', 'operator'],
      style: {
        color: 'var(--gray9)',
      },
    },
    {
      types: ['class-name', 'function', 'tag'],
      style: {
        color: 'var(--gray12)',
      },
    },
  ],
};

export const CodeBlock = ({ children, initialHeight = 0 }: { children: string; initialHeight?: number }) => {
  const [ref, bounds] = useMeasure();
  const [copying, setCopying] = React.useState<number>(0);

  const onCopy = React.useCallback(() => {
    copy(children);
    setCopying((c) => c + 1);
    setTimeout(() => {
      setCopying((c) => c - 1);
    }, 2000);
  }, [children]);

  return (
    <div className={styles.outerWrapper}>
      <button className={styles.copyButton} onClick={onCopy} aria-label="Copy code">
        <MotionConfig transition={{ duration: 0.15 }}>
          <AnimatePresence initial={false} mode="wait">
            {copying ? (
              <motion.div animate="visible" exit="hidden" initial="hidden" key="check" variants={variants}>
                <svg
                  viewBox="0 0 24 24"
                  width="14"
                  height="14"
                  stroke="currentColor"
                  strokeWidth="1.5"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  fill="none"
                  shapeRendering="geometricPrecision"
                >
                  <path d="M20 6L9 17l-5-5"></path>
                </svg>
              </motion.div>
            ) : (
              <motion.div animate="visible" exit="hidden" initial="hidden" key="copy" variants={variants}>
                <svg
                  viewBox="0 0 24 24"
                  width="14"
                  height="14"
                  stroke="currentColor"
                  strokeWidth="1.5"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  fill="none"
                  shapeRendering="geometricPrecision"
                >
                  <path d="M8 17.929H6c-1.105 0-2-.912-2-2.036V5.036C4 3.91 4.895 3 6 3h8c1.105 0 2 .911 2 2.036v1.866m-6 .17h8c1.105 0 2 .91 2 2.035v10.857C20 21.09 19.105 22 18 22h-8c-1.105 0-2-.911-2-2.036V9.107c0-1.124.895-2.036 2-2.036z"></path>
                </svg>
              </motion.div>
            )}
          </AnimatePresence>
        </MotionConfig>
      </button>
      {/* @ts-ignore */}
      <Highlight {...defaultProps} theme={theme} code={children} language="jsx">
        {({ className, tokens, getLineProps, getTokenProps }) => (
          <motion.pre
            className={styles.wrapper}
            animate={{ height: bounds.height || initialHeight }}
            transition={{ type: 'easeOut', duration: 0.2 }}
          >
            <div className={`${className} ${styles.root}`} ref={ref}>
              <div />
              {tokens.map((line, i) => {
                const { key: lineKey, ...rest } = getLineProps({ line, key: i });
                return (
                  <div key={lineKey} {...rest}>
                    {line.map((token, key) => {
                      const { key: tokenKey, ...rest } = getTokenProps({ token, key });
                      return <span key={tokenKey} {...rest} />;
                    })}
                  </div>
                );
              })}
            </div>
          </motion.pre>
        )}
      </Highlight>
    </div>
  );
};


================================================
FILE: website/src/components/ExpandModes/index.tsx
================================================
import { toast } from 'sonner';
import { CodeBlock } from '../CodeBlock';

export const ExpandModes = ({
  expand,
  setExpand,
}: {
  expand: boolean;
  setExpand: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
  return (
    <div>
      <h2>Expand</h2>
      <p>
        You can change the amount of toasts visible through the <code>visibleToasts</code> prop.
      </p>
      <div className="buttons">
        <button
          data-active={expand}
          className="button"
          onClick={() => {
            toast('Event has been created', {
              description: 'Monday, January 3rd at 6:00pm',
            });
            setExpand(true);
          }}
        >
          Expand
        </button>
        <button
          data-active={!expand}
          className="button"
          onClick={() => {
            toast('Event has been created', {
              description: 'Monday, January 3rd at 6:00pm',
            });
            setExpand(false);
          }}
        >
          Default
        </button>
      </div>
      <CodeBlock>{`<Toaster expand={${expand}} />`}</CodeBlock>
    </div>
  );
};


================================================
FILE: website/src/components/Footer/footer.module.css
================================================
.wrapper {
  padding: 32px 0;
  border-top: 1px solid var(--gray3);
  background: var(--gray1);
  margin-top: 164px;
}

.p {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0;
  font-size: 14px;
}

.p img {
  border-radius: 50%;
}

.p a {
  font-weight: 600;
  color: inherit;
  text-decoration: none;
}

.p a:hover {
  text-decoration: underline;
}

@media (max-width: 600px) {
  .wrapper {
    margin-top: 128px;
    padding: 16px 0;
  }
}


================================================
FILE: website/src/components/Footer/index.tsx
================================================
import Image from 'next/image';
import emil from 'public/emil.jpeg';
import styles from './footer.module.css';

export const Footer = () => {
  return (
    <footer className={styles.wrapper}>
      <div className="container">
        <p className={styles.p}>
          <Image alt="Emil's profile picture" src={emil} height={24} width={24} />
          <span>
            Made by{' '}
            <a href="https://twitter.com/emilkowalski_" target="_blank">
              Emil.
            </a>
          </span>
        </p>
      </div>
    </footer>
  );
};


================================================
FILE: website/src/components/Head/index.tsx
================================================
import NextHead from 'next/head';

const ogImage = 'https://sonner.emilkowal.ski/og.png';

const Head = () => (
  <NextHead>
    {/* Title */}
    <title>Sonner</title>
    <meta name="og:title" content="Sonner" />

    {/* Description */}
    <meta name="description" content="An opinionated toast component for React." />
    <meta name="og:description" content="An opinionated toast component for React." />

    {/* Image */}
    <meta name="twitter:image" content={ogImage} />
    <meta name="og:image" content={ogImage} />

    {/* URL */}
    <meta name="og:url" content="https://sonner.emilkowal.ski/" />

    {/* General */}
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta httpEquiv="Content-Language" content="en" />
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:site" content="@emilkowalski_" />
    <meta name="author" content="Emil Kowalski" />

    {/* Favicons */}
    <meta name="msapplication-TileColor" content="#ffffff" />
    <meta name="theme-color" content="#ffffff" />
    <link rel="shortcut icon" href="favicon.ico" />
    <link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
  </NextHead>
);

export default Head;


================================================
FILE: website/src/components/Hero/hero.module.css
================================================
.wrapper {
  display: flex;
  flex-direction: column;
  gap: 12px;
  align-items: center;
}

.toastWrapper {
  display: flex;
  flex-direction: column;
  margin: 0 auto;
  height: 100px;
  width: 400px;
  position: relative;
  mask-image: linear-gradient(to top, transparent 0%, black 35%);
  opacity: 1;
}

.toast {
  width: 356px;
  height: 40px;
  background: var(--gray0);
  box-shadow: 0 4px 12px #0000001a;
  border: 1px solid var(--gray3);
  border-radius: 6px;
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
}

.toast:nth-child(1) {
  transform: translateY(-60%) translateX(-50%) scale(0.9);
}

.toast:nth-child(2) {
  transform: translateY(-30%) translateX(-50%) scale(0.95);
}

.buttons {
  display: flex;
  gap: 8px;
}

.button {
  height: 40px;
  border-radius: 6px;
  border: none;
  background: linear-gradient(156deg, rgba(255, 255, 255, 1) 0%, rgba(240, 240, 240, 1) 100%);
  padding: 0 30px;
  font-weight: 600;
  flex-shrink: 0;
  font-family: inherit;
  box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.06), 0px 1px 0px 0px rgba(0, 0, 0, 0.08),
    0px 2px 2px 0px rgba(0, 0, 0, 0.04), 0px 3px 3px 0px rgba(0, 0, 0, 0.02), 0px 4px 4px 0px rgba(0, 0, 0, 0.01);
  position: relative;
  overflow: hidden;
  cursor: pointer;
  text-decoration: none;
  color: hsl(0, 0%, 9%);
  font-size: 13px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: box-shadow 200ms, background 200ms;
  width: 152px;
}

.button[data-primary] {
  box-shadow: 0px 0px 0px 1px var(--gray12);
  background: var(--gray12);
  color: var(--gray1);
}
.button:focus-visible {
  outline: none;
  box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.06), 0px 1px 0px 0px rgba(0, 0, 0, 0.08),
    0px 2px 2px 0px rgba(0, 0, 0, 0.04), 0px 3px 3px 0px rgba(0, 0, 0, 0.02), 0px 4px 4px 0px rgba(0, 0, 0, 0.01),
    0 0 0 2px rgba(0, 0, 0, 0.15);
}

.button:after {
  content: '';
  position: absolute;
  top: 100%;
  background: blue;
  left: 0;
  width: 100%;
  height: 35%;
  background: linear-gradient(
    to top,
    hsl(0, 0%, 91%) 0%,
    hsla(0, 0%, 91%, 0.987) 8.1%,
    hsla(0, 0%, 91%, 0.951) 15.5%,
    hsla(0, 0%, 91%, 0.896) 22.5%,
    hsla(0, 0%, 91%, 0.825) 29%,
    hsla(0, 0%, 91%, 0.741) 35.3%,
    hsla(0, 0%, 91%, 0.648) 41.2%,
    hsla(0, 0%, 91%, 0.55) 47.1%,
    hsla(0, 0%, 91%, 0.45) 52.9%,
    hsla(0, 0%, 91%, 0.352) 58.8%,
    hsla(0, 0%, 91%, 0.259) 64.7%,
    hsla(0, 0%, 91%, 0.175) 71%,
    hsla(0, 0%, 91%, 0.104) 77.5%,
    hsla(0, 0%, 91%, 0.049) 84.5%,
    hsla(0, 0%, 91%, 0.013) 91.9%,
    hsla(0, 0%, 91%, 0) 100%
  );
  opacity: 0.6;
  transition: transform 200ms;
}

.button:hover:not([data-primary]):after {
  transform: translateY(-100%);
}

.button[data-primary]:hover {
  background: var(--hover);
}

.heading {
  font-size: 48px;
  font-weight: 700;
  margin: -20px 0 12px;
}

.wrapper p {
  margin-bottom: 12px;
}

@media (max-width: 600px) {
  .toastWrapper {
    width: 100%;
  }
}

.link {
  color: var(--gray11) !important;
  font-size: 14px;
  text-decoration: underline;
}


================================================
FILE: website/src/components/Hero/index.tsx
================================================
import { toast } from 'sonner';

import styles from './hero.module.css';
import Link from 'next/link';

export const Hero = () => {
  return (
    <div className={styles.wrapper}>
      <div className={styles.toastWrapper}>
        <div className={styles.toast} />
        <div className={styles.toast} />
        <div className={styles.toast} />
      </div>
      <h1 className={styles.heading}>Sonner</h1>
      <p style={{ marginTop: 0, fontSize: 18, textAlign: 'center' }}>An opinionated toast component for React.</p>
      <div className={styles.buttons}>
        <button
          data-primary=""
          onClick={() => {
            toast('Sonner', {
              description: 'An opinionated toast component for React.',
            });
          }}
          className={styles.button}
        >
          Render a toast
        </button>
        <a className={styles.button} href="https://github.com/emilkowalski/sonner" target="_blank">
          GitHub
        </a>
      </div>
      <Link href="/getting-started" className={styles.link}>
        Documentation
      </Link>
    </div>
  );
};


================================================
FILE: website/src/components/How/How.tsx
================================================
import React from 'react';

export const How = () => {
  return (
    <>
      <div>
        <h2>Want to learn how to make components like this one?</h2>
        <p>
          I created an animations course in which I share everything I know about motion on the web. You can check it
          out{' '}
          <a href="https://animations.dev/" target="_blank" style={{ textDecoration: 'underline' }}>
            here
          </a>
          .
        </p>
      </div>
    </>
  );
};


================================================
FILE: website/src/components/Installation/index.tsx
================================================
'use client';

import React from 'react';
import copy from 'copy-to-clipboard';
import { motion, AnimatePresence, MotionConfig } from 'framer-motion';

import styles from './installation.module.css';

const variants = {
  visible: { opacity: 1, scale: 1 },
  hidden: { opacity: 0, scale: 0.5 },
};

export const Installation = () => {
  const [copying, setCopying] = React.useState(0);

  const onCopy = React.useCallback(() => {
    copy('npm install sonner');
    setCopying((c) => c + 1);
    setTimeout(() => {
      setCopying((c) => c - 1);
    }, 2000);
  }, []);

  return (
    <div>
      <h2>Installation</h2>
      <code className={styles.code} onClick={onCopy}>
        npm install sonner{' '}
        <button aria-label="Copy code" className={styles.copy}>
          <MotionConfig transition={{ duration: 0.15 }}>
            <AnimatePresence initial={false} mode="wait">
              {copying ? (
                <motion.div animate="visible" exit="hidden" initial="hidden" key="check" variants={variants}>
                  <svg
                    viewBox="0 0 24 24"
                    width="14"
                    height="14"
                    stroke="currentColor"
                    strokeWidth="1.5"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    fill="none"
                    shapeRendering="geometricPrecision"
                  >
                    <path d="M20 6L9 17l-5-5"></path>
                  </svg>
                </motion.div>
              ) : (
                <motion.div animate="visible" exit="hidden" initial="hidden" key="copy" variants={variants}>
                  <svg
                    viewBox="0 0 24 24"
                    width="14"
                    height="14"
                    stroke="currentColor"
                    strokeWidth="1.5"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    fill="none"
                    shapeRendering="geometricPrecision"
                  >
                    <path d="M8 17.929H6c-1.105 0-2-.912-2-2.036V5.036C4 3.91 4.895 3 6 3h8c1.105 0 2 .911 2 2.036v1.866m-6 .17h8c1.105 0 2 .91 2 2.035v10.857C20 21.09 19.105 22 18 22h-8c-1.105 0-2-.911-2-2.036V9.107c0-1.124.895-2.036 2-2.036z"></path>
                  </svg>
                </motion.div>
              )}
            </AnimatePresence>
          </MotionConfig>
        </button>
      </code>
    </div>
  );
};


================================================
FILE: website/src/components/Installation/installation.module.css
================================================
.code {
  padding: 0 62px 0 12px;
  border-radius: 6px;
  background: linear-gradient(to top, var(--gray2), var(--gray1) 8px);
  font-family: var(--font-mono);
  font-size: 14px;
  position: relative;
  cursor: copy;
  height: 40px;
  border: 1px solid var(--gray3);
  display: flex;
  align-items: center;
  color: var(--gray12);
}

.copy {
  position: absolute;
  right: 6px;
  top: 50%;
  transform: translateY(-50%);
  cursor: pointer;
  border-radius: 50%;
  border: none;
  border: 1px solid var(--gray4);
  background: #fff;
  color: var(--gray12);
  border-radius: 5px;
  width: 26px;
  height: 26px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.copy div {
  display: flex;
}


================================================
FILE: website/src/components/Other/Other.tsx
================================================
import React from 'react';
import { useMemo } from 'react';
import { toast } from 'sonner';
import { CodeBlock } from '../CodeBlock';
import styles from './other.module.css';

export const Other = ({
  setRichColors,
  setCloseButton,
}: {
  setRichColors: React.Dispatch<React.SetStateAction<boolean>>;
  setCloseButton: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
  const allTypes = useMemo(
    () => [
      {
        name: 'Rich Colors Success',
        snippet: `toast.success('Event has been created')`,
        action: () => {
          toast.success('Event has been created');
          setRichColors(true);
        },
      },
      {
        name: 'Rich Colors Error',
        snippet: `toast.error('Event has not been created')`,
        action: () => {
          toast.error('Event has not been created');
          setRichColors(true);
        },
      },
      {
        name: 'Rich Colors Info',
        snippet: `toast.info('Be at the area 10 minutes before the event time')`,
        action: () => {
          toast.info('Be at the area 10 minutes before the event time');
          setRichColors(true);
        },
      },
      {
        name: 'Rich Colors Warning',
        snippet: `toast.warning('Event start time cannot be earlier than 8am')`,
        action: () => {
          toast.warning('Event start time cannot be earlier than 8am');
          setRichColors(true);
        },
      },
      {
        name: 'Close Button',
        snippet: `toast('Event has been created', {
  description: 'Monday, January 3rd at 6:00pm',
})`,
        action: () => {
          toast('Event has been created', {
            description: 'Monday, January 3rd at 6:00pm',
          });
          setCloseButton((t) => !t);
        },
      },
      {
        name: 'Headless',
        snippet: `toast.custom((t) => (
  <div>
    <h1>Custom toast</h1>
     <button onClick={() => toast.dismiss(t)}>Dismiss</button>
  </div>
));`,
        action: () => {
          toast.custom(
            (t) => (
              <div className={styles.headless}>
                <p className={styles.headlessTitle}>Event Created</p>
                <p className={styles.headlessDescription}>Today at 4:00pm - &quot;Louvre Museum&quot;</p>
                <button className={styles.headlessClose} onClick={() => toast.dismiss(t)}>
                  <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
                    <path d="M2.96967 2.96967C3.26256 2.67678 3.73744 2.67678 4.03033 2.96967L8 6.939L11.9697 2.96967C12.2626 2.67678 12.7374 2.67678 13.0303 2.96967C13.3232 3.26256 13.3232 3.73744 13.0303 4.03033L9.061 8L13.0303 11.9697C13.2966 12.2359 13.3208 12.6526 13.1029 12.9462L13.0303 13.0303C12.7374 13.3232 12.2626 13.3232 11.9697 13.0303L8 9.061L4.03033 13.0303C3.73744 13.3232 3.26256 13.3232 2.96967 13.0303C2.67678 12.7374 2.67678 12.2626 2.96967 11.9697L6.939 8L2.96967 4.03033C2.7034 3.76406 2.6792 3.3474 2.89705 3.05379L2.96967 2.96967Z"></path>
                  </svg>
                </button>
              </div>
            ),
            { duration: 999999 },
          );
          setCloseButton((t) => !t);
        },
      },
    ],
    [setRichColors],
  );

  const [activeType, setActiveType] = React.useState(allTypes[0]);

  const richColorsActive = activeType?.name?.includes('Rich');
  const closeButtonActive = activeType?.name?.includes('Close');

  return (
    <div>
      <h2>Other</h2>
      <div className="buttons">
        {allTypes.map((type) => (
          <button
            className="button"
            onClick={() => {
              type.action();
              setActiveType(type);
            }}
            key={type.name}
          >
            {type.name}
          </button>
        ))}
      </div>
      <CodeBlock>
        {`${activeType.snippet || ''}

// ...

<Toaster ${richColorsActive ? 'richColors ' : ''} ${closeButtonActive ? 'closeButton ' : ''}/>`}
      </CodeBlock>
    </div>
  );
};


================================================
FILE: website/src/components/Other/other.module.css
================================================
ol[dir='ltr'] .headlessClose {
  --headless-close-start: unset;
  --headless-close-end: 6px;
}

ol[dir='rtl'] .headlessClose {
  --headless-close-start: 6px;
  --headless-close-end: unset;
}

.headless {
  padding: 16px;
  width: 356px;
  box-sizing: border-box;
  border-radius: 8px;
  background: var(--gray1);
  border: 1px solid var(--gray4);
  position: relative;
}

.headless .headlessDescription {
  margin: 0;
  color: var(--gray10);
  font-size: 14px;
  line-height: 1;
}

.headless .headlessTitle {
  font-size: 14px;
  margin: 0 0 8px;
  color: var(--gray12);
  font-weight: 500;
  line-height: 1;
}

.headlessClose {
  position: absolute;
  cursor: pointer;
  top: 6px;
  height: 24px;
  width: 24px;
  display: flex;
  justify-content: center;
  align-items: center;
  left: var(--headless-close-start);
  right: var(--headless-close-end);
  color: var(--gray10);
  padding: 0;
  background: transparent;
  border: none;
  transition: color 200ms;
}

.headlessClose:hover {
  color: var(--gray12);
}


================================================
FILE: website/src/components/Position/index.tsx
================================================
import { toast, useSonner } from 'sonner';
import { CodeBlock } from '../CodeBlock';
import React from 'react';

const positions = ['top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right'] as const;

export type Position = (typeof positions)[number];

export const Position = ({
  position: activePosition,
  setPosition,
}: {
  position: Position;
  setPosition: React.Dispatch<React.SetStateAction<Position>>;
}) => {
  const { toasts } = useSonner();

  function removeAllToasts() {
    toasts.forEach((t) => toast.dismiss(t.id));
  }

  return (
    <div>
      <h2>Position</h2>
      <p>Swipe direction changes depending on the position.</p>
      <div className="buttons">
        {positions.map((position) => (
          <button
            data-active={activePosition === position}
            className="button"
            onClick={() => {
              if (activePosition !== position) {
                setPosition(position);
                removeAllToasts();
              }

              toast('Event has been created', {
                description: 'Monday, January 3rd at 6:00pm',
              });
            }}
            key={position}
          >
            {position}
          </button>
        ))}
      </div>
      <CodeBlock>{`<Toaster position="${activePosition}" />`}</CodeBlock>
    </div>
  );
};


================================================
FILE: website/src/components/Types/Types.tsx
================================================
import React from 'react';
import { toast } from 'sonner';
import { CodeBlock } from '../CodeBlock';

const promiseCode = '`${data.name} toast has been added`';

export const Types = () => {
  const [activeType, setActiveType] = React.useState(allTypes[0]);

  return (
    <div>
      <h2>Types</h2>
      <p>You can customize the type of toast you want to render, and pass an options object as the second argument.</p>
      <div className="buttons">
        {allTypes.map((type) => (
          <button
            className="button"
            data-active={activeType.name === type.name}
            onClick={() => {
              type.action();
              setActiveType(type);
            }}
            key={type.name}
          >
            {type.name}
          </button>
        ))}
      </div>
      <CodeBlock>{`${activeType.snippet}`}</CodeBlock>
    </div>
  );
};

const allTypes = [
  {
    name: 'Default',
    snippet: `toast('Event has been created')`,
    action: () => toast('Event has been created'),
  },
  {
    name: 'Description',
    snippet: `toast.message('Event has been created', {
  description: 'Monday, January 3rd at 6:00pm',
})`,
    action: () =>
      toast('Event has been created', {
        description: 'Monday, January 3rd at 6:00pm',
      }),
  },
  {
    name: 'Success',
    snippet: `toast.success('Event has been created')`,
    action: () => toast.success('Event has been created'),
  },
  {
    name: 'Info',
    snippet: `toast.info('Be at the area 10 minutes before the event time')`,
    action: () => toast.info('Be at the area 10 minutes before the event time'),
  },
  {
    name: 'Warning',
    snippet: `toast.warning('Event start time cannot be earlier than 8am')`,
    action: () => toast.warning('Event start time cannot be earlier than 8am'),
  },
  {
    name: 'Error',
    snippet: `toast.error('Event has not been created')`,
    action: () => toast.error('Event has not been created'),
  },
  {
    name: 'Action',
    snippet: `toast('Event has been created', {
  action: {
    label: 'Undo',
    onClick: () => console.log('Undo')
  },
})`,
    action: () =>
      toast.message('Event has been created', {
        action: {
          label: 'Undo',
          onClick: () => console.log('Undo'),
        },
      }),
  },
  {
    name: 'Promise',
    snippet: `const promise = () => new Promise((resolve) => setTimeout(() => resolve({ name: 'Sonner' }), 2000));

toast.promise(promise, {
  loading: 'Loading...',
  success: (data) => {
    return ${promiseCode};
  },
  error: 'Error',
});`,
    action: () =>
      toast.promise<{ name: string }>(
        () =>
          new Promise((resolve) => {
            setTimeout(() => {
              resolve({ name: 'Sonner' });
            }, 2000);
          }),
        {
          loading: 'Loading...',
          success: (data) => {
            return `${data.name} toast has been added`;
          },
          error: 'Error',
        },
      ),
  },
  {
    name: 'Custom',
    snippet: `toast(<div>A custom toast with default styling</div>)`,
    action: () => toast(<div>A custom toast with default styling</div>, { duration: 1000000 }),
  },
];


================================================
FILE: website/src/components/Usage/index.tsx
================================================
import { CodeBlock } from '../CodeBlock';

export const Usage = () => {
  return (
    <div>
      <h2>Usage</h2>
      <p>Render the toaster in the root of your app.</p>
      <CodeBlock initialHeight={270}>{`import { Toaster, toast } from 'sonner'

// ...

function App() {
  return (
    <div>
      <Toaster />
      <button onClick={() => toast('My first toast')}>
        Give me a toast
      </button>
    </div>
  )
}`}</CodeBlock>
    </div>
  );
};


================================================
FILE: website/src/globals.css
================================================
:root,
.light {
  --gray0: #fff;
  --gray1: hsl(0, 0%, 99%);
  --gray2: hsl(0, 0%, 97.3%);
  --gray3: hsl(0, 0%, 95.1%);
  --gray4: hsl(0, 0%, 93%);
  --gray5: hsl(0, 0%, 90.9%);
  --gray6: hsl(0, 0%, 88.7%);
  --gray7: hsl(0, 0%, 85.8%);
  --gray8: hsl(0, 0%, 78%);
  --gray9: hsl(0, 0%, 56.1%);
  --gray10: hsl(0, 0%, 52.3%);
  --gray11: hsl(0, 0%, 43.5%);
  --gray12: hsl(0, 0%, 9%);
  --hover: rgb(40, 40, 40);
  --border-radius: 6px;
  --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial,
    Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
  --font-mono: 'SF Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
}

.dark {
  --gray0: #000;
  --gray1: hsl(0, 0%, 9.5%);
  --gray2: hsl(0, 0%, 10.5%);
  --gray3: hsl(0, 0%, 15.8%);
  --gray4: hsl(0, 0%, 18.9%);
  --gray5: hsl(0, 0%, 21.7%);
  --gray6: hsl(0, 0%, 24.7%);
  --gray7: hsl(0, 0%, 29.1%);
  --gray8: hsl(0, 0%, 37.5%);
  --gray9: hsl(0, 0%, 43%);
  --gray10: hsl(0, 0%, 50.7%);
  --gray11: hsl(0, 0%, 69.5%);
  --gray12: hsl(0, 0%, 93.5%);
}

::selection {
  background: var(--gray7);
}

.container {
  max-width: 642px;
  margin: 0 auto;
  padding-left: max(var(--side-padding), env(safe-area-inset-left));
  padding-right: max(var(--side-padding), env(safe-area-inset-right));
}

.wrapper {
  --side-padding: 16px;
  background: var(--gray0);
  margin: 0;
  padding: 0;
  padding-top: 100px;
  font-family: var(--font-sans);
  -webkit-font-smoothing: antialiased;
}

/* Disable double-tap zoom */
* {
  touch-action: manipulation;
}

h1,
p {
  color: var(--gray12);
}

h2 {
  font-size: 16px;
  color: var(--gray12);
  font-weight: 500;
}

h2 + p {
  margin-top: -4px;
}

p {
  font-size: 16px;
}

a {
  color: inherit;
  text-decoration-color: var(--gray10);
  text-underline-position: from-font;
}

code {
  font-size: 13px;
  line-height: 28px;
  padding: 2px 3.6px;
  border: 1px solid var(--gray3);
  background: var(--gray4);
  font-family: var(--font-mono);
  border-radius: 6px;
}

.content {
  display: flex;
  flex-direction: column;
  gap: 48px;
  margin-top: 96px;
}

.buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  overflow: auto;
  margin: 0 calc(-1 * var(--side-padding));
  padding: 4px var(--side-padding);
  position: relative;
}

.button {
  padding: 8px 12px;
  margin: 0;
  background: var(--gray1);
  border: 1px solid var(--gray3);
  white-space: nowrap;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 500;
  font-family: var(--font-sans);
  cursor: pointer;
  color: var(--gray12);
  transition: border-color 200ms, background 200ms, box-shadow 200ms;
}

.button:hover {
  background: var(--gray2);
  border-color: var(--gray4);
}

.button[data-active='true'] {
  background: var(--gray3);
  border-color: var(--gray7);
}

.button:focus-visible {
  outline: none;
  box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.06), 0px 1px 0px 0px rgba(0, 0, 0, 0.08),
    0px 2px 2px 0px rgba(0, 0, 0, 0.04), 0px 3px 3px 0px rgba(0, 0, 0, 0.02), 0px 4px 4px 0px rgba(0, 0, 0, 0.01),
    0 0 0 2px rgba(0, 0, 0, 0.15);
}

@media (max-width: 600px) {
  .buttons {
    mask-image: linear-gradient(to right, transparent, black 16px, black calc(100% - 16px), transparent);
  }
}

.wrapper h1,
.wrapper p {
  color: var(--gray12);
  line-height: 25px;
}
m .wrapper h2 {
  font-size: 16px;
  color: var(--gray12);
  font-weight: 500;
}

.wrapper h2 + p {
  margin-top: -4px;
}

.wrapper h2 {
  margin: 12px 0;
}

.wrapper p {
  font-size: 16px;
  margin-bottom: 16px;
}

.wrapper a {
  text-decoration-color: var(--gray10);
  text-underline-position: from-font;
}

.wrapper .content {
  display: flex;
  flex-direction: column;
  gap: 48px;
  margin-top: 96px;
}

.wrapper footer {
  padding: 0;
}

.wrapper footer .container {
  padding: 32px 16px !important;
}

.wrapper footer p {
  margin: 0;
  font-size: 14px;
}

footer {
  background: var(--gray1) !important;
}

hr {
  background: var(--gray3) !important;
}

.nx-border-primary-500 {
  border-color: var(--gray12) !important;
}

.nx-bg-primary-500\/10 {
  background: var(--gray3) !important;
}


================================================
FILE: website/src/pages/_app.tsx
================================================
import type { ReactElement } from 'react';
import type { AppProps } from 'next/app';
import { Analytics } from '@vercel/analytics/react';
import '../style.css';
import '../globals.css';

export default function Nextra({ Component, pageProps }: AppProps): ReactElement {
  return (
    <>
      {/* @ts-ignore */}
      <Component {...pageProps} />
      <Analytics />
    </>
  );
}


================================================
FILE: website/src/pages/_meta.json
================================================
{
  "getting-started": {
    "title": "Getting Started",
    "href": "/getting-started"
  },
  "-- API": {
    "type": "separator",
    "title": "API"
  },
  "toast": {
    "title": "toast()",
    "href": "/toast"
  },
  "toaster": {
    "title": "Toaster",
    "href": "/toaster"
  },
  "-- More": {
    "type": "separator",
    "title": "Guides"
  },
  "styling": {
    "title": "Styling",
    "href": "/styling"
  }
}


================================================
FILE: website/src/pages/getting-started.mdx
================================================
import { Tab, Tabs, Cards, Card, Steps } from 'nextra-theme-docs';
import { toast } from 'sonner';

# Getting Started

Sonner is an opinionated toast component for React. You can read more about why and how it was built [here](https://emilkowal.ski/ui/building-a-toast-component).

<Steps>
### Install

<Tabs items={['pnpm', 'npm', 'yarn', 'bun']}>
  <Tab>
    ```bash
    pnpm i sonner
    ```

  </Tab>
  <Tab>
    ```bash
    npm i sonner
    ```
  </Tab>
  <Tab>
    ```bash
    yarn add sonner
    ```
  </Tab>
    <Tab>
    ```bash
    bun add sonner
    ```
  </Tab>
</Tabs>

### Add Toaster to your app

It can be placed anywhere, even in server components such as `layout.tsx`.

```tsx
import { Toaster } from 'sonner';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Toaster />
      </body>
    </html>
  );
}
```

### Render a toast

```tsx
import { toast } from 'sonner';

function MyToast() {
  return <button onClick={() => toast('This is a sonner toast')}>Render my toast</button>;
}
```

</Steps>


================================================
FILE: website/src/pages/index.tsx
================================================
import React, { StrictMode } from 'react';
import { Toaster } from 'sonner';
import { Installation } from '@/src/components/Installation';
import { Hero } from '@/src/components/Hero';
import { Types } from '@/src/components/Types/Types';
import { ExpandModes } from '@/src/components/ExpandModes';
import { Position } from '@/src/components/Position';
import { Usage } from '@/src/components/Usage';
import { Other } from '@/src/components/Other/Other';
import Head from '../components/Head';
import { How } from '../components/How/How';
import { Footer } from '../components/Footer';

export default function Home() {
  const [expand, setExpand] = React.useState(false);
  const [position, setPosition] = React.useState<Position>('bottom-right');
  const [richColors, setRichColors] = React.useState(false);
  const [closeButton, setCloseButton] = React.useState(false);

  return (
    <div className="wrapper light">
      <Head />
      <Toaster
        theme="light"
        richColors={richColors}
        closeButton={closeButton}
        expand={expand}
        position={position}
        duration={Infinity}
      />
      <main className="container">
        <Hero />
        <div className="content">
          <Installation />
          <Usage />
          <Types />
          <Position position={position} setPosition={setPosition} />
          <ExpandModes expand={expand} setExpand={setExpand} />
          <Other setCloseButton={setCloseButton} setRichColors={setRichColors} />
          <How />
        </div>
      </main>
      <Footer />
    </div>
  );
}


================================================
FILE: website/src/pages/styling.mdx
================================================
# Styling

Styling can be done globally via `toastOptions`, this way every toast will have the same styling.

```jsx
<Toaster
  toastOptions={{
    style: {
      background: 'red',
    },
    className: 'class',
  }}
/>
```

You can also use the same props when calling `toast` to style a specific toast.

```jsx
toast('Hello World', {
  style: {
    background: 'red',
  },
  className: 'class',
});
```

## Tailwind CSS

The preferred way to style the toasts with tailwind is by using the `unstyled` prop. That will give you an unstyled toast which you can then style with tailwind.

```jsx
<Toaster
  toastOptions={{
    unstyled: true,
    classNames: {
      toast: 'bg-blue-400',
      title: 'text-red-400',
      description: 'text-red-400',
      actionButton: 'bg-zinc-400',
      cancelButton: 'bg-orange-400',
      closeButton: 'bg-lime-400',
    },
  }}
/>
```

You can do the same when calling `toast()`.

```jsx
toast('Hello World', {
  unstyled: true,
  classNames: {
    toast: 'bg-blue-400',
    title: 'text-red-400 text-2xl',
    description: 'text-red-400',
    actionButton: 'bg-zinc-400',
    cancelButton: 'bg-orange-400',
    closeButton: 'bg-lime-400',
  },
});
```

Styling per toast type is also possible.

```jsx
<Toaster
  toastOptions={{
    unstyled: true,
    classNames: {
      error: 'bg-red-400',
      success: 'text-green-400',
      warning: 'text-yellow-400',
      info: 'bg-blue-400',
    },
  }}
/>
```

## Changing Icons

You can change the default icons using the `icons` prop:

```jsx
<Toaster
  icons={{
    success: <SuccessIcon />,
    info: <InfoIcon />,
    warning: <WarningIcon />,
    error: <ErrorIcon />,
    loading: <LoadingIcon />,
  }}
/>
```

You can also set an icon for each toast:

```jsx
toast('Hello World', {
  icon: <Icon />,
});
```


================================================
FILE: website/src/pages/toast.mdx
================================================
import { toast } from 'sonner';

# Toast()

Use it to render a toast. You can call it from anywhere, even outside of React.

## Rendering the toast

You can call it with just a string.

```jsx
import { toast } from 'sonner';

toast('Hello World!');
```

Or provide an object as the second argument with more options. They will overwrite the options passed to [`<Toaster />`](/toaster) if you have provided any.

```jsx
import { toast } from 'sonner';

toast('My toast', {
  className: 'my-classname',
  description: 'My description',
  duration: 5000,
  icon: <MyIcon />,
});
```

### Render toast on page load

To render a toast on initial page load it is required that the function `toast()` is called inside of a `setTimeout` or `requestAnimationFrame`.

```jsx
setTimeout(() => {
  toast('My toast on a page load');
});
```

## Creating toasts

### Success

Renders a checkmark icon in front of the message.

```jsx
toast.success('My success toast');
```

### Error

Renders an error icon in front of the message.

```jsx
toast.error('My error toast');
```

### Action

Renders a primary button, clicking it will close the toast and run the callback passed via `onClick`. You can prevent the toast from closing by calling `event.preventDefault()` in the `onClick` callback.

```jsx
toast('My action toast', {
  action: {
    label: 'Action',
    onClick: () => console.log('Action!'),
  },
});
```

You can also render jsx as your action.

```jsx
toast('My action toast', {
  action: <Button onClick={() => console.log('Action!')}>Action</Button>,
});
```

### Cancel

Renders a secondary button, clicking it will close the toast and run the callback passed via `onClick`.

```jsx
toast('My cancel toast', {
  cancel: {
    label: 'Cancel',
    onClick: () => console.log('Cancel!'),
  },
});
```

You can also render jsx in the cancel option.

```jsx
toast('My cancel toast', {
  cancel: <Button onClick={() => console.log('Cancel!')}>Cancel</Button>,
});
```

### Promise

Starts in a loading state and will update automatically after the promise resolves or fails.
You can pass a function to the success/error messages to incorporate the result/error of the promise.

```jsx
toast.promise(myPromise, {
  loading: 'Loading...',
  success: (data) => {
    return `${data.name} toast has been added`;
  },
  error: 'Error',
});
```

### Loading

Renders a toast with a loading spinner. Useful when you want to handle various states yourself instead of using a promise toast.

```jsx
toast.loading('Loading data');
```

### Custom

You can pass jsx as the first argument instead of a string to render a custom toast while maintaining default styling.

```jsx
toast(<div>A custom toast with default styling</div>, { duration: 5000 });
```

### Headless

Use it to render an unstyled toast with custom jsx while maintaining the functionality. This function receives the `Toast` as an argument, giving you access to all properties.

```jsx
toast.custom((t) => (
  <div>
    This is a custom component <button onClick={() => toast.dismiss(t)}>close</button>
  </div>
));
```

### Dynamic Position

You can change the position of the toast dynamically by passing a `position` prop to the toast
function. It will not affect the positioning of other toasts.

```jsx
// Available positions:
// top-left, top-center, top-right, bottom-left, bottom-center, bottom-right
toast('Hello World', {
  position: 'top-center',
});
```

## Other

### Updating toasts

You can update a toast by using the `toast` function and passing it the id of the toast you want to update, the rest stays the same.

```jsx
const toastId = toast('Sonner');

toast.success('Toast has been updated', {
  id: toastId,
});
```

### On Close Callback

You can pass `onDismiss` and `onAutoClose` callbacks to each toast. `onDismiss` gets fired when either the close button gets clicked or the toast is swiped. `onAutoClose` fires when the toast disappears automatically after it's timeout (`duration` prop).

```jsx
toast('Event has been created', {
  onDismiss: (t) => console.log(`Toast with id ${t.id} has been dismissed`),
  onAutoClose: (t) => console.log(`Toast with id ${t.id} has been closed automatically`),
});
```

### Persisting toasts

If you want a toast to stay on screen forever, you can set the `duration` to [`Infinity`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Infinity).

```js
toast('This toast will stay on screen forever', {
  duration: Infinity,
});
```

### Dismissing toasts programmatically

To remove a toast programmatically use `toast.dismiss(id)`. The `toast()` function return the id of the toast.

```jsx
const toastId = toast('Event has been created');

toast.dismiss(toastId);
```

You can also dismiss all toasts at once by calling `toast.dismiss()` without an id.

```jsx
toast.dismiss();
```

### Rendering custom elements

You can render custom elements inside the toast like `<a />` or custom components by passing a function instead of a string. This work for both the title and description.

```jsx
toast(
  () => (
    <>
      View{' '}
      <a href="https://google.com" target="_blank">
        Animation on the Web
      </a>
    </>
  ),
  {
    description: () => <button>This is a button element!</button>,
  },
);
```

### Targeting a specific Toaster

You can target a specific Toaster by passing a `toasterId` option:

```jsx
// This toast will only appear in the Toaster with id="canvas"
toast('This will show in the canvas Toaster', { toasterId: 'canvas' });
```

## API Reference

| Property          |                                              Description                                               |        Default |
| :---------------- | :----------------------------------------------------------------------------------------------------: | -------------: |
| description       |                           Toast's description, renders underneath the title.                           |            `-` |
| closeButton       |                                          Adds a close button.                                          |        `false` |
| invert            |                                Dark toast in light mode and vice versa.                                |        `false` |
| duration          |            Time in milliseconds that should elapse before automatically closing the toast.             |         `4000` |
| position          |                                         Position of the toast.                                         | `bottom-right` |
| dismissible       |                     If `false`, it'll prevent the user from dismissing the toast.                      |         `true` |
| icon              |                      Icon displayed in front of toast's text, aligned vertically.                      |            `-` |
| action            |                      Renders a primary button, clicking it will close the toast.                       |            `-` |
| cancel            |                     Renders a secondary button, clicking it will close the toast.                      |            `-` |
| id                |                                        Custom id for the toast.                                        |            `-` |
| onDismiss         |       The function gets called when either the close button is clicked, or the toast is swiped.        |            `-` |
| onAutoClose       | Function that gets called when the toast disappears automatically after it's timeout (duration` prop). |            `-` |
| unstyled          |                  Removes the default styling, which allows for easier customization.                   |        `false` |
| actionButtonStyle |                                      Styles for the action button                                      |           `{}` |
| cancelButtonStyle |                                      Styles for the cancel button                                      |           `{}` |


================================================
FILE: website/src/pages/toaster.mdx
================================================
# Toaster

This component renders all the toasts, you can place it anywhere in your app.

## Customization

You can see examples of most of the scenarios described below on the [homepage](/).

### Multiple Toasters

You can render multiple Toaster components with different ids and target toasts to each one:

```jsx
<Toaster id="global" position="top-right" />
<Toaster id="canvas" position="bottom-left" />

<button onClick={() => toast('Global toast', { toasterId: 'global' })}>
  Show in Global Toaster
</button>
<button onClick={() => toast('Canvas toast', { toasterId: 'canvas' })}>
  Show in Canvas Toaster
</button>
```

### Expand

When you hover on one of the toasts, they will expand. You can make that the default behavior by setting the `expand` prop to `true`, and customize it even further with the `visibleToasts` prop.

```jsx
// 9 toasts will be visible instead of the default, which is 3.
<Toaster expand visibleToasts={9} />
```

### Position

Changes the place where all toasts will be rendered.

```jsx
// Available positions:
// top-left, top-center, top-right, bottom-left, bottom-center, bottom-right
<Toaster position="top-center" />
```

### Styling all toasts

You can customize all toasts at once with `toastOptions` prop. These options will act as the default for all toasts.

```jsx
<Toaster
  toastOptions={{
    style: { background: 'red' },
    className: 'my-toast',
  }}
/>
```

### dir

Changes the directionality of the toast's text.

```jsx
// rtl, ltr, auto
<Toaster dir="rtl" />
```

### Custom ARIA label

You can customize the default ARIA label for the notification container and the toast close button.

```jsx
// example in Finnish
<Toaster containerAriaLabel="Ilmoitukset" toastOptions={{ closeButtonAriaLabel: 'Sulje' }} />
```

## API Reference

| Property              |                                                           Description                                                           |        Default |
| :-------------------- | :-----------------------------------------------------------------------------------------------------------------------------: | -------------: |
| theme                 |                                       Toast's theme, either `light`, `dark`, or `system`                                        |        `light` |
| richColors            |                                           Makes error and success state more colorful                                           |        `false` |
| expand                |                                               Toasts will be expanded by default                                                |        `false` |
| visibleToasts         |                                                    Amount of visible toasts                                                     |            `3` |
| position              |                                             Place where the toasts will be rendered                                             | `bottom-right` |
| closeButton           |                                                Adds a close button to all toasts                                                |        `false` |
| offset                |                                              Offset from the edges of the screen.                                               |         `32px` |
| mobileOffset          |                    Offset from the left/right edges of the screen on screens with width smaller than 600px.                     |         `16px` |
| dir                   |                                                 Directionality of toast's text                                                  |          `ltr` |
| hotkey                |                                   Keyboard shortcut that will move focus to the toaster area.                                   |    `⌥/alt + T` |
| invert                |                                            Dark toasts in light mode and vice versa.                                            |        `false` |
| toastOptions          |               These will act as default options for all toasts. See [toast()](/toast) for all available options.                |         `4000` |
| gap                   |                                                Gap between toasts when expanded                                                 |           `14` |
| loadingIcon           |                                                Changes the default loading icon                                                 |            `-` |
| pauseWhenPageIsHidden | Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked. |        `false` |
| icons                 |                                                    Changes the default icons                                                    |            `-` |


================================================
FILE: website/src/style.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
  --gray0: #fff;
  --gray1: hsl(0, 0%, 99%);
  --gray2: hsl(0, 0%, 97.3%);
  --gray3: hsl(0, 0%, 95.1%);
  --gray4: hsl(0, 0%, 93%);
  --gray5: hsl(0, 0%, 90.9%);
  --gray6: hsl(0, 0%, 88.7%);
  --gray7: hsl(0, 0%, 85.8%);
  --gray8: hsl(0, 0%, 78%);
  --gray9: hsl(0, 0%, 56.1%);
  --gray10: hsl(0, 0%, 52.3%);
  --gray11: hsl(0, 0%, 43.5%);
  --gray12: hsl(0, 0%, 9%);
  --hover: rgb(40, 40, 40);
  --border-radius: 6px;
  --font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial,
    Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
  --font-mono: 'SF Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
  --shiki-token-comment: var(--gray11) !important;
}

.dark {
  --gray0: #000;
  --gray1: hsl(0, 0%, 9.5%);
  --gray2: hsl(0, 0%, 10.5%);
  --gray3: hsl(0, 0%, 15.8%);
  --gray4: hsl(0, 0%, 18.9%);
  --gray5: hsl(0, 0%, 21.7%);
  --gray6: hsl(0, 0%, 24.7%);
  --gray7: hsl(0, 0%, 29.1%);
  --gray8: hsl(0, 0%, 37.5%);
  --gray9: hsl(0, 0%, 43%);
  --gray10: hsl(0, 0%, 50.7%);
  --gray11: hsl(0, 0%, 69.5%);
  --gray12: hsl(0, 0%, 93.5%);
}

body {
  padding-top: 0;
}

.button {
  padding: 8px 12px;
  margin: 0;
  background: var(--gray1);
  border: 1px solid var(--gray3);
  white-space: nowrap;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 500;
  font-family: var(--font-sans);
  cursor: pointer;
  color: var(--gray12);
  transition: border-color 200ms, background 200ms, box-shadow 200ms;
  margin: 1.5rem 0 0;
}

.button p {
  line-height: 1.5;
}

.button:hover {
  background: var(--gray2);
  border-color: var(--gray4);
}

.button[data-active='true'] {
  background: var(--gray3);
  border-color: var(--gray7);
}

.button:focus-visible {
  outline: none;
  box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.06), 0px 1px 0px 0px rgba(0, 0, 0, 0.08),
    0px 2px 2px 0px rgba(0, 0, 0, 0.04), 0px 3px 3px 0px rgba(0, 0, 0, 0.02), 0px 4px 4px 0px rgba(0, 0, 0, 0.01),
    0 0 0 2px rgba(0, 0, 0, 0.15);
}

@media (max-width: 600px) {
  .buttons {
    mask-image: linear-gradient(to right, transparent, black 16px, black calc(100% - 16px), transparent);
  }
}

aside li.active a {
  background: var(--gray3) !important;
  color: var(--gray12) !important;
}

aside li:not(.active) a:hover {
  background: var(--gray2) !important;
}

pre {
  background-color: var(--gray0) !important;
  border: 1px solid var(--gray4);
  margin-bottom: 2rem !important;
}

button[title='Copy code'] {
  background: var(--gray2);
  color: var(--gray10);
}

main > p {
  line-height: 1.5rem !important;
  margin-top: 1rem !important;
}

.nx-text-primary-600 {
  color: var(--gray12) !important;
}

div > a:hover {
  color: var(--gray12) !important;
}

p {
  color: var(--gray12) !important;
}

footer > div {
  padding: 32px 24px !important;
}


================================================
FILE: website/tailwind.config.js
================================================
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx,mdx}',
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',

    // Or if using `src` directory:
    './src/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};


================================================
FILE: website/theme.config.jsx
================================================
export default {
  logo: <span style={{ fontWeight: 600 }}>Sonner</span>,
  project: {
    link: 'https://github.com/emilkowalski/sonner',
  },
  docsRepositoryBase: 'https://github.com/emilkowalski/sonner/tree/main/website',
  useNextSeoProps() {
    return {
      titleTemplate: '%s – Sonner',
    };
  },
  feedback: {
    content: null,
  },
  footer: {
    text: (
      <span>
        MIT {new Date().getFullYear()} ©{' '}
        <a href="https://sonner.emilkowal.ski" target="_blank">
          Sonner
        </a>
        .
      </span>
    ),
  },
  // ... other theme options
};


================================================
FILE: website/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
Download .txt
gitextract_cl8qn5z4/

├── .github/
│   └── workflows/
│       └── playwright.yml
├── .gitignore
├── .prettierrc.js
├── FUNDING.yml
├── LICENSE.md
├── README.md
├── package.json
├── playwright.config.ts
├── pnpm-workspace.yaml
├── src/
│   ├── assets.tsx
│   ├── hooks.tsx
│   ├── index.tsx
│   ├── state.ts
│   ├── styles.css
│   └── types.ts
├── test/
│   ├── .eslintrc.json
│   ├── .gitignore
│   ├── .npmrc
│   ├── .vscode/
│   │   └── settings.json
│   ├── README.md
│   ├── next.config.js
│   ├── package.json
│   ├── src/
│   │   └── app/
│   │       ├── action.tsx
│   │       ├── layout.tsx
│   │       └── page.tsx
│   ├── tests/
│   │   └── basic.spec.ts
│   └── tsconfig.json
├── tsconfig.json
├── turbo.json
└── website/
    ├── .eslintrc.json
    ├── .gitignore
    ├── .vscode/
    │   └── settings.json
    ├── README.md
    ├── next.config.js
    ├── package.json
    ├── postcss.config.js
    ├── src/
    │   ├── components/
    │   │   ├── CodeBlock/
    │   │   │   ├── code-block.module.css
    │   │   │   └── index.tsx
    │   │   ├── ExpandModes/
    │   │   │   └── index.tsx
    │   │   ├── Footer/
    │   │   │   ├── footer.module.css
    │   │   │   └── index.tsx
    │   │   ├── Head/
    │   │   │   └── index.tsx
    │   │   ├── Hero/
    │   │   │   ├── hero.module.css
    │   │   │   └── index.tsx
    │   │   ├── How/
    │   │   │   └── How.tsx
    │   │   ├── Installation/
    │   │   │   ├── index.tsx
    │   │   │   └── installation.module.css
    │   │   ├── Other/
    │   │   │   ├── Other.tsx
    │   │   │   └── other.module.css
    │   │   ├── Position/
    │   │   │   └── index.tsx
    │   │   ├── Types/
    │   │   │   └── Types.tsx
    │   │   └── Usage/
    │   │       └── index.tsx
    │   ├── globals.css
    │   ├── pages/
    │   │   ├── _app.tsx
    │   │   ├── _meta.json
    │   │   ├── getting-started.mdx
    │   │   ├── index.tsx
    │   │   ├── styling.mdx
    │   │   ├── toast.mdx
    │   │   └── toaster.mdx
    │   └── style.css
    ├── tailwind.config.js
    ├── theme.config.jsx
    └── tsconfig.json
Download .txt
SYMBOL INDEX (48 symbols across 10 files)

FILE: src/index.tsx
  constant VISIBLE_TOASTS_AMOUNT (line 22) | const VISIBLE_TOASTS_AMOUNT = 3;
  constant VIEWPORT_OFFSET (line 25) | const VIEWPORT_OFFSET = '24px';
  constant MOBILE_VIEWPORT_OFFSET (line 28) | const MOBILE_VIEWPORT_OFFSET = '16px';
  constant TOAST_LIFETIME (line 31) | const TOAST_LIFETIME = 4000;
  constant TOAST_WIDTH (line 34) | const TOAST_WIDTH = 356;
  constant GAP (line 37) | const GAP = 14;
  constant SWIPE_THRESHOLD (line 40) | const SWIPE_THRESHOLD = 45;
  constant TIME_BEFORE_UNMOUNT (line 43) | const TIME_BEFORE_UNMOUNT = 200;
  function cn (line 45) | function cn(...classes: (string | undefined)[]) {
  function getDefaultSwipeDirections (line 49) | function getDefaultSwipeDirections(position: string): Array<SwipeDirecti...
  function getLoadingIcon (line 244) | function getLoadingIcon() {
  function getDocumentDirection (line 507) | function getDocumentDirection(): ToasterProps['dir'] {
  function assignOffset (line 520) | function assignOffset(defaultOffset: ToasterProps['offset'], mobileOffse...
  function useSonner (line 552) | function useSonner() {

FILE: src/state.ts
  type titleT (line 15) | type titleT = (() => React.ReactNode) | React.ReactNode;
  class Observer (line 17) | class Observer {
    method constructor (line 22) | constructor() {

FILE: src/types.ts
  type ToastTypes (line 3) | type ToastTypes = 'normal' | 'action' | 'success' | 'info' | 'warning' |...
  type PromiseT (line 5) | type PromiseT<Data = any> = Promise<Data> | (() => Promise<Data>);
  type PromiseIExtendedResult (line 7) | interface PromiseIExtendedResult extends ExternalToast {
  type PromiseTExtendedResult (line 11) | type PromiseTExtendedResult<Data = any> =
  type PromiseTResult (line 15) | type PromiseTResult<Data = any> =
  type PromiseExternalToast (line 20) | type PromiseExternalToast = Omit<ExternalToast, 'description'>;
  type PromiseData (line 22) | type PromiseData<ToastData = any> = PromiseExternalToast & {
  type ToastClassnames (line 30) | interface ToastClassnames {
  type ToastIcons (line 48) | interface ToastIcons {
  type Action (line 57) | interface Action {
  type ToastT (line 63) | interface ToastT {
  function isAction (line 93) | function isAction(action: Action | React.ReactNode): action is Action {
  type Position (line 97) | type Position = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right...
  type HeightT (line 98) | interface HeightT {
  type ToastOptions (line 104) | interface ToastOptions {
  type Offset (line 118) | type Offset =
  type ToasterProps (line 128) | interface ToasterProps {
  type SwipeDirection (line 152) | type SwipeDirection = 'top' | 'right' | 'bottom' | 'left';
  type ToastProps (line 154) | interface ToastProps {
  type SwipeStateTypes (line 184) | enum SwipeStateTypes {
  type Theme (line 190) | type Theme = 'light' | 'dark';
  type ToastToDismiss (line 192) | interface ToastToDismiss {
  type ExternalToast (line 197) | type ExternalToast = Omit<ToastT, 'id' | 'type' | 'title' | 'jsx' | 'del...

FILE: test/src/app/action.tsx
  function action (line 4) | async function action() {

FILE: test/src/app/layout.tsx
  function RootLayout (line 6) | function RootLayout({ children }: { children: React.ReactNode }) {

FILE: test/src/app/page.tsx
  function Home (line 9) | function Home({ searchParams }: any) {

FILE: website/src/components/Position/index.tsx
  type Position (line 7) | type Position = (typeof positions)[number];
  function removeAllToasts (line 18) | function removeAllToasts() {

FILE: website/src/pages/_app.tsx
  function Nextra (line 7) | function Nextra({ Component, pageProps }: AppProps): ReactElement {

FILE: website/src/pages/index.tsx
  function Home (line 14) | function Home() {

FILE: website/theme.config.jsx
  method useNextSeoProps (line 7) | useNextSeoProps() {
Condensed preview — 64 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (170K chars).
[
  {
    "path": ".github/workflows/playwright.yml",
    "chars": 662,
    "preview": "name: Playwright Tests\non:\n  push:\n    branches: [main, master]\n  pull_request:\n    branches: [main, master]\njobs:\n  tes"
  },
  {
    "path": ".gitignore",
    "chars": 442,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\ndist\n\n\n# dependencies\nnode_modules"
  },
  {
    "path": ".prettierrc.js",
    "chars": 115,
    "preview": "module.exports = {\n  semi: true,\n  singleQuote: true,\n  tabWidth: 2,\n  trailingComma: 'all',\n  printWidth: 120,\n};\n"
  },
  {
    "path": "FUNDING.yml",
    "chars": 21,
    "preview": "github: emilkowalski\n"
  },
  {
    "path": "LICENSE.md",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright (c) 2023 Emil Kowalski\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "README.md",
    "chars": 867,
    "preview": "https://github.com/vallezw/sonner/assets/50796600/59b95cb7-9068-4f3e-8469-0b35d9de5cf0\n\n[Sonner](https://sonner.emilkowa"
  },
  {
    "path": "package.json",
    "chars": 1670,
    "preview": "{\n  \"name\": \"sonner\",\n  \"version\": \"2.0.7\",\n  \"description\": \"An opinionated toast component for React.\",\n  \"exports\": {"
  },
  {
    "path": "playwright.config.ts",
    "chars": 2058,
    "preview": "import { defineConfig, devices } from '@playwright/test';\n\n/**\n * Read environment variables from file.\n * https://githu"
  },
  {
    "path": "pnpm-workspace.yaml",
    "chars": 43,
    "preview": "packages:\n  - 'website'\n  - '.'\n  - 'test'\n"
  },
  {
    "path": "src/assets.tsx",
    "chars": 2797,
    "preview": "'use client';\nimport React from 'react';\nimport type { ToastTypes } from './types';\n\nexport const getAsset = (type: Toas"
  },
  {
    "path": "src/hooks.tsx",
    "chars": 440,
    "preview": "import React from 'react';\n\nexport const useIsDocumentHidden = () => {\n  const [isDocumentHidden, setIsDocumentHidden] ="
  },
  {
    "path": "src/index.tsx",
    "chars": 30197,
    "preview": "'use client';\n\nimport React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport { CloseIcon, getAsset, Loader } from"
  },
  {
    "path": "src/state.ts",
    "chars": 9262,
    "preview": "import type {\n  ExternalToast,\n  PromiseData,\n  PromiseIExtendedResult,\n  PromiseT,\n  ToastT,\n  ToastToDismiss,\n  ToastT"
  },
  {
    "path": "src/styles.css",
    "chars": 17522,
    "preview": "html[dir='ltr'],\n[data-sonner-toaster][dir='ltr'] {\n  --toast-icon-margin-start: -3px;\n  --toast-icon-margin-end: 4px;\n "
  },
  {
    "path": "src/types.ts",
    "chars": 5243,
    "preview": "import React from 'react';\n\nexport type ToastTypes = 'normal' | 'action' | 'success' | 'info' | 'warning' | 'error' | 'l"
  },
  {
    "path": "test/.eslintrc.json",
    "chars": 40,
    "preview": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": "test/.gitignore",
    "chars": 440,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "test/.npmrc",
    "chars": 29,
    "preview": "package-manager-strict=false\n"
  },
  {
    "path": "test/.vscode/settings.json",
    "chars": 145,
    "preview": "{\n  \"typescript.tsdk\": \"../node_modules/.pnpm/typescript@4.9.5/node_modules/typescript/lib\",\n  \"typescript.enablePromptU"
  },
  {
    "path": "test/README.md",
    "chars": 1748,
    "preview": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js"
  },
  {
    "path": "test/next.config.js",
    "chars": 94,
    "preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {};\n\nmodule.exports = nextConfig;\n"
  },
  {
    "path": "test/package.json",
    "chars": 512,
    "preview": "{\n  \"name\": \"test\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next bu"
  },
  {
    "path": "test/src/app/action.tsx",
    "chars": 409,
    "preview": "'use server';\nimport { createStreamableUI } from 'ai/rsc';\n\nexport async function action() {\n  'use server';\n  let progr"
  },
  {
    "path": "test/src/app/layout.tsx",
    "chars": 268,
    "preview": "export const metadata = {\n  title: 'Create Next App',\n  description: 'Generated by create next app',\n};\n\nexport default "
  },
  {
    "path": "test/src/app/page.tsx",
    "chars": 12001,
    "preview": "'use client';\n\nimport React from 'react';\nimport { Toaster, toast } from 'sonner';\nimport { action } from '@/app/action'"
  },
  {
    "path": "test/tests/basic.spec.ts",
    "chars": 14011,
    "preview": "import { expect, test } from '@playwright/test';\nimport { toast } from 'sonner';\n\ntest.beforeEach(async ({ page }) => {\n"
  },
  {
    "path": "test/tsconfig.json",
    "chars": 669,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"sk"
  },
  {
    "path": "tsconfig.json",
    "chars": 187,
    "preview": "{\n  \"compilerOptions\": {\n    \"jsx\": \"react\",\n    \"target\": \"ES2018\",\n    \"moduleResolution\": \"node\",\n    \"esModuleIntero"
  },
  {
    "path": "turbo.json",
    "chars": 227,
    "preview": "{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"extends\": [\"//\"],\n  \"pipeline\": {\n    \"build\": {\n      \"dependsOn\":"
  },
  {
    "path": "website/.eslintrc.json",
    "chars": 40,
    "preview": "{\n  \"extends\": \"next/core-web-vitals\"\n}\n"
  },
  {
    "path": "website/.gitignore",
    "chars": 385,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": "website/.vscode/settings.json",
    "chars": 145,
    "preview": "{\n  \"typescript.tsdk\": \"../node_modules/.pnpm/typescript@4.9.5/node_modules/typescript/lib\",\n  \"typescript.enablePromptU"
  },
  {
    "path": "website/README.md",
    "chars": 1748,
    "preview": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js"
  },
  {
    "path": "website/next.config.js",
    "chars": 307,
    "preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  experimental: {\n    appDir: true,\n  },\n};\n\nconst withNex"
  },
  {
    "path": "website/package.json",
    "chars": 858,
    "preview": "{\n  \"name\": \"website\",\n  \"version\": \"0.1.0\",\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start"
  },
  {
    "path": "website/postcss.config.js",
    "chars": 83,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "website/src/components/CodeBlock/code-block.module.css",
    "chars": 1080,
    "preview": ".root {\n  padding: 16px;\n  margin: 0;\n  background: var(--gray1);\n  border-radius: 0;\n  position: relative;\n  line-heigh"
  },
  {
    "path": "website/src/components/CodeBlock/index.tsx",
    "chars": 4162,
    "preview": "import React from 'react';\nimport Highlight, { defaultProps } from 'prism-react-renderer';\nimport useMeasure from 'react"
  },
  {
    "path": "website/src/components/ExpandModes/index.tsx",
    "chars": 1136,
    "preview": "import { toast } from 'sonner';\nimport { CodeBlock } from '../CodeBlock';\n\nexport const ExpandModes = ({\n  expand,\n  set"
  },
  {
    "path": "website/src/components/Footer/footer.module.css",
    "chars": 457,
    "preview": ".wrapper {\n  padding: 32px 0;\n  border-top: 1px solid var(--gray3);\n  background: var(--gray1);\n  margin-top: 164px;\n}\n\n"
  },
  {
    "path": "website/src/components/Footer/index.tsx",
    "chars": 561,
    "preview": "import Image from 'next/image';\nimport emil from 'public/emil.jpeg';\nimport styles from './footer.module.css';\n\nexport c"
  },
  {
    "path": "website/src/components/Head/index.tsx",
    "chars": 1243,
    "preview": "import NextHead from 'next/head';\n\nconst ogImage = 'https://sonner.emilkowal.ski/og.png';\n\nconst Head = () => (\n  <NextH"
  },
  {
    "path": "website/src/components/Hero/hero.module.css",
    "chars": 3063,
    "preview": ".wrapper {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  align-items: center;\n}\n\n.toastWrapper {\n  display: "
  },
  {
    "path": "website/src/components/Hero/index.tsx",
    "chars": 1111,
    "preview": "import { toast } from 'sonner';\n\nimport styles from './hero.module.css';\nimport Link from 'next/link';\n\nexport const Her"
  },
  {
    "path": "website/src/components/How/How.tsx",
    "chars": 490,
    "preview": "import React from 'react';\n\nexport const How = () => {\n  return (\n    <>\n      <div>\n        <h2>Want to learn how to ma"
  },
  {
    "path": "website/src/components/Installation/index.tsx",
    "chars": 2492,
    "preview": "'use client';\n\nimport React from 'react';\nimport copy from 'copy-to-clipboard';\nimport { motion, AnimatePresence, Motion"
  },
  {
    "path": "website/src/components/Installation/installation.module.css",
    "chars": 710,
    "preview": ".code {\n  padding: 0 62px 0 12px;\n  border-radius: 6px;\n  background: linear-gradient(to top, var(--gray2), var(--gray1)"
  },
  {
    "path": "website/src/components/Other/Other.tsx",
    "chars": 3984,
    "preview": "import React from 'react';\nimport { useMemo } from 'react';\nimport { toast } from 'sonner';\nimport { CodeBlock } from '."
  },
  {
    "path": "website/src/components/Other/other.module.css",
    "chars": 1013,
    "preview": "ol[dir='ltr'] .headlessClose {\n  --headless-close-start: unset;\n  --headless-close-end: 6px;\n}\n\nol[dir='rtl'] .headlessC"
  },
  {
    "path": "website/src/components/Position/index.tsx",
    "chars": 1364,
    "preview": "import { toast, useSonner } from 'sonner';\nimport { CodeBlock } from '../CodeBlock';\nimport React from 'react';\n\nconst p"
  },
  {
    "path": "website/src/components/Types/Types.tsx",
    "chars": 3175,
    "preview": "import React from 'react';\nimport { toast } from 'sonner';\nimport { CodeBlock } from '../CodeBlock';\n\nconst promiseCode "
  },
  {
    "path": "website/src/components/Usage/index.tsx",
    "chars": 460,
    "preview": "import { CodeBlock } from '../CodeBlock';\n\nexport const Usage = () => {\n  return (\n    <div>\n      <h2>Usage</h2>\n      "
  },
  {
    "path": "website/src/globals.css",
    "chars": 4201,
    "preview": ":root,\n.light {\n  --gray0: #fff;\n  --gray1: hsl(0, 0%, 99%);\n  --gray2: hsl(0, 0%, 97.3%);\n  --gray3: hsl(0, 0%, 95.1%);"
  },
  {
    "path": "website/src/pages/_app.tsx",
    "chars": 383,
    "preview": "import type { ReactElement } from 'react';\nimport type { AppProps } from 'next/app';\nimport { Analytics } from '@vercel/"
  },
  {
    "path": "website/src/pages/_meta.json",
    "chars": 421,
    "preview": "{\n  \"getting-started\": {\n    \"title\": \"Getting Started\",\n    \"href\": \"/getting-started\"\n  },\n  \"-- API\": {\n    \"type\": \""
  },
  {
    "path": "website/src/pages/getting-started.mdx",
    "chars": 1122,
    "preview": "import { Tab, Tabs, Cards, Card, Steps } from 'nextra-theme-docs';\nimport { toast } from 'sonner';\n\n# Getting Started\n\nS"
  },
  {
    "path": "website/src/pages/index.tsx",
    "chars": 1578,
    "preview": "import React, { StrictMode } from 'react';\nimport { Toaster } from 'sonner';\nimport { Installation } from '@/src/compone"
  },
  {
    "path": "website/src/pages/styling.mdx",
    "chars": 1805,
    "preview": "# Styling\n\nStyling can be done globally via `toastOptions`, this way every toast will have the same styling.\n\n```jsx\n<To"
  },
  {
    "path": "website/src/pages/toast.mdx",
    "chars": 7988,
    "preview": "import { toast } from 'sonner';\n\n# Toast()\n\nUse it to render a toast. You can call it from anywhere, even outside of Rea"
  },
  {
    "path": "website/src/pages/toaster.mdx",
    "chars": 4909,
    "preview": "# Toaster\n\nThis component renders all the toasts, you can place it anywhere in your app.\n\n## Customization\n\nYou can see "
  },
  {
    "path": "website/src/style.css",
    "chars": 2959,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n  --gray0: #fff;\n  --gray1: hsl(0, 0%, 99%);\n  --gra"
  },
  {
    "path": "website/tailwind.config.js",
    "chars": 285,
    "preview": "module.exports = {\n  content: [\n    './app/**/*.{js,ts,jsx,tsx,mdx}',\n    './pages/**/*.{js,ts,jsx,tsx,mdx}',\n    './com"
  },
  {
    "path": "website/theme.config.jsx",
    "chars": 592,
    "preview": "export default {\n  logo: <span style={{ fontWeight: 600 }}>Sonner</span>,\n  project: {\n    link: 'https://github.com/emi"
  },
  {
    "path": "website/tsconfig.json",
    "chars": 658,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"sk"
  }
]

About this extraction

This page contains the full source code of the emilkowalski/sonner GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 64 files (156.4 KB), approximately 44.4k tokens, and a symbol index with 48 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!