"use client" import { ArrowUpIcon, MicIcon, PlusIcon, MousePointer2, LoaderIcon, CheckIcon, XIcon, ChevronRightIcon, } from "lucide-react" import { NavMenu } from "./nav-menu" import { useOnboarding } from "./onboarding-context" import { motion, AnimatePresence, type ResolvedValues } from "motion/react" import { useEffect, useMemo, useRef, useState, useLayoutEffect } from "react" import React from "react" import { cn } from "@lib/utils" import { Button } from "@ui/components/button" type CursorAction = | { type: "startAt"; target: React.RefObject } | { type: "startAtPercent"; xPercent: number; yPercent: number } | { type: "move" target: React.RefObject duration: number } | { type: "move"; xPercent: number; yPercent: number; duration: number } | { type: "pause"; duration: number } | { type: "click" } | { type: "call"; fn: () => void } interface CursorProps { actions: CursorAction[] className?: string onPositionChange?: (clientX: number, clientY: number) => void } function useContainerRect(ref: React.RefObject) { const rectRef = React.useRef(null) useLayoutEffect( function setup() { if (!ref.current) return function measure() { if (ref.current) { rectRef.current = ref.current.getBoundingClientRect() } } measure() let resizeObserver: ResizeObserver | null = null if (typeof ResizeObserver !== "undefined") { resizeObserver = new ResizeObserver(function onResize() { measure() }) if (ref.current) { resizeObserver.observe(ref.current) } } function onScroll() { measure() } window.addEventListener("resize", measure) window.addEventListener("scroll", onScroll, true) return function cleanup() { if (resizeObserver) { resizeObserver.disconnect() } window.removeEventListener("resize", measure) window.removeEventListener("scroll", onScroll, true) } }, [ref], ) return rectRef } function Cursor({ actions, className, onPositionChange }: CursorProps) { const [position, setPosition] = useState({ x: "0px", y: "0px" }) const [scale, setScale] = useState(1) const [currentMoveDuration, setCurrentMoveDuration] = useState(0.7) // Default move duration const containerRef = useRef(null) const timeoutsRef = useRef([]) const containerRectRef = useContainerRect(containerRef) const lastUpdateRef = useRef(0) function moveToElement( elementRef: React.RefObject, duration: number, ) { if (!containerRef.current || !elementRef.current) return setCurrentMoveDuration(duration / 1000) // Convert to seconds for Framer Motion const containerRect = containerRef.current.getBoundingClientRect() const elementRect = elementRef.current.getBoundingClientRect() // Position the TOP-LEFT of the cursor at the center of the target element const x = elementRect.left - containerRect.left + elementRect.width / 2 const y = elementRect.top - containerRect.top + elementRect.height / 2 setPosition({ x: `${x}px`, y: `${y}px` }) } function setPositionByPercent(xPercent: number, yPercent: number) { if (!containerRef.current) return const containerRect = containerRef.current.getBoundingClientRect() // Percentages indicate where the TOP-LEFT of the cursor should be placed const x = (containerRect.width * xPercent) / 100 const y = (containerRect.height * yPercent) / 100 setCurrentMoveDuration(0) // snap without animating setPosition({ x: `${x}px`, y: `${y}px` }) } function moveToPercent(xPercent: number, yPercent: number, duration: number) { if (!containerRef.current) return setCurrentMoveDuration(duration / 1000) const containerRect = containerRef.current.getBoundingClientRect() const x = (containerRect.width * xPercent) / 100 const y = (containerRect.height * yPercent) / 100 setPosition({ x: `${x}px`, y: `${y}px` }) } useEffect(() => { // Clear any existing timeouts before scheduling new ones timeoutsRef.current.forEach((id) => { clearTimeout(id) }) timeoutsRef.current = [] let timeAccumulator = 0 function schedule(callback: () => void, delay: number): number { const id = window.setTimeout(callback, delay) timeoutsRef.current.push(id) return id } function executeActions(): void { actions.forEach((action) => { // startAt should apply immediately at its place in the sequence and not advance time if (action.type === "startAt") { moveToElement(action.target, 0) return } if (action.type === "startAtPercent") { setPositionByPercent(action.xPercent, action.yPercent) return } schedule(() => { switch (action.type) { case "move": if ("target" in action) { moveToElement(action.target, action.duration) } else { moveToPercent(action.xPercent, action.yPercent, action.duration) } break case "click": setScale(0.9) schedule(function resetClickScale() { setScale(1) }, 100) // Fixed 100ms click duration break case "call": try { action.fn() } catch (_) { // no-op on errors to avoid breaking demo } break case "pause": // Pause doesn't require any action, just time passing break } }, timeAccumulator) // Add this action's duration to the accumulator for the next action if (action.type === "click") { timeAccumulator += 100 } else if (action.type === "pause") { timeAccumulator += action.duration } else if (action.type === "move") { timeAccumulator += action.duration } else { // 'call' and startAt/startAtPercent don't consume time } }) } // make sure refs are ready schedule(executeActions, 100) return function cleanup(): void { timeoutsRef.current.forEach((id) => { clearTimeout(id) }) timeoutsRef.current = [] } }, [actions]) return (
{ if (!onPositionChange) return const containerRect = containerRectRef.current if (!containerRect) return const now = performance.now() if (now - lastUpdateRef.current < 50) return // ~20fps throttle lastUpdateRef.current = now const latestX = typeof latest.x === "number" ? latest.x : Number.parseFloat(String(latest.x || 0)) const latestY = typeof latest.y === "number" ? latest.y : Number.parseFloat(String(latest.y || 0)) const clientX = containerRect.left + latestX const clientY = containerRect.top + latestY onPositionChange(clientX, clientY) }} >
) } function SnippetDemo() { const snippetRootRef = useRef(null) const sentenceRef = useRef(null) const [currentEndIndex, setCurrentEndIndex] = useState(0) const lastStableIndexRef = useRef(0) const [cursorActions, setCursorActions] = useState([]) const [menuOpen, setMenuOpen] = useState(false) const [hoveredMenuIndex, setHoveredMenuIndex] = useState(null) const menuItemRefs = useRef<(HTMLDivElement | null)[]>([]) const menuItem6Ref = useRef(null) const charRectsRef = useRef([]) const targetText = 'There\'s an Italian dish called saltimbocca, which means "leap into the mouth."' function getIndexFromClientPoint(clientX: number, clientY: number): number { const rects = charRectsRef.current if (!sentenceRef.current || rects.length === 0) return 0 let bestIdx = 0 let bestDist = Number.POSITIVE_INFINITY for (let i = 0; i < rects.length; i++) { const r = rects[i] if (!r) continue const cx = r.left + r.width / 2 const cy = r.top + r.height / 2 const dx = clientX - cx const dy = clientY - cy const d = dx * dx + dy * dy if (d < bestDist) { bestDist = d bestIdx = i } } return bestIdx } function getMenuItemIndexFromPoint( clientX: number, clientY: number, ): number | null { const el = document.elementFromPoint(clientX, clientY) as HTMLElement | null if (!el) return null const menuItem = el.closest("[data-menu-idx]") as HTMLElement | null if (menuItem) { const idx = Number.parseInt(menuItem.dataset.menuIdx || "", 10) return Number.isFinite(idx) ? idx : null } return null } useLayoutEffect( function setupCharRectsMeasurement() { function measureCharRects(): void { if (!sentenceRef.current) { charRectsRef.current = [] return } const spans = sentenceRef.current.querySelectorAll("span[data-idx]") const rects: DOMRect[] = [] spans.forEach(function collect(node) { rects.push((node as HTMLElement).getBoundingClientRect()) }) charRectsRef.current = rects } measureCharRects() let ro1: ResizeObserver | null = null let ro2: ResizeObserver | null = null if (typeof ResizeObserver !== "undefined") { ro1 = new ResizeObserver(function onResize() { measureCharRects() }) ro2 = new ResizeObserver(function onResize() { measureCharRects() }) if (snippetRootRef.current) ro1.observe(snippetRootRef.current) if (sentenceRef.current) ro2.observe(sentenceRef.current) } function onScroll(): void { measureCharRects() } window.addEventListener("resize", measureCharRects) window.addEventListener("scroll", onScroll, true) return function cleanup(): void { if (ro1) ro1.disconnect() if (ro2) ro2.disconnect() window.removeEventListener("resize", measureCharRects) window.removeEventListener("scroll", onScroll, true) } }, [targetText], ) useEffect(function setupActionsOnce() { lastStableIndexRef.current = 0 setCurrentEndIndex(0) if (!sentenceRef.current) return const total = targetText.length const firstSpan = sentenceRef.current.querySelector( 'span[data-idx="0"]', ) as HTMLSpanElement | null const lastSpan = sentenceRef.current.querySelector( `span[data-idx="${Math.max(0, total - 1)}"]`, ) as HTMLSpanElement | null if (!firstSpan || !lastSpan) return const firstRef = { current: firstSpan } as React.RefObject const lastRef = { current: lastSpan } as React.RefObject setCursorActions([ { type: "call", fn: function reset() { lastStableIndexRef.current = 0 setCurrentEndIndex(0) setHoveredMenuIndex(null) }, }, { type: "startAt", target: firstRef }, { type: "pause", duration: 200 }, { type: "move", target: lastRef, duration: 1800 }, { type: "pause", duration: 1200 }, { type: "click" }, { type: "call", fn: () => { setMenuOpen(true) }, }, { type: "pause", duration: 1000 }, { type: "move", target: menuItem6Ref, duration: 1000 }, { type: "pause", duration: 500 }, { type: "click" }, ]) }, []) return (
writing is easier to read, and the easier something is to read, the more deeply readers will engage with it. The less energy they expend on your prose, the more they'll have left for your ideas. And the further they'll read. Most readers' energy tends to flag part way through an article or essay. If the friction of reading is low enough, more keep going till the end.{" "} {targetText.split("").map(function renderChar(ch, idx) { const highlighted = idx <= currentEndIndex return ( {ch} ) })} {menuOpen && (
{[ "Back", "Forward", "Reload", "Save As...", "Print...", "Translate to English", "Save to Supermemory", "View Page Source", "Inspect", ].map((item, idx) => (
{ menuItemRefs.current[idx] = el if (idx === 6) { menuItem6Ref.current = el } }} data-menu-idx={idx} className={cn( "px-2 py-0.5 flex items-center gap-1.5 rounded-sm transition-colors", (idx === 0 || idx === 1) && "text-white/30", hoveredMenuIndex === idx && "bg-blue-500", )} > {idx === 6 && ( Supermemory )} {item}
{[2, 5, 6].includes(idx) && (
)} ))}
)} {" "} My goal when writing might be called saltintesta: the ideas leap into your head and you barely notice the words that got them there. It's too much to hope that writing could ever be pure ideas. You might not even want it to be. But for most writers, most of the time, that's the goal to aim for. The gap between most writing and pure ideas is not filled with poetry. Plus it's more considerate to write simply. When you write in a fancy way to impress people, you're making them do extra work just so you can seem cool. It's like trailing a long train behind you that readers have to carry.
) } function ChatGPTDemo() { const iconRef = useRef(null) const submitButtonRef = useRef(null) const [enhancementStatus, setEnhancementStatus] = useState< "notStarted" | "enhancing" | "done" >("notStarted") const [memoriesExpanded, setMemoriesExpanded] = useState(false) const cursorActions: CursorAction[] = useMemo( () => [ { type: "call", fn: function resetStates() { setEnhancementStatus("notStarted") setMemoriesExpanded(false) }, }, { type: "startAtPercent", xPercent: 80, yPercent: 80 }, { type: "pause", duration: 1000 }, { type: "move", target: iconRef, duration: 1000 }, { type: "pause", duration: 1000 }, { type: "click" }, { type: "call", fn: function startEnhancing() { setEnhancementStatus("enhancing") }, }, { type: "pause", duration: 1000 }, { type: "move", xPercent: 10, yPercent: 80, duration: 1000 }, { type: "call", fn: function finishEnhancing() { setEnhancementStatus("done") }, }, { type: "pause", duration: 500 }, { type: "move", target: iconRef, duration: 1000 }, { type: "call", fn: function expandMemories() { setMemoriesExpanded(true) }, }, { type: "pause", duration: 1000 }, { type: "move", xPercent: 80, yPercent: 80, duration: 1000 }, ], [], ) return (
What's on your mind today?
what are my card's benefits?
{enhancementStatus === "notStarted" && ( Enhance with Supermemory )} {enhancementStatus === "enhancing" && ( <> Searching... )} {enhancementStatus === "done" && ( <> Including 1 memory )} {memoriesExpanded && (
Enhance with Supermemory User possesses an American Express Platinum card
)}
) } function TwitterDemo() { const importButtonRef = useRef(null) const [importStatus, setImportStatus] = useState< "notStarted" | "importing" | "done" >("notStarted") const cursorActions: CursorAction[] = useMemo( () => [ { type: "call", fn: function resetStates() { setImportStatus("notStarted") }, }, { type: "startAtPercent", xPercent: 10, yPercent: 80 }, { type: "pause", duration: 1300 }, { type: "move", target: importButtonRef, duration: 1100 }, { type: "pause", duration: 800 }, { type: "click" }, { type: "call", fn: function startImporting() { setImportStatus("importing") }, }, { type: "pause", duration: 700 }, { type: "move", xPercent: 80, yPercent: 80, duration: 1200 }, { type: "call", fn: function finishImporting() { setImportStatus("done") }, }, ], [], ) return (
𝕏 Import Twitter Bookmarks

This will import all your Twitter bookmarks to Supermemory

{importStatus === "importing" && ( )} {importStatus === "done" && ( )} {importStatus === "importing" ? "Importing bookmarks..." : importStatus === "done" ? "Import successful" : "Import All Bookmarks"}
) } export function ExtensionForm() { const { totalSteps, nextStep, getStepNumberFor } = useOnboarding() return (

Step {getStepNumberFor("extension")} of {totalSteps}

Install the Chrome extension

Bring Supermemory everywhere

Remember anything, anywhere

Just right-click to save instantly.

Integrate with your AI

{/* Supercharge your AI with memory */} {/* Supercharge any AI with Supermemory. */} {/* ChatGPT is better with Supermemory. */} {/* Seamless integration with your workflow */}

{/* Integrates with ChatGPT and Claude. */} {/* Integrates with your chat apps */} Enhance any prompt with Supermemory. {/* Seamlessly */}

Import Twitter bookmarks

Search semantically and effortlessly. {/* Import instantly and search effortlessly. */}

) }