Repository: stephenscaff/react-animated-cursor
Branch: master
Commit: 94a3b7252f3d
Files: 25
Total size: 42.2 KB
Directory structure:
gitextract_qw9xtc50/
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── .parcelrc
├── .prettierrc
├── LICENSE
├── docs/
│ └── src/
│ ├── App.js
│ ├── DemoContent.js
│ ├── DemoCustomTest.js
│ ├── DemoFooter.js
│ ├── DemoHeader.js
│ ├── demo-styles.css
│ ├── index.html
│ └── index.js
├── lib/
│ ├── AnimatedCursor.tsx
│ ├── AnimatedCursor.types.ts
│ ├── helpers/
│ │ └── find.ts
│ ├── hooks/
│ │ ├── useEventListener.ts
│ │ └── useIsTouchdevice.ts
│ └── index.ts
├── package.json
├── readme.md
├── rollup.config.mjs
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
*.json
build
config
dist
**node_modules**
./node_modules/**
================================================
FILE: .eslintrc.json
================================================
{
"env": {
"node": true,
"browser": true,
"es6": true,
"commonjs": true
},
"plugins": ["react", "react-hooks", "prettier"],
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/recommended"
],
"settings": {
"react": {
"version": "18.2.0"
},
"import/resolver": {
"node": {
"paths": ["./"]
}
}
},
"ignorePatterns": ["temp.js", "node_modules"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"requireConfigFile": false,
"plugins": ["@typescript-eslint"],
"ecmaFeatures": {
"jsx": true,
"experimentalObjectRestSpread": true,
"modules": true
}
},
"rules": {
"linebreak-style": 0,
"no-underscore-dangle": 0,
"no-nested-ternary": 0,
"prettier/prettier": "error",
"react-hooks/exhaustive-deps": "warn",
"react/react-in-jsx-scope": "off",
"react-hooks/rules-of-hooks": "error",
"react/jsx-uses-react": "off",
"react/jsx-uses-vars": "error",
"react/no-unescaped-entities": 0,
"react/prefer-stateless-function": 1
},
"globals": {
"grecaptcha": "readonly"
}
}
================================================
FILE: .gitignore
================================================
node_modules
.cache
npm-debug.log
.DS_Store
.cache
.tmp
*.log
.parcel-cache
dist
================================================
FILE: .npmignore
================================================
.DS_Store
.babelrc
.cache
.parcel-cache
.tmp
*.log
.gitignore
node_modules
npm-debug.log
demo
docs
lib
src
================================================
FILE: .parcelrc
================================================
{
"extends": "@parcel/config-default",
"transformers": {
"*.{js,mjs,jsx,cjs,ts,tsx}": [
"@parcel/transformer-js",
"@parcel/transformer-react-refresh-wrap"
]
}
}
================================================
FILE: .prettierrc
================================================
{
"printWidth": 80,
"trailingComma": "none",
"singleQuote": true,
"tabWidth": 2,
"semi": false,
"jsxSingleQuote": false,
"quoteProps": "as-needed",
"bracketSpacing": true,
"jsxBracketSameLine": false
}
================================================
FILE: LICENSE
================================================
ISC License
Copyright 2021 Stephen Scaff
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
================================================
FILE: docs/src/App.js
================================================
import { React, useState, useEffect } from 'react'
import AnimatedCursor from '../../lib'
import DemoContent from './DemoContent'
import DemoCustomTest from './DemoCustomTest'
import DemoHeader from './DemoHeader'
import DemoFooter from './DemoFooter'
import './demo-styles.css'
export default function App() {
const [state, setState] = useState('donut')
const searchParams = new URLSearchParams(document.location.search)
const cursorParam = searchParams.get('cursor')
useEffect(() => {
if (cursorParam) setState(cursorParam)
}, [cursorParam])
return (
<div className="App">
{state === 'default' && <AnimatedCursor />}
{state === 'donut' && (
<AnimatedCursor
innerSize={8}
outerSize={35}
innerScale={1}
outerScale={2}
outerAlpha={0}
showSystemCursor={false}
hasBlendMode={true}
outerStyle={{
border: '3px solid var(--cursor-color)'
}}
innerStyle={{
backgroundColor: 'var(--cursor-color)'
}}
/>
)}
{state === 'blendmode' && (
<AnimatedCursor
color="255,255,255"
innerSize={8}
outerSize={35}
innerScale={1}
outerScale={2}
outerAlpha={1}
hasBlendMode={true}
outerStyle={{
mixBlendMode: 'exclusion'
}}
innerStyle={{
backgroundColor: 'var(--cursor-color)',
mixBlendMode: 'exclusion'
}}
/>
)}
{state === 'custom' && (
<AnimatedCursor
clickables={[
{
target: '.small',
innerScale: 3,
outerScale: 1
},
{
target: '.big',
innerScale: 9,
outerScale: 7
},
{
target: '.blue',
color: 'blue',
innerStyle: {
backgroundColor: 'blue'
},
outerStyle: {
backgroundColor: 'rgb(0,0,255,0.4)'
}
},
{
target: '#blueDonut',
innerSize: 8,
outerSize: 35,
innerScale: 1,
outerScale: 2,
outerAlpha: 0,
showSystemCursor: true,
hasBlendMode: true,
outerStyle: {
border: '3px solid blue'
},
innerStyle: {
backgroundColor: 'blue'
}
},
'a',
'input[type="text"]',
'input[type="email"]',
'input[type="number"]',
'input[type="submit"]',
'input[type="image"]',
'label[for]',
'select',
'textarea',
'button',
'.link'
]}
color={'220, 90, 90'}
innerScale={0.6}
innerSize={8}
outerAlpha={0.4}
outerScale={6}
outerSize={8}
showSystemCursor={false}
trailingSpeed={8}
/>
)}
<DemoHeader />
<DemoContent />
{state === 'custom' && <DemoCustomTest />}
<DemoFooter />
</div>
)
}
================================================
FILE: docs/src/DemoContent.js
================================================
import React from 'react'
const s = {
section: {
paddingTop: '6em',
width: '80%',
maxWidth: '36em',
margin: '0 auto 1em'
},
title: {
marginBottom: '1em',
fontSize: '3em',
fontWeight: 800,
textAlign: 'center',
lineHeight: 1
},
pretitle: {
textAlign: 'center'
},
subtitle: {
textAlign: 'center'
},
sep: {
border: 0,
margin: '2em auto',
height: 2,
width: '3em',
backgroundColor: 'rgba(255, 255, 255, 0.5)'
}
}
export default function Content() {
return (
<section style={s.section}>
<p style={s.pretitle}>Demos</p>
<h1 style={s.title}>React Animated Cursor</h1>
<p style={s.subtitle}>
A component by <a href="http://stephenscaff.com/">Stephen Scaff</a>
</p>
<hr style={s.sep} />
<p>
React animated cursor is a React component that creates a custom cursor
experience. You can craft a variety of cursor types, and animate
movement, hover and clicking properties.
</p>
<p>
Hover over these <a>links</a> and see how that animated cursor does it's
thing. Kinda nifty, right? Not applicable to most projects, but a nice
move for more interactive/immersive stuff... if you're into that kinda
thing? Here's another <a href="">link to nowhere.</a>
</p>
<p>Essentially, the cursor consists:</p>
<ul>
<li>
An inner dot (<code>cursorInner</code>)
</li>
<li>
An outer, outlining circle (<code>cursorOuter</code>), with slight
opacity based on the dot/primary color
</li>
<li>
An inversely scaling effect between the inner and outer cursor parts
on click or link hover
</li>
</ul>
<p>
Style props exist for in the inner and outer cursor allow you to easily
create unique cursor types. Play with <a>css variables</a> to influence
the cursor, cursor outline size, and amount of scale on target hover.
</p>
<h3>Demo Cursors</h3>
<p>Here's a few cursor types you can create to test</p>
<ul>
<li>
<a href="?cursor=default">Default</a>
</li>
<li>
<a href="?cursor=donut">Donut</a>
</li>
<li>
<a href="?cursor=blendmode">Blendmode</a>
</li>
<li>
<a href="?cursor=custom">Custom</a>
</li>
</ul>
<h3>Test Clickables</h3>
<p>Here's a collection of test clickable elements to hover over:</p>
<ul>
<li>
<a>Basic Link Tag</a>
</li>
<li>
<button>Buttons</button>
</li>
<li>
<input type="submit" value="Submit" />
</li>
<li>
<select>
<option>Select</option>
</select>
</li>
<li>
<input
type="image"
id="image-input"
alt="Image Input"
src="https://cdn2.iconfinder.com/data/icons/button-v1/30/25-512.png"
width="30px"
/>
</li>
<li>
<label htmlFor="label_for">Label For</label>
<input type="radio" name="gender" id="label_for" value="label_for" />
</li>
<li>
<div className="link">Class name ="link"</div>
</li>
</ul>
</section>
)
}
================================================
FILE: docs/src/DemoCustomTest.js
================================================
import React from 'react'
const s = {
section: {
paddingBottom: '6em',
width: '80%',
maxWidth: '36em',
margin: '0 auto 1em'
}
}
export default function Content() {
return (
<section style={s.section}>
<h3>Test custom Clickables</h3>
<p>
Here's a collection of additional elements to test custom behaviors:
</p>
<ul>
<li>
<div className="small">Class name ="small"</div>
</li>
<li>
<div className="big">Class name ="big"</div>
</li>
<li>
<div className="blue">Class name ="blue"</div>
</li>
<li>
<div id="blueDonut">Id ="blueDonut"</div>
</li>
</ul>
</section>
)
}
================================================
FILE: docs/src/DemoFooter.js
================================================
import React from 'react'
const s = {
footer: {
position: 'relative',
width: '100%',
padding: '6em 0 3em',
backgroundColor: '#2f2c2c',
textAlign: 'center'
},
footer__grid: {
position: 'relative',
maxWidth: '95%',
margin: '0 auto'
},
footer__border: {
height: '1px',
width: '100%',
marginBottom: '4em',
border: '0',
backgroundColor: 'rgba(255,255,255,0.4)'
},
footer__copy: {
fontSize: '0.8em'
},
footer__icon: {
width: '2em',
margin: '0 auto',
textAlign: 'center'
},
footer__icon_vector: {
fill: '#fff'
}
}
export default function DemoHeader() {
return (
<section style={s.footer}>
<div style={s.footer__grid}>
<hr style={s.footer__border} />
<div style={s.footer__icon}>
<svg
style={s.footer__icon_vector}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
>
<path d="M262.468 0H251.85C153.818 0 72.902 79.089 72.902 177.126v324.407c0 4.026 2.884 7.684 6.511 9.431 3.617 1.747 8.214 1.257 11.354-1.257l45.149-36.007 35.303 35.23c4.087 4.087 10.744 4.087 14.831 0l34.471-34.454 34.462 34.454c4.087 4.087 10.713 4.087 14.8 0l34.456-34.454 34.454 34.454c4.087 4.087 10.709 4.087 14.796 0l34.454-34.454 34.453 34.454a10.452 10.452 0 0 0 7.398 3.065c1.349 0 2.129-.255 3.427-.797 3.908-1.614 5.878-5.436 5.878-9.666V177.126C439.098 79.089 360.499 0 262.468 0zm155.705 476.275-23.411-23.991c-4.087-4.087-10.418-4.087-14.505 0l-34.309 34.454-34.381-34.454a10.35 10.35 0 0 0-7.362-3.065 10.392 10.392 0 0 0-7.38 3.065l-34.444 34.454-34.449-34.454a10.455 10.455 0 0 0-14.792 0l-34.454 34.454-34.452-34.454c-3.77-3.77-10.357-4.107-14.51-.777l-35.897 28.252V177.126c0-86.502 71.527-156.201 158.023-156.201h10.617c86.495 0 155.705 69.699 155.705 156.201v299.149z" />
<path d="M194.383 156.262c-14.423 0-26.157 11.73-26.157 26.157s11.733 26.157 26.157 26.157c14.421 0 26.156-11.73 26.156-26.157s-11.735-26.157-26.156-26.157zm0 31.388a5.237 5.237 0 0 1-5.231-5.231 5.237 5.237 0 0 1 5.231-5.231 5.239 5.239 0 0 1 5.231 5.231 5.238 5.238 0 0 1-5.231 5.231zM319.935 156.262c-14.422 0-26.157 11.73-26.157 26.157s11.735 26.157 26.157 26.157 26.157-11.73 26.157-26.157-11.735-26.157-26.157-26.157zm0 31.388a5.238 5.238 0 0 1-5.231-5.231c0-2.881 2.345-5.231 5.231-5.231s5.231 2.35 5.231 5.231a5.238 5.238 0 0 1-5.231 5.231z" />
</svg>
</div>
<p style={s.footer__copy}>
A little thing by{' '}
<a href="https://stephenscaff.com" target="_blank" rel="noreferrer">
Stephen Scaff
</a>
</p>
</div>
</section>
)
}
================================================
FILE: docs/src/DemoHeader.js
================================================
import React from 'react'
const s = {
header: {
position: 'fixed',
top: 0,
left: 0,
width: '100%',
backgroundColor: '#2f2c2c'
},
header__grid: {
position: 'relative',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
height: '4em',
maxWidth: '95%',
margin: '0 auto'
},
nav: {
marginLeft: 'auto'
},
nav__link: {
marginLeft: '1em',
fontSize: '0.8em',
fontWeight: '400'
},
brand: {
display: 'flex',
alignItems: 'center'
},
brand__icon_inner: {
position: 'relative',
right: '-5px',
display: 'block',
height: '10px',
width: '10px',
borderRadius: '100%',
backgroundColor: '#fff'
},
brand__icon_outer: {
position: 'relative',
left: '-10px',
top: '-8px',
display: 'block',
height: '6px',
width: '6px',
borderRadius: '100%',
backgroundColor: 'rgba(255,255,255,0.4)'
}
}
export default function DemoHeader() {
return (
<section style={s.header}>
<div style={s.header__grid}>
<div style={s.brand}>
<span style={s.brand__icon_inner}></span>
<span style={s.brand__icon_outer}></span>
</div>
<nav style={s.nav}>
<a
style={s.nav__link}
href="https://github.com/stephenscaff/react-animated-cursor"
>
Repo
</a>
<a
style={s.nav__link}
href="https://github.com/stephenscaff/react-animated-cursor/blob/master/readme.md"
>
Docs
</a>
<a
style={s.nav__link}
href="https://stephenscaff.github.io/react-animated-cursor/"
>
Demos
</a>
</nav>
</div>
</section>
)
}
================================================
FILE: docs/src/demo-styles.css
================================================
/* Cursor vars if you wanna use css over css in js */
:root {
--cursor-color: #fff;
}
html,
body {
background-color: #2f2c2c;
color: #fff;
font-family: 'Inter', sans-serif;
}
/* Demo Content */
a {
text-decoration: none;
color: #fff;
font-weight: 600;
border-bottom: 1px solid rgba(255, 255, 255, 0.7);
transition: 0.5s ease;
}
a:hover {
color: rgba(255, 255, 255, 0.5);
border-bottom-color: rgba(255, 255, 255, 0.1);
}
section {
line-height: 1.7;
font-weight: 300;
}
h3 {
font-size: 1.3em;
margin: 2em 0 1em;
}
ul {
margin: 2em 0 1em;
padding-left: 1em;
}
ul li {
padding-bottom: 1.25em;
padding-left: 0;
}
================================================
FILE: docs/src/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;500;800&display=swap"
rel="stylesheet"
/>
<title>React Animated Cursor - by Stephen Scaff</title>
<meta name="description" content="React Animated Cursor is a React component that allows you to craft a custom cursor experience. Created by Stephen Scaff."/>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="app"></div>
<script type="module" src="index.js"></script>
</body>
</html>
================================================
FILE: docs/src/index.js
================================================
import React from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
const container = document.getElementById('app')
const root = createRoot(container)
root.render(<App />)
================================================
FILE: lib/AnimatedCursor.tsx
================================================
import {
useState,
useEffect,
useCallback,
useRef,
CSSProperties,
useMemo
} from 'react'
import { useEventListener } from './hooks/useEventListener'
import type {
AnimatedCursorProps,
AnimatedCursorCoordinates,
AnimatedCursorOptions,
Clickable
} from './AnimatedCursor.types'
import find from './helpers/find'
import useIsTouchdevice from './hooks/useIsTouchdevice'
/**
* Cursor Core
* Replaces the native cursor with a custom animated cursor, consisting
* of an inner and outer dot that scale inversely based on hover or click.
*
* @author Stephen Scaff (github.com/stephenscaff)
*
* @param {object} obj
* @param {array} obj.clickables - array of clickable selectors
* @param {string} obj.children - element that is shown instead of the inner dot
* @param {string} obj.color - rgb color value
* @param {number} obj.innerScale - inner cursor scale amount
* @param {number} obj.innerSize - inner cursor size in px
* @param {object} obj.innerStyle - style object for inner cursor
* @param {number} obj.outerAlpha - level of alpha transparency for color
* @param {number} obj.outerScale - outer cursor scale amount
* @param {number} obj.outerSize - outer cursor size in px
* @param {object} obj.outerStyle - style object for outer cursor
* @param {bool} obj.showSystemCursor - show/hide system cursor1
* @param {number} obj.trailingSpeed - speed the outer cursor trails at
*/
function CursorCore({
clickables = [
'a',
'input[type="text"]',
'input[type="email"]',
'input[type="number"]',
'input[type="submit"]',
'input[type="image"]',
'label[for]',
'select',
'textarea',
'button',
'.link'
],
children,
color = '220, 90, 90',
innerScale = 0.6,
innerSize = 8,
innerStyle,
outerAlpha = 0.4,
outerScale = 6,
outerSize = 8,
outerStyle,
showSystemCursor = false,
trailingSpeed = 8
}: AnimatedCursorProps) {
const defaultOptions = useMemo(
() => ({
children,
color,
innerScale,
innerSize,
innerStyle,
outerAlpha,
outerScale,
outerSize,
outerStyle
}),
[
children,
color,
innerScale,
innerSize,
innerStyle,
outerAlpha,
outerScale,
outerSize,
outerStyle
]
)
const cursorOuterRef = useRef<HTMLDivElement>(null)
const cursorInnerRef = useRef<HTMLDivElement>(null)
const requestRef = useRef<number | null>(null)
const previousTimeRef = useRef<number | null>(null)
const [coords, setCoords] = useState<AnimatedCursorCoordinates>({
x: 0,
y: 0
})
const [isVisible, setIsVisible] = useState(false)
const [options, setOptions] = useState(defaultOptions)
const [isActive, setIsActive] = useState<boolean | AnimatedCursorOptions>(
false
)
const [isActiveClickable, setIsActiveClickable] = useState(false)
const endX = useRef(0)
const endY = useRef(0)
/**
* Primary Mouse move event
* @param {number} clientX - MouseEvent.clientX
* @param {number} clientY - MouseEvent.clientY
*/
const onMouseMove = useCallback((event: MouseEvent) => {
const { clientX, clientY } = event
setCoords({ x: clientX, y: clientY })
if (cursorInnerRef.current !== null) {
cursorInnerRef.current.style.top = `${clientY}px`
cursorInnerRef.current.style.left = `${clientX}px`
}
endX.current = clientX
endY.current = clientY
}, [])
// Outer Cursor Animation Delay
const animateOuterCursor = useCallback(
(time: number) => {
if (previousTimeRef.current !== undefined) {
coords.x += (endX.current - coords.x) / trailingSpeed
coords.y += (endY.current - coords.y) / trailingSpeed
if (cursorOuterRef.current !== null) {
cursorOuterRef.current.style.top = `${coords.y}px`
cursorOuterRef.current.style.left = `${coords.x}px`
}
}
previousTimeRef.current = time
requestRef.current = requestAnimationFrame(animateOuterCursor)
},
[requestRef] // eslint-disable-line
)
// Outer cursor RAF setup / cleanup
useEffect(() => {
requestRef.current = requestAnimationFrame(animateOuterCursor)
return () => {
if (requestRef.current !== null) {
cancelAnimationFrame(requestRef.current)
}
}
}, [animateOuterCursor])
/**
* Calculates amount to scale cursor in px3
* @param {number} orignalSize - starting size
* @param {number} scaleAmount - Amount to scale
* @returns {String} Scale amount in px
*/
const getScaleAmount = (orignalSize: number, scaleAmount: number) => {
return `${parseInt(String(orignalSize * scaleAmount))}px`
}
// Scales cursor by HxW
const scaleBySize = useCallback(
(
cursorRef: HTMLDivElement | null,
orignalSize: number,
scaleAmount: number
) => {
if (cursorRef) {
cursorRef.style.height = getScaleAmount(orignalSize, scaleAmount)
cursorRef.style.width = getScaleAmount(orignalSize, scaleAmount)
}
},
[]
)
// Mouse Events State updates
const onMouseDown = useCallback(() => setIsActive(true), [])
const onMouseUp = useCallback(() => setIsActive(false), [])
const onMouseEnterViewport = useCallback(() => setIsVisible(true), [])
const onMouseLeaveViewport = useCallback(() => setIsVisible(false), [])
useEventListener('mousemove', onMouseMove)
useEventListener('mousedown', onMouseDown)
useEventListener('mouseup', onMouseUp)
useEventListener('mouseover', onMouseEnterViewport)
useEventListener('mouseout', onMouseLeaveViewport)
// Cursors Hover/Active State
useEffect(() => {
if (isActive) {
scaleBySize(cursorInnerRef.current, options.innerSize, options.innerScale)
scaleBySize(cursorOuterRef.current, options.outerSize, options.outerScale)
} else {
scaleBySize(cursorInnerRef.current, options.innerSize, 1)
scaleBySize(cursorOuterRef.current, options.outerSize, 1)
}
}, [
options.innerSize,
options.innerScale,
options.outerSize,
options.outerScale,
scaleBySize,
isActive
])
// Cursors Click States
useEffect(() => {
if (isActiveClickable) {
scaleBySize(
cursorInnerRef.current,
options.innerSize,
options.innerScale * 1.2
)
scaleBySize(
cursorOuterRef.current,
options.outerSize,
options.outerScale * 1.4
)
}
}, [
options.innerSize,
options.innerScale,
options.outerSize,
options.outerScale,
scaleBySize,
isActiveClickable
])
// Cursor Visibility Statea
useEffect(() => {
if (cursorInnerRef.current == null || cursorOuterRef.current == null) return
if (isVisible) {
cursorInnerRef.current.style.opacity = '1'
cursorOuterRef.current.style.opacity = '1'
} else {
cursorInnerRef.current.style.opacity = '0'
cursorOuterRef.current.style.opacity = '0'
}
}, [isVisible])
// Click event state updates
useEffect(() => {
const clickableEls = document.querySelectorAll<HTMLElement>(
clickables
.map((clickable) =>
typeof clickable === 'object' && clickable?.target
? clickable.target
: clickable ?? ''
)
.join(',')
)
clickableEls.forEach((el) => {
if (!showSystemCursor) el.style.cursor = 'none'
const clickableOptions =
typeof clickables === 'object'
? find(
clickables,
(clickable: Clickable) =>
typeof clickable === 'object' && el.matches(clickable.target)
)
: {}
const options = {
...defaultOptions,
...clickableOptions
}
el.addEventListener('mouseover', () => {
setIsActive(true)
setOptions(options)
})
el.addEventListener('click', () => {
setIsActive(true)
setIsActiveClickable(false)
})
el.addEventListener('mousedown', () => {
setIsActiveClickable(true)
})
el.addEventListener('mouseup', () => {
setIsActive(true)
})
el.addEventListener('mouseout', () => {
setIsActive(false)
setIsActiveClickable(false)
setOptions(defaultOptions)
})
})
return () => {
clickableEls.forEach((el) => {
const clickableOptions =
typeof clickables === 'object'
? find(
clickables,
(clickable: Clickable) =>
typeof clickable === 'object' && el.matches(clickable.target)
)
: {}
const options = {
...defaultOptions,
...clickableOptions
}
el.removeEventListener('mouseover', () => {
setIsActive(true)
setOptions(options)
})
el.removeEventListener('click', () => {
setIsActive(true)
setIsActiveClickable(false)
})
el.removeEventListener('mousedown', () => {
setIsActiveClickable(true)
})
el.removeEventListener('mouseup', () => {
setIsActive(true)
})
el.removeEventListener('mouseout', () => {
setIsActive(false)
setIsActiveClickable(false)
setOptions(defaultOptions)
})
})
}
}, [isActive, clickables, showSystemCursor, defaultOptions])
useEffect(() => {
if (typeof window === 'object' && !showSystemCursor) {
document.body.style.cursor = 'none'
}
}, [showSystemCursor])
const coreStyles: CSSProperties = {
zIndex: 999,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
position: 'fixed',
borderRadius: '50%',
pointerEvents: 'none',
transform: 'translate(-50%, -50%)',
transition:
'opacity 0.15s ease-in-out, height 0.2s ease-in-out, width 0.2s ease-in-out'
}
// Cursor Styles
const styles = {
cursorInner: {
width: !options.children ? options.innerSize : 'auto',
height: !options.children ? options.innerSize : 'auto',
backgroundColor: !options.children
? `rgba(${options.color}, 1)`
: 'transparent',
...coreStyles,
...(options.innerStyle && options.innerStyle)
},
cursorOuter: {
width: options.outerSize,
height: options.outerSize,
backgroundColor: `rgba(${options.color}, ${options.outerAlpha})`,
...coreStyles,
...(options.outerStyle && options.outerStyle)
}
}
return (
<>
<div ref={cursorOuterRef} style={styles.cursorOuter} />
<div ref={cursorInnerRef} style={styles.cursorInner}>
<div
style={{
opacity: !options.children ? 0 : 1,
transition: 'opacity 0.3s ease-in-out'
}}
>
{options.children}
</div>
</div>
</>
)
}
/**
* AnimatedCursor
* Calls and passes props to CursorCore if not a touch/mobile device.
*/
function AnimatedCursor({
children,
clickables,
color,
innerScale,
innerSize,
innerStyle,
outerAlpha,
outerScale,
outerSize,
outerStyle,
showSystemCursor,
trailingSpeed
}: AnimatedCursorProps) {
const isTouchdevice = useIsTouchdevice()
if (typeof window !== 'undefined' && isTouchdevice) {
return <></>
}
return (
<CursorCore
clickables={clickables}
color={color}
innerScale={innerScale}
innerSize={innerSize}
innerStyle={innerStyle}
outerAlpha={outerAlpha}
outerScale={outerScale}
outerSize={outerSize}
outerStyle={outerStyle}
showSystemCursor={showSystemCursor}
trailingSpeed={trailingSpeed}
>
{children}
</CursorCore>
)
}
export default AnimatedCursor
================================================
FILE: lib/AnimatedCursor.types.ts
================================================
import { CSSProperties, ReactNode } from 'react'
export interface AnimatedCursorOptions {
children?: ReactNode
color?: string
innerScale?: number
innerSize?: number
innerStyle?: CSSProperties
outerAlpha?: number
outerScale?: number
outerSize?: number
outerStyle?: CSSProperties
}
export type Clickable = string | ({ target: string } & AnimatedCursorOptions)
export interface AnimatedCursorProps extends AnimatedCursorOptions {
clickables?: Clickable[]
showSystemCursor?: boolean
trailingSpeed?: number
}
export interface AnimatedCursorCoordinates {
x: number
y: number
}
================================================
FILE: lib/helpers/find.ts
================================================
export default function findInArray<T>(
arr: T[],
callback: (element: T, index: number, array: T[]) => boolean,
...args
): T | undefined {
if (typeof callback !== 'function') {
throw new TypeError('callback must be a function')
}
const list = Object(arr)
// Makes sure it always has a positive integer as length.
const length = list.length >>> 0
const thisArg = args[2]
for (let i = 0; i < length; i++) {
const element = list[i]
if (callback.call(thisArg, element, i, list)) {
return element
}
}
return undefined
}
================================================
FILE: lib/hooks/useEventListener.ts
================================================
import { useEffect, useRef } from 'react'
type AllEventMaps = HTMLElementEventMap & DocumentEventMap & WindowEventMap
export function useEventListener<K extends keyof HTMLElementEventMap>(
type: K,
listener: (event: HTMLElementEventMap[K]) => void,
element: HTMLElement
): void
export function useEventListener<K extends keyof DocumentEventMap>(
type: K,
listener: (event: DocumentEventMap[K]) => void,
element: Document
): void
export function useEventListener<K extends keyof WindowEventMap>(
type: K,
listener: (event: WindowEventMap[K]) => void,
element?: Window
): void
export function useEventListener<K extends keyof AllEventMaps>(
type: K,
listener: (event: AllEventMaps[K]) => void,
element?: HTMLElement | Document | Window | null
) {
const listenerRef = useRef(listener)
useEffect(() => {
listenerRef.current = listener
})
useEffect(() => {
const el = element === undefined ? window : element
const internalListener = (ev: AllEventMaps[K]) => {
return listenerRef.current(ev)
}
el?.addEventListener(
type,
internalListener as EventListenerOrEventListenerObject
)
return () => {
el?.removeEventListener(
type,
internalListener as EventListenerOrEventListenerObject
)
}
}, [type, element])
}
================================================
FILE: lib/hooks/useIsTouchdevice.ts
================================================
import { useEffect, useState } from 'react'
const useIsTouchdevice = (): boolean => {
const [isTouchdevice, setIsTouchdevice] = useState<boolean>()
useEffect(() => {
if (typeof window !== 'undefined') {
setIsTouchdevice(window.matchMedia('(hover: none)').matches)
}
}, [])
return isTouchdevice
}
export default useIsTouchdevice
================================================
FILE: lib/index.ts
================================================
import AnimatedCursor from './AnimatedCursor'
export default AnimatedCursor
================================================
FILE: package.json
================================================
{
"name": "react-animated-cursor",
"version": "2.11.1",
"description": "An animated custom cursor component in React.",
"author": "Stephen Scaff <stephenscaff.com>",
"homepage": "https://stephenscaff.github.io/react-animated-cursor/",
"main": "dist/index.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"browser": "dist/index.umd.js",
"files": [
"dist/index.js",
"dist/index.es.js",
"dist/index.umd.js",
"dist/index.d.ts"
],
"targets": {
"main": false,
"module": false,
"browser": false,
"types": false
},
"scripts": {
"clean": "rm -rf ./dist",
"build": "rollup -c",
"dev": "parcel ./docs/src/index.html --dist-dir ./docs/dist",
"demo:clean": "rm -rf ./docs/dist",
"demo:start": "parcel ./docs/src/index.html --dist-dir ./docs/dist",
"demo:build": "parcel build ./docs/src/index.html --dist-dir ./docs/dist --public-url ./",
"demo:deploy": "npm run demo:build && gh-pages -d ./docs/dist",
"prepare": "npm run build",
"prepublish": "rm -rf ./dist && npm run build",
"lint": "eslint \"lib/**/*.+(ts|tsx)\" --fix ",
"format": "prettier --write \"lib/**/*.+(ts|tsx)\""
},
"keywords": [
"react cursor",
"custom cursor",
"animated cursor"
],
"license": "ISC",
"repository": {
"type": "git",
"url": "https://github.com/stephenscaff/react-animated-cursor"
},
"bugs": {
"url": "https://github.com/stephenscaff/react-animated-cursor/issues"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.0",
"@rollup/plugin-node-resolve": "^15.0.2",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^11.1.1",
"@types/react": "^18.2.6",
"@types/react-dom": "^18.2.4",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.36.2",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"gh-pages": "^5.0.0",
"parcel": "^2.3.2",
"prettier": "^2.0.5",
"process": "^0.11.10",
"react": "18.2.0",
"react-dom": "18.2.0",
"rollup": "^3.22.0",
"rollup-plugin-dts": "^5.3.0",
"rollup-plugin-peer-deps-external": "^2.2.2",
"typescript": "^5.0.4"
}
}
================================================
FILE: readme.md
================================================
# React Animated Cursor
A React component that replaces the native cursor with a custom animated [jawn](https://www.urbandictionary.com/define.php?term=Jawn). Available options and props allow you to easily craft a unique cursor experience.
## Contents
1. [📌 Features](#-features)
2. [🎯 Quickstart](#-quickstart)
3. [🤖 Commands](#-commands)
4. [🧬 Options](#-options)
5. [🕹️ Usage](#-usage)
6. [🎨 Cursor Types](#-cursor-types)
7. [📓 Notes](#-notes)
8. [📅 To Dos](#-to-dos)
<br/>
## 📌 Features
### The custom cursor is comprised of
- An inner dot (`cursorInner`)
- An outer, outlining circle (`cursorOuter`), with slight opacity based on the dot/primary color
- A slight trailing animation of the outer outline
- An inversely scaling effect between the inner and outer cursor parts on click or link hover
Options exist for modifying the color and scaling of the cursor elements (see props/options below). Style props for in the inner and outer cursor allow you to easily create unique cursor types.
[Live Demo→](https://stephenscaff.github.io/react-animated-cursor/)
<br/>
## 🎯 Quickstart
### Install package from npm
`npm i react-animated-cursor`
### Add to you project
Add to a global location, like `_app.js`
```
import React from "react";
import AnimatedCursor from "react-animated-cursor"
export default function App() {
return (
<div className="App">
<AnimatedCursor />
</div>
);
}
```
<br>
## 🤖 Commands
**Install** `npm i react-animated-cursor` <br/>
**Build**: `npm run build` <br/>
**Dev**: `npm run dev` <br/>
**Demo Run**: `npm run demo:start` <br/>
**Demo Build**: `npm run demo:build` <br/>
**Demo Clean**: `npm run demo:clean` <br/>
### Demo
The demo is bundled with [`Parcel.js`](https://parceljs.org/) and served up at [http://localhost:1234/](http://localhost:1234/).
### Dist
On build, `lib` populates `dist` with commonjs, es, umd versions of the component.
<br/>
## 🕹️ Usage
```
import React from "react";
import AnimatedCursor from "react-animated-cursor"
export default function App() {
return (
<div className="App">
<AnimatedCursor />
</div>
);
}
```
### Example Usage - with options
```
import React from "react";
import AnimatedCursor from "react-animated-cursor"
export default function App() {
return (
<div className="App">
<AnimatedCursor
innerSize={8}
outerSize={8}
color='193, 11, 111'
outerAlpha={0.2}
innerScale={0.7}
outerScale={5}
clickables={[
'a',
'input[type="text"]',
'input[type="email"]',
'input[type="number"]',
'input[type="submit"]',
'input[type="image"]',
'label[for]',
'select',
'textarea',
'button',
'.link'
]}
/>
</div>
);
}
```
### Example Usage - with simple options and custom config for one class
```
import React from "react";
import AnimatedCursor from "react-animated-cursor"
export default function App() {
return (
<div className="App">
<AnimatedCursor
innerSize={8}
outerSize={8}
color='193, 11, 111'
outerAlpha={0.2}
innerScale={0.7}
outerScale={5}
clickables={[
'a',
'input[type="text"]',
'input[type="email"]',
'input[type="number"]',
'input[type="submit"]',
'input[type="image"]',
'label[for]',
'select',
'textarea',
'button',
'.link',
{
target: '.custom',
options: {
innerSize: 12,
outerSize: 12,
color: '255, 255, 255',
outerAlpha: 0.3,
innerScale: 0.7,
outerScale: 5
}
}
]}
/>
</div>
);
}
```
### Client Components, Next.js, SSR
In previous versions of the component, integration with Next's SSR environment required using a `Dynamic Import`.
However, as of version `2.10.1`, **you _should_ be good to go with a simple `import`.**
Relevant updates:
- Included module directive `'use client'` to indicate a client side component.
- Updated `useEventListener` hook with `window` checks.
- Wrapped the `document` use in a check.
However, if you do run into any issues, you could try including with Dynamic Import.
**Next's Dynamic Import**
```
'use client'; // indicates Client Component
// Import with next's dynamic import
import dynamic from 'next/dynamic';
const AnimatedCursor = dynamic(() => import('react-animated-cursor'), {
ssr: false,
});
<AnimatedCursor/>
```
<br/>
## 🧬 Options
<!-- prettier-ignore -->
| Option | Type | Description | Default |
| ---- | ---- | -------- | -------|
| `clickables` | array | Collection of selectors cursor that trigger cursor interaction or object with single target and possibly the rest of the options listed below | `['a', 'input[type="text"]', 'input[type="email"]', 'input[type="number"]', 'input[type="submit"]', 'input[type="image"]', 'label[for]', 'select', 'textarea', 'button', '.link']` |
| `color` | string | rgb value | `220, 90, 90` |
| `innerScale` | number | amount dot scales on click or link hover | `0.7` |
| `innerSize` | number | Size (px) of inner cursor dot | `8` |
| `innerStyle` | object | provides custom styles / css to inner cursor | `null` |
| `outerAlpha` | number | amount of alpha transparency for outer cursor dot | `0.4` |
| `outerScale` | number | amount outer dot scales on click or link hover | `5` |
| `outerSize` | number | Size (px) of outer cursor outline | `8` |
| `outerStyle` | object | provides custom styles / css to outer cursor | `null` |
| `showSystemCursor` | boolean | Show system/brower cursor | `false` |
| `trailingSpeed` | number | Outer dot's trailing speed | `8` |
<br/>
## 🎨 Cursor Types
You can use the `innerStyle` and `outerStyle` props to provide custom styles and create a variery of custom cursor types. Additionally, you can pass custom styles and css vars to create unique cursors or update style based on events.
### Dynamic Styles
Use CSS variables with `innerStyle` and `outerStyle` props to create dynamic styles that you can easily update.
For example, perhaps you have a light and dark mode experience and what your cursor to also adapt it's colors.
**CSS Vars**
```
html {
--cursor-color: #333
}
html.dark-mode {
--cursor-color: #fff
}
```
**Pass CSS Var as Style Props**
```
<AnimatedCursor
innerSize={8}
outerSize={35}
innerScale={1}
outerScale={1.7}
outerAlpha={0}
outerStyle={{
border: '3px solid var(--cursor-color)'
}}
innerStyle={{
backgroundColor: 'var(--cursor-color)'
}}
/>
```
### Donut Cursor
A donut style cursor basically resembles a donut. You can easily create on by applying using the `outerStyle` props to apply an outer border
```
<AnimatedCursor
innerSize={8}
outerSize={35}
innerScale={1}
outerScale={2}
outerAlpha={0}
hasBlendMode={true}
innerStyle={{
backgroundColor: 'var(--cursor-color)'
}}
outerStyle={{
border: '3px solid var(--cursor-color)'
}}
/>
```
[Donut Demo→](https://stephenscaff.github.io/react-animated-cursor?cursor=donut)
<br/>
### Blend Mode Cursor
You can use CSS mix-blend-mode with the style props to create an intersting cursor effect on hover that inverts the content's color. Works best with white / black cursors.
```
<AnimatedCursor
color="#fff"
innerSize={8}
outerSize={35}
innerScale={1}
outerScale={1.7}
outerAlpha={0}
outerStyle={{
mixBlendMode: 'exclusion'
}}
/>
```
[Blend Mode Demo→](https://stephenscaff.github.io/react-animated-cursor?cursor=blendmode)
<br/>
## 📓 Notes
### Mobile / Touch
`helpers/isDevice.js` uses UA sniffing to determine if on a common device so we can avoid rendering cursors. Yes... I know, there are other and probably better ways to handle this. Whatevers.
<br/>
## 📅 To Dos
- ~~Either remove on mobile, or provide touch events.~~
- ~~Separate click and hover scalings to provide a different scaling when clicking on links/clickables~~
- ~~Fix transform blur in Safari, which may mean migrating from `scale` to a `width` &`height` update~~ 4/4/23
- ~~Make clickables (cursor targets / selectors) a prop~~
- ~~Add PropType checks~~
- ~~Open cursor styles as props~~
- ~~Add ability to maintain system cursor for the squeamish~~ 4/4/23
- ~~Migrate to TS~~
- ~~Allow for different behavior based on the element hovered~~
- Options to control cursor transition speed and bezier
- Solution for impacting state during route changes
- Add some proper tests
<br/>
Have fun ya'll.
================================================
FILE: rollup.config.mjs
================================================
import commonjs from '@rollup/plugin-commonjs'
import external from 'rollup-plugin-peer-deps-external'
import resolve from '@rollup/plugin-node-resolve'
import typescript from '@rollup/plugin-typescript'
import dts from 'rollup-plugin-dts'
import pkg from './package.json' assert { type: 'json' }
const umdGlobals = {
react: 'React',
'react-animated-cursor': 'AnimatedCursor',
'react/jsx-runtime': 'jsxRuntime'
}
const config = [
{
external: ['react', 'react-dom'],
input: 'lib/index.ts',
output: [
{
file: pkg.main,
format: 'cjs',
banner: "'use client';"
},
{
file: pkg.module,
format: 'esm',
banner: "'use client';"
},
{
file: pkg.browser,
format: 'umd',
name: 'AnimatedCursor',
globals: umdGlobals,
banner: "'use client';"
}
],
plugins: [
external(),
resolve(),
commonjs(),
typescript({
exclude: 'node_modules'
})
]
},
{
external: ['react', 'react-dom'],
input: 'lib/index.ts',
output: [{ file: pkg.types, format: 'es' }],
plugins: [external(), resolve(), dts()]
}
]
export default config
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"jsx": "react-jsx",
"moduleResolution": "node",
"target": "es5",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"allowJs": true
},
"include": ["lib"],
"exclude": ["node_modules"]
}
gitextract_qw9xtc50/ ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .parcelrc ├── .prettierrc ├── LICENSE ├── docs/ │ └── src/ │ ├── App.js │ ├── DemoContent.js │ ├── DemoCustomTest.js │ ├── DemoFooter.js │ ├── DemoHeader.js │ ├── demo-styles.css │ ├── index.html │ └── index.js ├── lib/ │ ├── AnimatedCursor.tsx │ ├── AnimatedCursor.types.ts │ ├── helpers/ │ │ └── find.ts │ ├── hooks/ │ │ ├── useEventListener.ts │ │ └── useIsTouchdevice.ts │ └── index.ts ├── package.json ├── readme.md ├── rollup.config.mjs └── tsconfig.json
SYMBOL INDEX (14 symbols across 9 files)
FILE: docs/src/App.js
function App (line 9) | function App() {
FILE: docs/src/DemoContent.js
function Content (line 32) | function Content() {
FILE: docs/src/DemoCustomTest.js
function Content (line 12) | function Content() {
FILE: docs/src/DemoFooter.js
function DemoHeader (line 36) | function DemoHeader() {
FILE: docs/src/DemoHeader.js
function DemoHeader (line 54) | function DemoHeader() {
FILE: lib/AnimatedCursor.tsx
function CursorCore (line 40) | function CursorCore({
function AnimatedCursor (line 391) | function AnimatedCursor({
FILE: lib/AnimatedCursor.types.ts
type AnimatedCursorOptions (line 3) | interface AnimatedCursorOptions {
type Clickable (line 15) | type Clickable = string | ({ target: string } & AnimatedCursorOptions)
type AnimatedCursorProps (line 17) | interface AnimatedCursorProps extends AnimatedCursorOptions {
type AnimatedCursorCoordinates (line 23) | interface AnimatedCursorCoordinates {
FILE: lib/helpers/find.ts
function findInArray (line 1) | function findInArray<T>(
FILE: lib/hooks/useEventListener.ts
type AllEventMaps (line 3) | type AllEventMaps = HTMLElementEventMap & DocumentEventMap & WindowEventMap
function useEventListener (line 23) | function useEventListener<K extends keyof AllEventMaps>(
Condensed preview — 25 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (47K chars).
[
{
"path": ".eslintignore",
"chars": 60,
"preview": "*.json\nbuild\nconfig\ndist\n**node_modules**\n./node_modules/**\n"
},
{
"path": ".eslintrc.json",
"chars": 1301,
"preview": "{\n \"env\": {\n \"node\": true,\n \"browser\": true,\n \"es6\": true,\n \"commonjs\": true\n },\n \"plugins\": [\"react\", \"r"
},
{
"path": ".gitignore",
"chars": 80,
"preview": "node_modules\n.cache\nnpm-debug.log\n.DS_Store\n.cache\n.tmp\n*.log\n.parcel-cache\ndist"
},
{
"path": ".npmignore",
"chars": 108,
"preview": ".DS_Store\n.babelrc\n.cache\n.parcel-cache\n.tmp\n*.log\n.gitignore\nnode_modules\nnpm-debug.log\ndemo\ndocs\nlib\nsrc\n\n"
},
{
"path": ".parcelrc",
"chars": 186,
"preview": "{\n \"extends\": \"@parcel/config-default\",\n \"transformers\": {\n \"*.{js,mjs,jsx,cjs,ts,tsx}\": [\n \"@parcel/transform"
},
{
"path": ".prettierrc",
"chars": 220,
"preview": "{\n \"printWidth\": 80,\n \"trailingComma\": \"none\",\n \"singleQuote\": true,\n \"tabWidth\": 2,\n \"semi\": false,\n \"jsxSingleQu"
},
{
"path": "LICENSE",
"chars": 740,
"preview": "ISC License\n\nCopyright 2021 Stephen Scaff\n\nPermission to use, copy, modify, and/or distribute this software for any purp"
},
{
"path": "docs/src/App.js",
"chars": 3278,
"preview": "import { React, useState, useEffect } from 'react'\nimport AnimatedCursor from '../../lib'\nimport DemoContent from './Dem"
},
{
"path": "docs/src/DemoContent.js",
"chars": 3404,
"preview": "import React from 'react'\n\nconst s = {\n section: {\n paddingTop: '6em',\n width: '80%',\n maxWidth: '36em',\n m"
},
{
"path": "docs/src/DemoCustomTest.js",
"chars": 740,
"preview": "import React from 'react'\n\nconst s = {\n section: {\n paddingBottom: '6em',\n width: '80%',\n maxWidth: '36em',\n "
},
{
"path": "docs/src/DemoFooter.js",
"chars": 2688,
"preview": "import React from 'react'\n\nconst s = {\n footer: {\n position: 'relative',\n width: '100%',\n padding: '6em 0 3em'"
},
{
"path": "docs/src/DemoHeader.js",
"chars": 1815,
"preview": "import React from 'react'\n\nconst s = {\n header: {\n position: 'fixed',\n top: 0,\n left: 0,\n width: '100%',\n "
},
{
"path": "docs/src/demo-styles.css",
"chars": 653,
"preview": "/* Cursor vars if you wanna use css over css in js */\n\n:root {\n --cursor-color: #fff;\n}\nhtml,\nbody {\n background-color"
},
{
"path": "docs/src/index.html",
"chars": 698,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "docs/src/index.js",
"chars": 202,
"preview": "import React from 'react'\nimport { createRoot } from 'react-dom/client'\nimport App from './App'\n\nconst container = docum"
},
{
"path": "lib/AnimatedCursor.tsx",
"chars": 11709,
"preview": "import {\n useState,\n useEffect,\n useCallback,\n useRef,\n CSSProperties,\n useMemo\n} from 'react'\nimport { useEventLi"
},
{
"path": "lib/AnimatedCursor.types.ts",
"chars": 603,
"preview": "import { CSSProperties, ReactNode } from 'react'\n\nexport interface AnimatedCursorOptions {\n children?: ReactNode\n colo"
},
{
"path": "lib/helpers/find.ts",
"chars": 564,
"preview": "export default function findInArray<T>(\n arr: T[],\n callback: (element: T, index: number, array: T[]) => boolean,\n .."
},
{
"path": "lib/hooks/useEventListener.ts",
"chars": 1323,
"preview": "import { useEffect, useRef } from 'react'\n\ntype AllEventMaps = HTMLElementEventMap & DocumentEventMap & WindowEventMap\n\n"
},
{
"path": "lib/hooks/useIsTouchdevice.ts",
"chars": 354,
"preview": "import { useEffect, useState } from 'react'\n\nconst useIsTouchdevice = (): boolean => {\n const [isTouchdevice, setIsTouc"
},
{
"path": "lib/index.ts",
"chars": 76,
"preview": "import AnimatedCursor from './AnimatedCursor'\nexport default AnimatedCursor\n"
},
{
"path": "package.json",
"chars": 2412,
"preview": "{\n \"name\": \"react-animated-cursor\",\n \"version\": \"2.11.1\",\n \"description\": \"An animated custom cursor component in Rea"
},
{
"path": "readme.md",
"chars": 8584,
"preview": "# React Animated Cursor\n\nA React component that replaces the native cursor with a custom animated [jawn](https://www.urb"
},
{
"path": "rollup.config.mjs",
"chars": 1211,
"preview": "import commonjs from '@rollup/plugin-commonjs'\nimport external from 'rollup-plugin-peer-deps-external'\nimport resolve fr"
},
{
"path": "tsconfig.json",
"chars": 252,
"preview": "{\n \"compilerOptions\": {\n \"jsx\": \"react-jsx\",\n \"moduleResolution\": \"node\",\n \"target\": \"es5\",\n \"esModuleInter"
}
]
About this extraction
This page contains the full source code of the stephenscaff/react-animated-cursor GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 25 files (42.2 KB), approximately 12.5k tokens, and a symbol index with 14 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.