Components
A camera-style notch ruler you drag instead of a thumb — momentum, masked edge fade, optional progress ring, and any min/max/step you need.

"use client";
import { useState } from "react";
import Component from "@/components/ui/exposure-slider";
const EXPOSURE_MIN = -100;
const EXPOSURE_MAX = 100;
const NEUTRAL = 0;
const PERCENT = 100;
const BRIGHTNESS_RANGE = 0.5;
const CONTRAST_RANGE = 0.4;
const SATURATE_RANGE = 0.6;
const CONTROLS = [
{ accent: "oklch(72% 0.17 150)", id: "exposure", label: "Exposure" },
{ accent: "oklch(70% 0.16 250)", id: "contrast", label: "Contrast" },
{ accent: "oklch(75% 0.18 60)", id: "warmth", label: "Warmth" },
];
export default function DemoOne() {
const [values, setValues] = useState<Record<string, number>>({
contrast: 12,
exposure: 34,
warmth: -18,
});
const brightness = 1 + (values.exposure / PERCENT) * BRIGHTNESS_RANGE;
const contrast = 1 + (values.contrast / PERCENT) * CONTRAST_RANGE;
const saturate = 1 + (values.warmth / PERCENT) * SATURATE_RANGE;
return (
<div className="flex min-h-[720px] w-full items-center justify-center bg-background p-8">
<div className="grid w-full max-w-5xl gap-10 lg:grid-cols-[1.1fr_1fr] lg:items-center">
<div
className="h-[460px] w-full overflow-hidden rounded-3xl bg-[radial-gradient(120%_120%_at_20%_10%,#ffd76e_0%,#ff7a59_45%,#7b2d6b_100%)]"
style={{
filter: `brightness(${brightness}) contrast(${contrast}) saturate(${saturate})`,
}}
>
<div className="flex h-full flex-col justify-between p-8">
<span className="font-mono text-white/80 text-xs uppercase tracking-[0.2em]">
DSC_4821.RAW
</span>
<div className="flex items-end justify-between gap-4">
<p className="font-semibold text-4xl text-white tracking-tight">
Golden hour
</p>
<span className="rounded-full bg-black/25 px-3 py-1 font-mono text-white text-xs tabular-nums">
EV {values.exposure > NEUTRAL ? "+" : ""}
{(values.exposure / PERCENT).toFixed(2)}
</span>
</div>
</div>
</div>
<div className="flex flex-col gap-8">
<div>
<p className="font-medium text-muted-foreground text-xs uppercase tracking-[0.18em]">
Exposure Slider
</p>
<h2 className="mt-2 font-semibold text-2xl text-foreground tracking-tight">
Drag the ticker, not a thumb
</h2>
<p className="mt-2 text-muted-foreground text-sm leading-relaxed">
A camera-style notch ruler with momentum, a masked edge fade and a
progress ring that reads the live value.
</p>
</div>
{CONTROLS.map((control) => (
<div className="flex flex-col gap-2" key={control.id}>
<div className="flex items-baseline justify-between">
<span className="font-medium text-foreground text-sm">
{control.label}
</span>
<span className="font-mono text-muted-foreground text-xs tabular-nums">
{values[control.id] > NEUTRAL ? "+" : ""}
{values[control.id]}
</span>
</div>
<Component
accentColor={control.accent}
defaultValue={values[control.id]}
max={EXPOSURE_MAX}
min={EXPOSURE_MIN}
onChange={(value: number) =>
setValues((current) => ({ ...current, [control.id]: value }))
}
showIndicator={control.id === "exposure"}
/>
</div>
))}
</div>
</div>
</div>
);
}