ui

Flow Progress

Flow progress primitives.

Installation

pnpm dlx shadcn@latest add https://registry.circle.health/r/flow-progress.json

Registry 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