Fastwhisper / frontend /src /components /Pressable.tsx
Mbonea's picture
Overhaul journal UI: pending resolve sheet, gold FAB nav, and Home week score.
709db16
Raw
History Blame Contribute Delete
1.72 kB
/** Pressable with pointer-down scale feedback. */
import { animate } from "motion";
import type { ComponentChildren, JSX } from "preact";
import { useRef } from "preact/hooks";
import { prefersReducedMotion, springs } from "../motion/springs";
type PressableProps = {
children: ComponentChildren;
className?: string;
disabled?: boolean;
onClick?: JSX.MouseEventHandler<HTMLButtonElement>;
type?: "button" | "submit";
ariaLabel?: string;
ariaPressed?: boolean;
};
export function Pressable({
children,
className = "",
disabled = false,
onClick,
type = "button",
ariaLabel,
ariaPressed,
}: PressableProps) {
const ref = useRef<HTMLButtonElement>(null);
const active = useRef(false);
const setScale = (value: number) => {
const el = ref.current;
if (!el || disabled) return;
if (prefersReducedMotion()) {
el.style.transform = "none";
el.style.opacity = value < 1 ? "0.85" : "1";
return;
}
animate(el, { scale: value }, springs.press);
};
const onPointerDown = (event: PointerEvent) => {
if (disabled || event.button !== 0) return;
active.current = true;
ref.current?.setPointerCapture(event.pointerId);
setScale(0.98);
};
const endPress = () => {
if (!active.current) return;
active.current = false;
setScale(1);
};
return (
<button
ref={ref}
type={type}
disabled={disabled}
className={`pressable ${className}`.trim()}
aria-label={ariaLabel}
aria-pressed={ariaPressed}
onPointerDown={onPointerDown}
onPointerUp={endPress}
onPointerCancel={endPress}
onPointerLeave={endPress}
onClick={onClick}
>
{children}
</button>
);
}