[
  {
    "path": ".changeset/README.md",
    "content": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works\nwith multi-package repos, or single-package repos to help you version and publish your code. You can\nfind the full documentation for it [in our repository](https://github.com/changesets/changesets)\n\nWe have a quick list of common questions to get you started engaging with this project in\n[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)\n"
  },
  {
    "path": ".changeset/config.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@3.1.2/schema.json\",\n  \"changelog\": \"@changesets/cli/changelog\",\n  \"commit\": false,\n  \"fixed\": [],\n  \"linked\": [],\n  \"access\": \"public\",\n  \"baseBranch\": \"main\",\n  \"updateInternalDependencies\": \"patch\",\n  \"ignore\": [\"example\"]\n}\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: CI\non: [push]\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Begin CI...\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Use Node 20\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20.x\n          cache: pnpm\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n        env:\n          CI: true\n\n      - name: Lint\n        run: pnpm lint\n        env:\n          CI: true\n\n      - name: Test\n        run: pnpm test\n        env:\n          CI: true\n\n      - name: Build\n        run: pnpm build\n        env:\n          CI: true\n\n  react-compatibility:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        react-version: [16.14.0, 17.0.2, 18.3.1, 19.0.0]\n\n    steps:\n      - name: Begin compatibility checks...\n        uses: actions/checkout@v4\n\n      - name: Setup pnpm\n        uses: pnpm/action-setup@v4\n\n      - name: Use Node 20\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20.x\n          cache: pnpm\n\n      - name: Setup Bun\n        uses: oven-sh/setup-bun@v2\n\n      - name: Install dependencies\n        run: pnpm install --frozen-lockfile\n        env:\n          CI: true\n\n      - name: Test React ${{ matrix.react-version }} compatibility\n        run: pnpm add -Dw react@${{ matrix.react-version }} react-dom@${{ matrix.react-version }} --ignore-scripts\n\n      - name: Run tests\n        run: pnpm test\n        env:\n          CI: true\n\n      - name: Build with matrix React version\n        run: pnpm build\n        env:\n          CI: true\n"
  },
  {
    "path": ".gitignore",
    "content": "*.log\n.DS_Store\nnode_modules\n.pnpm-store\n.cache\ndist\n.parcel-cache"
  },
  {
    "path": ".husky/pre-commit",
    "content": "pnpm lint\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# react-toggle-dark-mode\n\n## 2.0.0\n\n### Major Changes\n\n- Modernize build setup and harden DarkModeSwitch for React 16–19\n\n  ### Changed\n  - Migrated project tooling from Yarn to pnpm.\n  - Updated root scripts and package metadata for pnpm-based workflows.\n  - Switched CI to modern GitHub Actions (`checkout@v4`, `setup-node@v4`, Node 20) and pnpm caching.\n  - Added a React compatibility CI matrix for React 16, 17, 18, and 19.\n  - Replaced `react-spring` import usage with `@react-spring/web`.\n  - Updated dependencies/devDependencies across root and example app (React 19, newer TypeScript/tooling).\n  - Modernized example app bootstrapping to `createRoot` and module script loading.\n  - Updated README install instructions to show pnpm usage.\n  - Added `.pnpm-store` to `.gitignore`.\n\n  ### Improved\n  - Refactored `DarkModeSwitch` animation property merging to avoid mutating defaults.\n  - Improved prop typing (button-oriented props) and event handling in `DarkModeSwitch`.\n  - Improved accessibility defaults (`role=\"switch\"`, `aria-checked`, default label behavior).\n  - Strengthened test suite with coverage for:\n    - animation-property merge behavior\n    - non-mutation of defaults\n    - accessibility attributes\n    - unique mask IDs across multiple component instances\n\n  ### Removed\n  - Removed legacy Yarn lockfiles (`yarn.lock`, `example/yarn.lock`).\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Jose Felix\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <h1>React Toggle Dark Mode</h1>\n</div>\n<p>\n  <a href=\"https://www.npmjs.com/package/react-toggle-dark-mode\" target=\"_blank\">\n    <img alt=\"Version\" src=\"https://img.shields.io/npm/v/react-toggle-dark-mode.svg\">\n  </a>\n  <img src=\"https://img.shields.io/badge/node-%3E%3D20-blue.svg\" />\n  <a href=\"#\" target=\"_blank\">\n    <img alt=\"License: MIT\" src=\"https://img.shields.io/badge/License-MIT-yellow.svg\" />\n  </a> \n  <a href=\"http://makeapullrequest.com\" target=\"_blank\">\n    <img alt=\"PRs Welcome\" src=\"https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square\" />\n  </a>\n  <img alt=\"Bundle size\" src=\"https://badgen.net/bundlephobia/minzip/react-toggle-dark-mode\" /> \n</p>\n\n> 🌃 Animated dark mode toggle as seen in blogs!\n\nSupports React 16.14+ through 19.\n\n![Interactive sun and moon transition](./docs/demo.gif)\n\n## Prerequisites\n\n- node >=20\n\n## Installation\n\n```shell\nnpm i react-toggle-dark-mode\n```\n\nor with pnpm:\n\n```shell\npnpm add react-toggle-dark-mode\n```\n\n## Usage\n\n```jsx\nimport * as React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { DarkModeSwitch } from 'react-toggle-dark-mode';\n\nconst App = () => {\n  const [isDarkMode, setDarkMode] = React.useState(false);\n\n  const toggleDarkMode = (checked: boolean) => {\n    setDarkMode(checked);\n  };\n\n  return (\n    <DarkModeSwitch\n      style={{ marginBottom: '2rem' }}\n      checked={isDarkMode}\n      onChange={toggleDarkMode}\n      size={120}\n    />\n  );\n};\n\nconst rootElement = document.getElementById('root');\n\nif (!rootElement) {\n  throw new Error('Unable to find root element');\n}\n\ncreateRoot(rootElement).render(<App />);\n```\n\n## API\n\n### DarkModeSwitch\n\n#### Props\n\n| Name                | Type                                               | Default Value                   | Description                                                                                  |\n| ------------------- | -------------------------------------------------- | ------------------------------- | -------------------------------------------------------------------------------------------- |\n| onChange            | \\(checked: boolean\\) => void                       |                                 | Event that triggers when icon is clicked.                                                    |\n| checked             | boolean                                            | false                           | Current icon state.                                                                          |\n| style               | React.CSSProperties                                |                                 | CSS properties object applied to the button wrapper.                                         |\n| size                | number \\| string                                   | 24                              | SVG size.                                                                                    |\n| animationProperties | Partial animation properties object                | defaultProperties \\(see below\\) | Override only the fields you want; missing fields are merged with defaults.                 |\n| moonColor           | string                                             | white                           | Color of the moon.                                                                           |\n| sunColor            | string                                             | black                           | Color of the sun.                                                                            |\n| aria-label          | string                                             | Toggle dark mode                | Accessible label for the control. Ignored when `aria-labelledby` is provided.               |\n| aria-labelledby     | string                                             |                                 | Links the control to an external label element.                                              |\n| onClick             | \\(event: React.MouseEvent<HTMLButtonElement>\\)=>void |                                 | Optional button click handler. Call `event.preventDefault()` to prevent toggling on click. |\n\nAll valid button attributes (except `children`) are forwarded to the underlying button element.\n\n### Default Animation Properties\n\n```javascript\nconst defaultProperties = {\n  dark: {\n    circle: {\n      r: 9,\n    },\n    mask: {\n      cx: '50%',\n      cy: '23%',\n    },\n    svg: {\n      transform: 'rotate(40deg)',\n    },\n    lines: {\n      opacity: 0,\n    },\n  },\n  light: {\n    circle: {\n      r: 5,\n    },\n    mask: {\n      cx: '100%',\n      cy: '0%',\n    },\n    svg: {\n      transform: 'rotate(90deg)',\n    },\n    lines: {\n      opacity: 1,\n    },\n  },\n  springConfig: { mass: 4, tension: 250, friction: 35 },\n};\n```\n\n## Contributors\n\nThanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):\n\n<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->\n<!-- prettier-ignore-start -->\n<!-- markdownlint-disable -->\n<table>\n  <tr>\n    <td align=\"center\"><a href=\"https://jfelix.info\"><img src=\"https://avatars1.githubusercontent.com/u/21092519?s=460&u=55be9996a2652c79880c62ad50d06e17639456e8&v=4\" width=\"100px;\" alt=\"\"/><br /><sub><b>Jose Felix</b></sub></a><br /><a href=\"https://github.com/JoseRFelix/react-toggle-dark-mode/commits?author=JoseRFelix\" title=\"Code\">💻</a> <a href=\"https://github.com/JoseRFelix/react-toggle-dark-mode/commits?author=JoseRFelix\" title=\"Documentation\">📖</a> <a href=\"https://github.com/JoseRFelix/react-toggle-dark-mode/commits?author=JoseRFelix\" title=\"Tests\">⚠️</a></td>    \n  </tr>  \n</table>\n\n<!-- markdownlint-enable -->\n<!-- prettier-ignore-end -->\n\n<!-- ALL-CONTRIBUTORS-LIST:END -->\n\nThis project follows the [all-contributors](https://allcontributors.org) specification.\nContributions of any kind are welcome!\n\n## Show your support\n\nGive a ⭐️ if this project helped you!\n"
  },
  {
    "path": "example/.npmignore",
    "content": "node_modules\n.cache\ndist"
  },
  {
    "path": "example/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>Playground</title>\n  </head>\n\n  <body style=\"margin: 0 ;\">\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"./index.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "example/index.tsx",
    "content": "import * as React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { DarkModeSwitch } from '../src';\n\nfunction arrayN(size: number) {\n  return new Array(size).fill(undefined);\n}\n\nconst App = () => {\n  const [isDarkMode, setDarkMode] = React.useState(false);\n  const [toggleAmount, setToggleAmount] = React.useState(0);\n\n  const toggleDarkMode = (checked: boolean) => {\n    setDarkMode(checked);\n  };\n\n  const addToggle = () => {\n    setToggleAmount((prevValue) => prevValue + 1);\n  };\n\n  return (\n    <div\n      style={{\n        minHeight: '100vh',\n        display: 'flex',\n        flexDirection: 'column',\n        justifyContent: 'center',\n        alignItems: 'center',\n        background: isDarkMode ? '#1b242c' : 'white',\n        transition: '0.2s background',\n      }}\n    >\n      <DarkModeSwitch\n        style={{ marginBottom: '2rem' }}\n        checked={isDarkMode}\n        onChange={toggleDarkMode}\n        size={120}\n      />\n      <DarkModeSwitch\n        style={{ marginBottom: '2rem' }}\n        checked={isDarkMode}\n        onChange={toggleDarkMode}\n        size={80}\n      />\n      <DarkModeSwitch\n        style={{ marginBottom: '2rem' }}\n        checked={isDarkMode}\n        onChange={toggleDarkMode}\n        moonColor=\"red\"\n        size={30}\n      />\n      {arrayN(toggleAmount).map((_, index) => (\n        <DarkModeSwitch\n          key={index}\n          style={{ marginBottom: '2rem' }}\n          checked={isDarkMode}\n          onChange={toggleDarkMode}\n          size={Math.floor(Math.random() * 20) + 20}\n        />\n      ))}\n      <button onClick={addToggle}>Add toggle</button>\n    </div>\n  );\n};\n\nconst rootElement = document.getElementById('root');\n\nif (!rootElement) {\n  throw new Error('Unable to find root element');\n}\n\ncreateRoot(rootElement).render(<App />);\n"
  },
  {
    "path": "example/package.json",
    "content": "{\n  \"name\": \"example\",\n  \"version\": \"1.0.0\",\n  \"license\": \"MIT\",\n  \"private\": true,\n  \"scripts\": {\n    \"start\": \"parcel index.html\",\n    \"build\": \"parcel build index.html\"\n  },\n  \"dependencies\": {\n    \"react\": \"19.0.0\",\n    \"react-dom\": \"19.0.0\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"parcel\": \"^2.16.4\",\n    \"typescript\": \"^3.4.5\"\n  }\n}\n"
  },
  {
    "path": "example/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"allowSyntheticDefaultImports\": false,\n    \"target\": \"es5\",\n    \"module\": \"commonjs\",\n    \"jsx\": \"react\",\n    \"moduleResolution\": \"node\",\n    \"noImplicitAny\": false,\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": false,\n    \"removeComments\": true,\n    \"strictNullChecks\": true,\n    \"preserveConstEnums\": true,\n    \"sourceMap\": true,\n    \"lib\": [\"es2015\", \"es2016\", \"dom\"],\n    \"baseUrl\": \".\",\n    \"types\": [\"node\"]\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"packageManager\": \"pnpm@10.28.2\",\n  \"version\": \"2.0.0\",\n  \"license\": \"MIT\",\n  \"main\": \"dist/index.js\",\n  \"typings\": \"dist/index.d.ts\",\n  \"private\": false,\n  \"files\": [\n    \"dist\",\n    \"src\"\n  ],\n  \"engines\": {\n    \"node\": \">=20\"\n  },\n  \"scripts\": {\n    \"start\": \"tsdx watch\",\n    \"build\": \"tsdx build\",\n    \"test\": \"tsdx test\",\n    \"lint\": \"tsdx lint\",\n    \"prepare\": \"husky && tsdx build\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=16\"\n  },\n  \"prettier\": {\n    \"printWidth\": 80,\n    \"semi\": true,\n    \"singleQuote\": true,\n    \"trailingComma\": \"es5\"\n  },\n  \"name\": \"react-toggle-dark-mode\",\n  \"description\": \"Animated dark mode toggle as seen in blogs!\",\n  \"author\": \"Jose R. Felix (https://jfelix.info)\",\n  \"module\": \"dist/react-toggle-dark-mode.esm.js\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/JoseRFelix/react-toggle-dark-mode\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/JoseRFelix/react-toggle-dark-mode/issues\"\n  },\n  \"homepage\": \"https://github.com/JoseRFelix/react-toggle-dark-mode#readme\",\n  \"devDependencies\": {\n    \"@types/jest\": \"^24.9.1\",\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.2.3\",\n    \"bunchee\": \"^6.9.4\",\n    \"husky\": \"^9.1.7\",\n    \"react\": \"19.0.0\",\n    \"react-dom\": \"19.0.0\",\n    \"tsdx\": \"^2.0.0\",\n    \"tslib\": \"^2.8.1\",\n    \"typescript\": \"^5.9.3\",\n    \"vitest\": \"^4.0.18\",\n    \"@changesets/cli\": \"^2.29.8\"\n  },\n  \"dependencies\": {\n    \"@react-spring/web\": \"^10.0.3\"\n  }\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - .\n  - example\n"
  },
  {
    "path": "src/index.tsx",
    "content": "import * as React from 'react';\nimport { useSpring, animated } from '@react-spring/web';\n\nexport const defaultProperties = {\n  dark: {\n    circle: {\n      r: 9,\n    },\n    mask: {\n      cx: '50%',\n      cy: '23%',\n    },\n    svg: {\n      transform: 'rotate(40deg)',\n    },\n    lines: {\n      opacity: 0,\n    },\n  },\n  light: {\n    circle: {\n      r: 5,\n    },\n    mask: {\n      cx: '100%',\n      cy: '0%',\n    },\n    svg: {\n      transform: 'rotate(90deg)',\n    },\n    lines: {\n      opacity: 1,\n    },\n  },\n  springConfig: { mass: 4, tension: 250, friction: 35 },\n};\n\nlet REACT_TOGGLE_DARK_MODE_GLOBAL_ID = 0;\ntype AnimationProperties = typeof defaultProperties;\ntype ThemeProperties = AnimationProperties['dark'];\ntype PartialThemeProperties = {\n  circle?: Partial<ThemeProperties['circle']>;\n  mask?: Partial<ThemeProperties['mask']>;\n  svg?: Partial<ThemeProperties['svg']>;\n  lines?: Partial<ThemeProperties['lines']>;\n};\ntype DarkModeSwitchAnimationProperties = {\n  dark?: PartialThemeProperties;\n  light?: PartialThemeProperties;\n  springConfig?: Partial<AnimationProperties['springConfig']>;\n};\n\nconst mergeThemeProperties = (\n  theme: ThemeProperties,\n  customTheme?: PartialThemeProperties\n): ThemeProperties => ({\n  circle: { ...theme.circle, ...customTheme?.circle },\n  mask: { ...theme.mask, ...customTheme?.mask },\n  svg: { ...theme.svg, ...customTheme?.svg },\n  lines: { ...theme.lines, ...customTheme?.lines },\n});\n\nconst resolveAnimationProperties = (\n  animationProperties?: DarkModeSwitchAnimationProperties\n): AnimationProperties => {\n  if (!animationProperties || animationProperties === defaultProperties) {\n    return defaultProperties;\n  }\n\n  return {\n    dark: mergeThemeProperties(\n      defaultProperties.dark,\n      animationProperties.dark\n    ),\n    light: mergeThemeProperties(\n      defaultProperties.light,\n      animationProperties.light\n    ),\n    springConfig: {\n      ...defaultProperties.springConfig,\n      ...animationProperties.springConfig,\n    },\n  };\n};\n\ntype ButtonProps = Omit<\n  React.ButtonHTMLAttributes<HTMLButtonElement>,\n  'onChange' | 'children'\n>;\nexport interface Props extends ButtonProps {\n  onChange: (checked: boolean) => void;\n  checked: boolean;\n  style?: React.CSSProperties;\n  size?: number | string;\n  animationProperties?: DarkModeSwitchAnimationProperties;\n  moonColor?: string;\n  sunColor?: string;\n}\n\nexport const DarkModeSwitch: React.FC<Props> = ({\n  onChange,\n  checked = false,\n  size = 24,\n  animationProperties,\n  moonColor = 'white',\n  sunColor = 'black',\n  style,\n  onClick: onClickProp,\n  role,\n  tabIndex,\n  'aria-label': ariaLabel,\n  'aria-labelledby': ariaLabelledBy,\n  ...rest\n}) => {\n  const [id] = React.useState(() => {\n    REACT_TOGGLE_DARK_MODE_GLOBAL_ID += 1;\n    return REACT_TOGGLE_DARK_MODE_GLOBAL_ID;\n  });\n\n  const properties = React.useMemo(\n    () => resolveAnimationProperties(animationProperties),\n    [animationProperties]\n  );\n\n  const { circle, svg, lines, mask } = properties[checked ? 'dark' : 'light'];\n  const { springConfig } = properties;\n\n  const svgContainerProps = useSpring({\n    ...svg,\n    config: springConfig,\n  });\n  const centerCircleProps = useSpring({\n    ...circle,\n    config: springConfig,\n  });\n  const maskedCircleProps = useSpring({\n    ...mask,\n    config: springConfig,\n  });\n  const linesProps = useSpring({\n    ...lines,\n    config: springConfig,\n  });\n\n  const toggle = React.useCallback(\n    () => onChange(!checked),\n    [checked, onChange]\n  );\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<HTMLButtonElement>) => {\n      event.stopPropagation();\n      onClickProp?.(event);\n\n      if (!event.defaultPrevented) {\n        toggle();\n      }\n    },\n    [onClickProp, toggle]\n  );\n\n  const uniqueMaskId = `circle-mask-${id}`;\n\n  return (\n    <button\n      type=\"button\"\n      role={role ?? 'switch'}\n      aria-checked={checked}\n      aria-label={ariaLabelledBy ? undefined : ariaLabel ?? 'Toggle dark mode'}\n      aria-labelledby={ariaLabelledBy}\n      tabIndex={tabIndex ?? 0}\n      onClick={onClick}\n      style={{\n        cursor: 'pointer',\n        padding: 0,\n        border: 0,\n        background: 'transparent',\n        lineHeight: 0,\n        ...style,\n      }}\n      {...rest}\n    >\n      <animated.svg\n        xmlns=\"http://www.w3.org/2000/svg\"\n        width={size}\n        height={size}\n        viewBox=\"0 0 24 24\"\n        color={checked ? moonColor : sunColor}\n        fill=\"none\"\n        strokeWidth=\"2\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        stroke=\"currentColor\"\n        style={svgContainerProps}\n      >\n        <mask id={uniqueMaskId}>\n          <rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" fill=\"white\" />\n          <animated.circle\n            // @ts-ignore\n            style={maskedCircleProps}\n            r=\"9\"\n            fill=\"black\"\n          />\n        </mask>\n\n        <animated.circle\n          cx=\"12\"\n          cy=\"12\"\n          fill={checked ? moonColor : sunColor}\n          style={centerCircleProps}\n          mask={`url(#${uniqueMaskId})`}\n        />\n        <animated.g stroke=\"currentColor\" style={linesProps}>\n          <line x1=\"12\" y1=\"1\" x2=\"12\" y2=\"3\" />\n          <line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"23\" />\n          <line x1=\"4.22\" y1=\"4.22\" x2=\"5.64\" y2=\"5.64\" />\n          <line x1=\"18.36\" y1=\"18.36\" x2=\"19.78\" y2=\"19.78\" />\n          <line x1=\"1\" y1=\"12\" x2=\"3\" y2=\"12\" />\n          <line x1=\"21\" y1=\"12\" x2=\"23\" y2=\"12\" />\n          <line x1=\"4.22\" y1=\"19.78\" x2=\"5.64\" y2=\"18.36\" />\n          <line x1=\"18.36\" y1=\"5.64\" x2=\"19.78\" y2=\"4.22\" />\n        </animated.g>\n      </animated.svg>\n    </button>\n  );\n};\n"
  },
  {
    "path": "test/index.test.tsx",
    "content": "import * as React from 'react';\nimport { useSpring } from '@react-spring/web';\nimport { renderToStaticMarkup } from 'react-dom/server';\nimport { describe, expect, it, vi } from 'vitest';\nimport { DarkModeSwitch, defaultProperties } from '../src';\n\nvi.mock('@react-spring/web', async () => {\n  const actual =\n    await vi.importActual<typeof import('@react-spring/web')>('@react-spring/web');\n  return {\n    ...actual,\n    useSpring: vi.fn((props: unknown) => props),\n  };\n});\n\ndescribe('DarkModeSwitch', () => {\n  it('renders without crashing', () => {\n    // Keep this test React-version-agnostic for the CI matrix (16/17/18/19):\n    // server rendering avoids client API differences like render/createRoot.\n    const html = renderToStaticMarkup(\n      <DarkModeSwitch onChange={() => {}} checked={false} />\n    );\n\n    expect(html).toContain('<svg');\n  });\n\n  it('does not mutate default animation properties when overridden', () => {\n    const originalDarkTransform = defaultProperties.dark.svg.transform;\n    const originalLightRadius = defaultProperties.light.circle.r;\n    const originalTension = defaultProperties.springConfig.tension;\n\n    renderToStaticMarkup(\n      <DarkModeSwitch\n        onChange={() => {}}\n        checked={false}\n        animationProperties={{\n          dark: { svg: { transform: 'rotate(0deg)' } },\n          springConfig: { tension: 1 },\n        }}\n      />\n    );\n\n    expect(defaultProperties.dark.svg.transform).toBe(originalDarkTransform);\n    expect(defaultProperties.light.circle.r).toBe(originalLightRadius);\n    expect(defaultProperties.springConfig.tension).toBe(originalTension);\n  });\n\n  it('merges custom animation properties with defaults', () => {\n    const springMock = vi.mocked(useSpring);\n    springMock.mockClear();\n\n    renderToStaticMarkup(\n      <DarkModeSwitch\n        onChange={() => {}}\n        checked={true}\n        animationProperties={{\n          dark: {\n            svg: { transform: 'rotate(0deg)' },\n            mask: { cx: '42%' },\n          },\n          springConfig: { tension: 1 },\n        }}\n      />\n    );\n\n    const [svgProps, circleProps, maskProps, linesProps] = springMock.mock.calls.map(\n      ([props]) => props as any\n    );\n    const expectedConfig = {\n      ...defaultProperties.springConfig,\n      tension: 1,\n    };\n\n    expect(springMock).toHaveBeenCalledTimes(4);\n    expect(svgProps.transform).toBe('rotate(0deg)');\n    expect(svgProps.config).toEqual(expectedConfig);\n    expect(circleProps.r).toBe(defaultProperties.dark.circle.r);\n    expect(circleProps.config).toEqual(expectedConfig);\n    expect(maskProps.cx).toBe('42%');\n    expect(maskProps.cy).toBe(defaultProperties.dark.mask.cy);\n    expect(linesProps.opacity).toBe(defaultProperties.dark.lines.opacity);\n    expect(linesProps.config).toEqual(expectedConfig);\n  });\n\n  it('renders switch accessibility attributes by default', () => {\n    const html = renderToStaticMarkup(\n      <DarkModeSwitch onChange={() => {}} checked={true} />\n    );\n\n    expect(html).toContain('role=\"switch\"');\n    expect(html).toContain('aria-checked=\"true\"');\n    expect(html).toContain('aria-label=\"Toggle dark mode\"');\n    expect(html).toContain('tabindex=\"0\"');\n  });\n\n  it('supports aria-labelledby without requiring an aria-label', () => {\n    const html = renderToStaticMarkup(\n      <DarkModeSwitch\n        onChange={() => {}}\n        checked={false}\n        aria-labelledby=\"dark-mode-switch-label\"\n      />\n    );\n\n    expect(html).toContain('aria-labelledby=\"dark-mode-switch-label\"');\n    expect(html).not.toContain('aria-label=\"Toggle dark mode\"');\n  });\n\n  it('generates unique mask ids across multiple instances', () => {\n    const html = renderToStaticMarkup(\n      <>\n        <DarkModeSwitch onChange={() => {}} checked={false} />\n        <DarkModeSwitch onChange={() => {}} checked={true} />\n      </>\n    );\n\n    const ids = html.match(/circle-mask-\\d+/g) || [];\n    const uniqueIds = Array.from(new Set(ids));\n\n    expect(uniqueIds.length).toBe(2);\n  });\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"include\": [\"src\", \"types\"],\n  \"compilerOptions\": {\n    \"module\": \"esnext\",\n    \"lib\": [\"dom\", \"esnext\"],\n    \"importHelpers\": true,\n    \"declaration\": true,\n    \"sourceMap\": true,\n    \"rootDir\": \"./src\",\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"moduleResolution\": \"node\",\n    \"baseUrl\": \"./\",\n    \"paths\": {\n      \"*\": [\"src/*\", \"node_modules/*\"]\n    },\n    \"jsx\": \"react\",\n    \"esModuleInterop\": true\n  }\n}\n"
  }
]