diff --git a/www/src/modules/docs/components-list/autoplay/card-hover.tsx b/www/src/modules/docs/components-list/autoplay/card-hover.tsx deleted file mode 100644 index bc0550c25..000000000 --- a/www/src/modules/docs/components-list/autoplay/card-hover.tsx +++ /dev/null @@ -1,22 +0,0 @@ -"use client" - -import { createContext, useContext } from "react" - -/** - * Whether the surrounding component card is "active" — hovered or keyboard - * focused. The card (component-card.tsx) is the only pointer/focus target: the - * demo it previews is `inert`, so it can never receive hover/focus itself. The - * card tracks its own hover/focus and broadcasts it down here, and the demo's - * autoplay hooks read it to drive their looping interaction animations. - * - * Nothing here ever touches real focus — the animations are purely visual state - * (controlled props, faux focus rings, motion transforms), so simulating a - * "focused" field or an "open" popover never steals the page's focus. - */ -const CardHoverContext = createContext(false) - -export const CardHoverProvider = CardHoverContext.Provider - -export function useCardHover(): boolean { - return useContext(CardHoverContext) -} diff --git a/www/src/modules/docs/components-list/autoplay/demo-cursor.tsx b/www/src/modules/docs/components-list/autoplay/demo-cursor.tsx deleted file mode 100644 index 7f89c5bad..000000000 --- a/www/src/modules/docs/components-list/autoplay/demo-cursor.tsx +++ /dev/null @@ -1,156 +0,0 @@ -"use client" - -import { useEffect, useState } from "react" -import { useReducedMotion } from "motion/react" - -import { Pointer } from "./pointer" - -/** - * A decorative macOS pointer that follows whatever control the card's autoplay is - * currently "clicking" and dips on each click. Where the overlay cursor runs a - * fixed choreography, this one reads the DOM: the demos already mirror real RAC - * state onto the real elements (see interaction.tsx), so we just find the pressed - * / selected / current element and glide the tip onto it. - * - * Mounted in the (unscaled) preview and measured against it, so the card's inner - * `scale()` is absorbed by getBoundingClientRect. Purely cosmetic — aria-hidden, - * pointer-events-none, and it never touches the real cursor. - */ - -// The arrow tip's offset into the SVG at the default size — subtract it so the -// tip, not the SVG's corner, lands on the target point. -const TIP_X = 4 -const TIP_Y = 3 -// How long a click reads as "pressed" — long enough to cover the ripple and give -// every click (even the briefest toggle beat) a deliberate dip. -const PRESS_MS = 240 - -interface Point { - x: number - y: number -} - -function firstMatch(root: HTMLElement, selector: string): HTMLElement | null { - return root.querySelector(selector) -} - -export function DemoCursor({ - containerRef, - active, -}: { - containerRef: React.RefObject - active: boolean -}) { - const reduce = useReducedMotion() ?? false - const [point, setPoint] = useState(null) - const [pressed, setPressed] = useState(false) - const [shown, setShown] = useState(false) - // Bumping this remounts the ripple so it replays on every click. - const [clicks, setClicks] = useState(0) - - useEffect(() => { - if (!active || reduce) { - setShown(false) - return - } - - // The element pressed on the previous frame (to detect a fresh press) and the - // one the pointer currently rests on (to detect a selection jumping). - let lastPressEl: Element | null = null - let restTarget: Element | null = null - let pressUntil = 0 - let placed = false - let raf = 0 - - const tick = (now: number) => { - const container = containerRef.current - if (!container) { - raf = requestAnimationFrame(tick) - return - } - - const pressedEl = firstMatch(container, "[data-pressed]") - const selectedEl = - firstMatch(container, "[data-selected]:not([data-disabled])") ?? - firstMatch(container, '[aria-current="page"]') - const hoveredEl = firstMatch(container, "[data-hovered]") - - // Where the pointer wants to be. A live press wins; otherwise a moving - // selection; otherwise a plain hover. A selection that merely *contains* the - // last press target is a fixed control (a checkbox toggling its own box) — - // hold position there instead of jumping out to the row. - let target: HTMLElement | null = null - if (pressedEl) { - target = pressedEl - } else if ( - selectedEl && - !(lastPressEl && selectedEl.contains(lastPressEl)) - ) { - target = selectedEl - } else if (hoveredEl && !selectedEl) { - target = hoveredEl - } - - const pressRising = pressedEl != null && lastPressEl == null - const targetJumped = - target != null && restTarget != null && target !== restTarget - if (pressRising || targetJumped) { - pressUntil = now + PRESS_MS - setClicks((c) => c + 1) - } - lastPressEl = pressedEl - if (target) restTarget = target - - if (target) { - const c = container.getBoundingClientRect() - const t = target.getBoundingClientRect() - const x = t.left - c.left + t.width / 2 - const y = t.top - c.top + t.height / 2 - setPoint((prev) => - prev && Math.abs(prev.x - x) < 0.5 && Math.abs(prev.y - y) < 0.5 - ? prev - : { x, y }, - ) - // Fade in a frame after the first placement so the pointer eases in at the - // target rather than appearing mid-air. - if (placed) setShown(true) - placed = true - } - - const isPressed = now < pressUntil - setPressed((prev) => (prev === isPressed ? prev : isPressed)) - - raf = requestAnimationFrame(tick) - } - - raf = requestAnimationFrame(tick) - return () => cancelAnimationFrame(raf) - }, [active, reduce, containerRef]) - - if (!point) return null - - return ( - - ) -} diff --git a/www/src/modules/docs/components-list/autoplay/index.ts b/www/src/modules/docs/components-list/autoplay/index.ts deleted file mode 100644 index f2536ee9e..000000000 --- a/www/src/modules/docs/components-list/autoplay/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -export { CardHoverProvider, useCardHover } from "./card-hover" -export { - useAutoplay, - useTypewriter, - useToggleAutoplay, - useStepAutoplay, - useCycle, - useOpenAutoplay, - useValueAutoplay, -} from "./use-autoplay" -export type { - AutoplayPhase, - AutoplayState, - TypewriterState, - ToggleAutoplayState, - StepAutoplayState, - ValueAutoplayState, - ScenePhase, -} from "./use-autoplay" -export { DemoCursor } from "./demo-cursor" -export { Pointer } from "./pointer" -export { - DemoState, - DemoPress, - DemoFocus, - DemoCaret, - applyDemoState, -} from "./interaction" -export type { DemoStateFlags } from "./interaction" -export { OverlayScene } from "./overlay-scene" -export type { SurfaceVariant } from "./overlay-scene" diff --git a/www/src/modules/docs/components-list/autoplay/interaction.tsx b/www/src/modules/docs/components-list/autoplay/interaction.tsx deleted file mode 100644 index c06360e35..000000000 --- a/www/src/modules/docs/components-list/autoplay/interaction.tsx +++ /dev/null @@ -1,171 +0,0 @@ -"use client" - -import { type ReactNode, useEffect, useLayoutEffect, useRef } from "react" - -import { cn } from "@/registry/lib/utils" - -/** - * Exact-fidelity state simulation for the preview cards. - * - * Instead of faking interaction with our own transforms, we mirror the desired - * state onto the REAL React Aria state attributes (`data-hovered`, `data-pressed`, - * `data-focused`, …) of the real component. The component then renders its OWN - * state styles — the exact look it will have under any design-system preset — - * with no interaction and without ever touching the page's real focus. - * - * The attributes are (re)applied in a layout effect after every render because - * React Aria rewrites them from its internal state (always idle here) on each - * render; re-applying post-commit, pre-paint, keeps them set with no flicker. - */ - -// useLayoutEffect on the client, no-op useEffect on the server (avoids the SSR warning). -const useIsoLayoutEffect = - typeof window === "undefined" ? useEffect : useLayoutEffect - -export interface DemoStateFlags { - hovered?: boolean - pressed?: boolean - /** Adds `data-focused` — drives `focus:` (input rings, etc.). */ - focused?: boolean - /** Adds `data-focused` + `data-focus-visible` — drives `focus-visible:` rings. */ - focusVisible?: boolean - selected?: boolean - open?: boolean - current?: boolean -} - -const FLAG_ATTRS: [keyof DemoStateFlags, string][] = [ - ["hovered", "data-hovered"], - ["pressed", "data-pressed"], - ["selected", "data-selected"], - ["open", "data-open"], - ["current", "data-current"], -] - -export function applyDemoState(el: HTMLElement, flags: DemoStateFlags): void { - for (const [key, attr] of FLAG_ATTRS) { - if (flags[key]) el.setAttribute(attr, "") - else el.removeAttribute(attr) - } - const focused = flags.focused || flags.focusVisible - if (focused) el.setAttribute("data-focused", "") - else el.removeAttribute("data-focused") - if (flags.focusVisible) el.setAttribute("data-focus-visible", "") - else el.removeAttribute("data-focus-visible") -} - -/** - * Wraps a single child in a `display:contents` span (so it adds no box) and - * mirrors `flags` onto the child element's real state attributes. `target` - * selects which descendant to drive when the styled element isn't the wrapper's - * first child. - */ -export function DemoState({ - flags, - target = "first", - children, -}: { - flags: DemoStateFlags - target?: "first" | "last" | "all" - children: ReactNode -}) { - const ref = useRef(null) - - useIsoLayoutEffect(() => { - const host = ref.current - if (!host) return - const targets = - target === "all" - ? Array.from(host.children) - : target === "last" - ? [host.lastElementChild] - : [host.firstElementChild] - for (const el of targets) { - if (el instanceof HTMLElement) applyDemoState(el, flags) - } - }) - - return ( - - {children} - - ) -} - -/** - * Drives the real hover / pressed states of a pressable (Button, Link, - * ToggleButton, menu item…). Accepts either an explicit `phase` - * (`idle | hover | press | …`) or the `hovering` / `pressing` booleans. - */ -export function DemoPress({ - phase, - pressing, - hovering, - selected, - target, - children, -}: { - phase?: string - pressing?: boolean - hovering?: boolean - /** Mirror a selected/toggled state — needed when the pressable IS the styled - * element (e.g. ToggleButton), where the real `data-selected` would otherwise - * be stripped on re-apply. */ - selected?: boolean - target?: "first" | "last" | "all" - children: ReactNode -}) { - const isPressed = pressing || phase === "press" - const isHovered = isPressed || hovering || phase === "hover" - return ( - - {children} - - ) -} - -/** - * Drives the real focus state of a field so it shows its actual focus ring/border - * — without taking real focus (the preview is `inert`; we only set the attribute). - */ -export function DemoFocus({ - active, - target, - children, -}: { - active: boolean - target?: "first" | "last" | "all" - children: ReactNode -}) { - return ( - - {children} - - ) -} - -/** - * A blinking text caret, for fields where the typed text sits in normal flow - * (search, command palette) rather than inside a real ``. - */ -export function DemoCaret({ - visible = true, - className, -}: { - visible?: boolean - className?: string -}) { - if (!visible) return null - return ( -