ui
Flow Progress
Flow progress primitives.
Installation
pnpm dlx shadcn@latest add https://registry.circle.health/r/flow-progress.jsonRegistry dependencies
Component
import * as React from "react";
import { cn } from "@/registry/berlin/lib/utils";
import { Button, type ButtonProps } from "@/registry/berlin/ui/button";
import { ProgressBar } from "@/registry/berlin/ui/progress-bar";
function CaretLeftIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="m10 3-5 5 5 5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function CloseIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="m4 4 8 8m0-8-8 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
}
function CheckIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="m3.5 8 2.5 2.5L12.5 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
export function FlowWrapper({ children }: { children: React.ReactNode }) {
return <div className="w-full">{children}</div>;
}
export function FlowContainer({ children }: { children: React.ReactNode }) {
return <div className="mx-auto flex w-full max-w-[480px] items-center">{children}</div>;
}
export function ResponsiveButtonContainer({ children }: { children: React.ReactNode }) {
return (
<div className="flex w-full flex-col gap-2 bg-[linear-gradient(180deg,rgba(250,248,245,0)_0%,#FAF8F5_25.72%)] px-6 pb-8 pt-16 sm:pb-32">
{children}
</div>
);
}
export function FlowActionButton({ className, ...props }: ButtonProps) {
return <Button block className={cn("bg-primary text-primary-foreground", className)} {...props} />;
}
export function FlowActions({ children }: { children: React.ReactNode }) {
return (
<FlowWrapper>
<FlowContainer>
<ResponsiveButtonContainer>{children}</ResponsiveButtonContainer>
</FlowContainer>
</FlowWrapper>
);
}
export function FlowActionContinue(props: ButtonProps) {
return (
<FlowActions>
<FlowActionButton {...props}>Continue</FlowActionButton>
</FlowActions>
);
}
export function FlowProgress({ progress }: { progress: number }) {
return (
<div className="w-full px-4">
<ProgressBar progress={progress} />
</div>
);
}
export function FlowHeadButton(props: ButtonProps) {
return (
<Button
aria-label={props["aria-label"] ?? "Flow control"}
size="small"
theme="dark"
variant="secondary"
className={cn("min-h-11 min-w-11 rounded-full p-3", props.className)}
{...props}
/>
);
}
export interface FlowHeadProps {
progress: number;
children?: React.ReactNode;
onCloseClick: () => void;
onBackClick: () => void;
}
export function FlowHead({ progress, children, onCloseClick, onBackClick }: FlowHeadProps) {
return (
<FlowWrapper>
<FlowContainer>
<FlowHeadButton leftIcon={CaretLeftIcon} onClick={onBackClick} />
<FlowProgress progress={progress} />
{children ?? <FlowHeadButton leftIcon={CloseIcon} onClick={onCloseClick} />}
</FlowContainer>
</FlowWrapper>
);
}
export interface OptionButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "type" | "onClick" | "onSelect"> {
className?: string;
optionType?: "option" | "singleOption";
selected?: boolean;
onSelect?: (selected: boolean) => void;
onSelectAndContinue?: () => void;
value?: string | number;
label?: string;
subline?: string;
info?: string;
children?: React.ReactNode;
disabled?: boolean;
}
export function OptionButton({
className,
optionType = "option",
selected = false,
onSelect,
onSelectAndContinue,
value,
label,
subline,
info,
children,
disabled = false,
...props
}: OptionButtonProps) {
function handleClick() {
if (disabled) return;
if (optionType === "singleOption") {
if (!selected) {
onSelect?.(true);
onSelectAndContinue?.();
}
return;
}
onSelect?.(!selected);
}
return (
<button
type="button"
data-selected={selected}
data-value={value}
aria-pressed={selected}
onClick={handleClick}
disabled={disabled}
className={cn(
"w-full rounded-xl border-none bg-card px-4 py-4 text-left text-foreground shadow-[0_0_0_0.5px_rgba(0,0,0,0.1),0_4px_12px_rgba(0,0,0,0.02),0_1px_4px_rgba(0,0,0,0.04)] transition-[box-shadow,background-color]",
"hover:bg-black/[0.02] focus-visible:outline-none focus-visible:shadow-[0_0_0_2px_var(--Blue),0_4px_12px_rgba(0,0,0,0.02),0_1px_4px_rgba(0,0,0,0.04)]",
selected && "shadow-[0_0_0_2px_var(--Blue),0_4px_12px_rgba(0,0,0,0.02),0_1px_4px_rgba(0,0,0,0.04)]",
disabled && "cursor-not-allowed opacity-50",
className
)}
{...props}
>
{optionType === "option" ? (
<div className="flex items-center">
<span
className={cn(
"mr-[18px] flex size-5 shrink-0 items-center justify-center rounded-md border border-black/20 text-white transition-colors",
selected && "border-[var(--Blue)] bg-[var(--Blue)]"
)}
>
{selected ? <CheckIcon className="size-3" /> : null}
</span>
<span className="text-base font-medium leading-6 tracking-[-0.02em]">{label ?? children}</span>
</div>
) : label || subline || info ? (
<div className="flex items-center justify-between gap-6">
<div className="flex flex-1 flex-col items-start">
{label ? <span className="text-base font-medium leading-6 tracking-[-0.02em]">{label}</span> : null}
{subline ? <span className="text-sm leading-6 text-muted-foreground">{subline}</span> : null}
</div>
{info ? <span className="shrink-0 text-sm leading-6 text-muted-foreground">{info}</span> : null}
</div>
) : (
children
)}
</button>
);
}
export function FlowOptionContainer({ children }: { children: React.ReactNode }) {
return <div className="flex w-full flex-col gap-5 px-6 py-6">{children}</div>;
}
export function FlowOptionButton({ className, ...props }: OptionButtonProps) {
return <OptionButton className={cn("w-full", className)} {...props} />;
}
export function FlowOptions({ children }: { children: React.ReactNode }) {
return (
<FlowWrapper>
<FlowContainer>
<FlowOptionContainer>{children}</FlowOptionContainer>
</FlowContainer>
</FlowWrapper>
);
}
Raw registry JSON/r/flow-progress.json