Components
2D MD Slider Input
2D multi-dimensional slider with circular range and canvas performance widget.
The 2D MD Slider Input is a geometry-focused control for exploring values in a normalised 2D space. It lets you pick an ((x, y)) point where both coordinates are in the range ([0, 1]) — left-to-right for (x), bottom-to-top for (y).
The slider uses a canvas-backed square widget for smooth rendering, with a handle that tracks the current ((x, y)) position.
0
100
100
0
X:50
Y:50
Installation (shadcn-style CLI)
These commands assume you will expose Geometa components through a shadcn-style CLI. Adjust the exact names once your CLI is wired up.
# React / Next.js
npx shadcn@latest add geometa-2d-md-slider
# Svelte
npx shadcn-svelte@latest add geometa-2d-md-sliderUsage – React (Next.js)
The example below shows the 2D slider backed by React state, exposing the current ((x, y)) value in ([0, 1] \times [0, 1]).
'use client';
import * as React from 'react';
type Vec2 = { x: number; y: number };
interface TwoDSliderProps {
radius?: number;
onChange?(value: Vec2): void;
}
export function TwoDSlider({ radius = 80, onChange }: TwoDSliderProps) {
const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
const [angle, setAngle] = React.useState(0);
const position: Vec2 = React.useMemo(
() => ({
x: Math.cos(angle) * radius,
y: Math.sin(angle) * radius,
}),
[angle, radius],
);
React.useEffect(() => {
if (onChange) onChange(position);
}, [position, onChange]);
React.useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const dpr = window.devicePixelRatio ?? 1;
const size = radius * 2 + 32;
canvas.width = size * dpr;
canvas.height = size * dpr;
canvas.style.width = `${size}px`;
canvas.style.height = `${size}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, size, size);
// circle
ctx.strokeStyle = '#888';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(size / 2, size / 2, radius, 0, Math.PI * 2);
ctx.stroke();
// handle
ctx.fillStyle = '#22c55e';
ctx.beginPath();
ctx.arc(size / 2 + position.x, size / 2 + position.y, 6, 0, Math.PI * 2);
ctx.fill();
}, [position, radius]);
const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
const rect = event.currentTarget.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const dx = event.clientX - cx;
const dy = event.clientY - cy;
const nextAngle = Math.atan2(dy, dx);
setAngle(nextAngle);
};
const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
event.currentTarget.setPointerCapture(event.pointerId);
handlePointerMove(event);
};
const handlePointerUp = (event: React.PointerEvent<HTMLDivElement>) => {
event.currentTarget.releasePointerCapture(event.pointerId);
};
return (
<div className="flex flex-col gap-3">
<div
className="relative inline-flex items-center justify-center rounded-full bg-neutral-900/40"
style={{ width: radius * 2 + 32, height: radius * 2 + 32 }}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
>
<canvas ref={canvasRef} className="pointer-events-none" />
</div>
<div className="text-xs text-muted-foreground">
Angle: <span className="font-mono">{angle.toFixed(2)} rad</span>{' '}
| x:{' '}
<span className="font-mono">
{position.x.toFixed(1)}
</span>{' '}
y:{' '}
<span className="font-mono">
{position.y.toFixed(1)}
</span>
</div>
</div>
);
}