diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..3dfdde7
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+# Allow personal config overrides
+root = false
+
+[*]
+indent_style = tab
+insert_final_newline = true
+trim_trailing_whitespace = true
+end_of_line = lf
+charset = utf-8
+
+[*.yml]
+indent_style = space
+indent_size = 2
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6313b56
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..330a447
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,42 @@
+# https://vitejs.dev/guide/static-deploy.html#github-pages
+name: Deploy to GitHub Pages
+on:
+ push:
+ branches: ["main"]
+ workflow_dispatch:
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+concurrency:
+ group: "pages"
+ cancel-in-progress: true
+jobs:
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ - name: Set up Node
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: ".nvmrc"
+ cache: "pnpm"
+ - name: Install dependencies
+ run: pnpm install
+ - name: Build
+ run: pnpm build:demo
+ - name: Setup Pages
+ uses: actions/configure-pages@v5
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: "./demo/dist"
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..500642c
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,15 @@
+name: CI Lint
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+jobs:
+ lint:
+ name: Lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ - run: pnpm install
+ - run: pnpm lint
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..cdcd601
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,22 @@
+name: CI Test
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+jobs:
+ test:
+ name: Node.js ${{ matrix.node-version }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ node-version: [18.19, 20.8, 21, 22]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ - run: pnpm install
+ - run: pnpm exec tsimp --start # Preload the transpiler
+ - run: pnpm test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..23d56e1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+coverage
+.tsimp
+dist
+package-lock.json
+yarn.lock
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..c12134b
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+v20.15.0
diff --git a/.xo-config.js b/.xo-config.js
new file mode 100644
index 0000000..bcaf734
--- /dev/null
+++ b/.xo-config.js
@@ -0,0 +1,21 @@
+import { fileURLToPath } from "url";
+
+export default {
+ extends: [
+ "@tommy-mitchell/xo",
+ "@tommy-mitchell/xo/react",
+ "@tommy-mitchell/xo/tailwind",
+ "@tommy-mitchell/xo/dprint",
+ ],
+ settings: {
+ tailwindcss: {
+ config: fileURLToPath(new URL("demo/tailwind.config.ts", import.meta.url)),
+ },
+ },
+ rules: {
+ "@typescript-eslint/naming-convention": "off",
+ "simple-import-sort/imports": ["error", {
+ groups: [["^\\u0000", "^node:", "^react", "^react-dom", "^@?\\w", "^", "^\\.", "^.+\\.s?css$"]],
+ }],
+ },
+};
diff --git a/demo/index.html b/demo/index.html
new file mode 100644
index 0000000..91c3ca9
--- /dev/null
+++ b/demo/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ use-boop-simple
+
+
+
+
+
+
+
diff --git a/demo/package.json b/demo/package.json
new file mode 100644
index 0000000..4748209
--- /dev/null
+++ b/demo/package.json
@@ -0,0 +1,31 @@
+{
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "host": "vite --host",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@icons-pack/react-simple-icons": "10.0.0",
+ "@radix-ui/react-slider": "1.2.0",
+ "lucide-react": "0.428.0",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "tailwind-merge": "2.5.2",
+ "use-boop-simple": "workspace:*"
+ },
+ "devDependencies": {
+ "@mdx-js/rollup": "3.0.1",
+ "@tailwindcss/typography": "0.5.14",
+ "@types/mdx": "2.0.13",
+ "@types/react": "18.3.3",
+ "@types/react-dom": "18.3.0",
+ "@vitejs/plugin-react-swc": "3.7.0",
+ "autoprefixer": "10.4.20",
+ "tailwind-mode-aware-colors": "2.0.2",
+ "tailwindcss": "3.4.10",
+ "vite": "5.4.1"
+ }
+}
diff --git a/demo/src/components/App.tsx b/demo/src/components/App.tsx
new file mode 100644
index 0000000..f5816ee
--- /dev/null
+++ b/demo/src/components/App.tsx
@@ -0,0 +1,32 @@
+import { SiGithub } from "@icons-pack/react-simple-icons";
+import About from "../content/about.md";
+import { BoopInput } from "./BoopInput.tsx";
+import { IconButton } from "./IconButton.tsx";
+import { Prose } from "./Prose.tsx";
+import { ThemeToggle } from "./ThemeToggle.tsx";
+
+export function App() {
+ return (
+
+
+
+
+ use-boop-simple
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/demo/src/components/Boop.tsx b/demo/src/components/Boop.tsx
new file mode 100644
index 0000000..c6b43ea
--- /dev/null
+++ b/demo/src/components/Boop.tsx
@@ -0,0 +1,53 @@
+import { useEffect } from "react";
+import { twMerge } from "tailwind-merge";
+import { useBoop, type UseBoopOptions } from "use-boop-simple";
+
+export type BoopProps =
+ & Omit, "onClick" | "type">
+ & Readonly<{
+ trigger?: boolean;
+ onClick?: () => void;
+ }>
+ & UseBoopOptions;
+
+export function Boop({
+ className,
+ friction,
+ onClick,
+ rotate,
+ scale,
+ tension,
+ timing,
+ trigger,
+ x,
+ y,
+ ...props
+}: BoopProps) {
+ const boopOptions = { friction, rotate, scale, tension, timing, x, y };
+ const [style, boopTrigger] = useBoop(boopOptions);
+
+ useEffect(() => {
+ if (trigger) {
+ boopTrigger();
+ }
+ }, [boopTrigger, trigger]);
+
+ const triggers = props.disabled ? {} : {
+ onFocus: boopTrigger,
+ onPointerEnter: boopTrigger,
+ };
+
+ return (
+