{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "approval-card",
  "title": "Approval Card",
  "description": "A composable decision surface that keeps risk, evidence, and impact explicit.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "input",
    "label",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/approval-card.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n  Check,\n  CircleAlert,\n  LoaderCircle,\n  OctagonAlert,\n  ShieldCheck,\n  TriangleAlert,\n  X,\n} from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  type ReactNode,\n  useContext,\n  useEffect,\n  useId,\n  useState,\n} from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const APPROVAL_CARD_RISKS = [\"low\", \"medium\", \"high\", \"critical\"] as const;\nexport const APPROVAL_CARD_VARIANTS = [\"compact\", \"detailed\"] as const;\nexport const APPROVAL_CARD_STATUSES = [\n  \"ready\",\n  \"submitting\",\n  \"approved\",\n  \"rejected\",\n  \"error\",\n] as const;\n\nexport type ApprovalRisk = (typeof APPROVAL_CARD_RISKS)[number];\nexport type ApprovalCardVariant = (typeof APPROVAL_CARD_VARIANTS)[number];\nexport type ApprovalCardStatus = (typeof APPROVAL_CARD_STATUSES)[number];\nexport type ApprovalDecision = \"approved\" | \"rejected\";\n\nexport type ApprovalConfirmation = {\n  phrase: string;\n  label?: ReactNode;\n  placeholder?: string;\n};\n\ntype ApprovalRiskProps =\n  | { risk?: Exclude<ApprovalRisk, \"critical\">; confirmation?: never }\n  | { risk: \"critical\"; confirmation: ApprovalConfirmation };\n\nexport type ApprovalCardState =\n  | { status?: \"ready\"; pendingDecision?: never }\n  | { status: \"submitting\"; pendingDecision: ApprovalDecision }\n  | { status: \"approved\" | \"rejected\" | \"error\"; pendingDecision?: never };\n\nexport type ApprovalCardProps = ComponentProps<\"section\"> &\n  ApprovalRiskProps &\n  ApprovalCardState & {\n    variant?: ApprovalCardVariant;\n    disabled?: boolean;\n  };\n\nconst riskConfig = {\n  low: {\n    label: \"Low risk\",\n    icon: ShieldCheck,\n    iconClassName: \"bg-success/14 text-success-ink\",\n  },\n  medium: {\n    label: \"Medium risk\",\n    icon: CircleAlert,\n    iconClassName: \"bg-warning/14 text-warning-ink\",\n  },\n  high: {\n    label: \"High risk\",\n    icon: TriangleAlert,\n    iconClassName: \"bg-destructive/14 text-destructive-ink\",\n  },\n  critical: {\n    label: \"Critical risk\",\n    icon: OctagonAlert,\n    iconClassName:\n      \"bg-destructive/14 text-destructive-ink shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--destructive)_28%,transparent)]\",\n  },\n} satisfies Record<\n  ApprovalRisk,\n  { label: string; icon: typeof ShieldCheck; iconClassName: string }\n>;\n\nconst approvalCardVariants = cva(\n  \"rounded-[14px] border bg-card text-card-foreground transition-[border-color,opacity] duration-150 motion-reduce:transition-none\",\n  {\n    variants: {\n      variant: { compact: \"p-3.5\", detailed: \"p-4\" },\n    },\n  },\n);\n\ntype ApprovalCardContextValue = {\n  risk: ApprovalRisk;\n  variant: ApprovalCardVariant;\n  status: ApprovalCardStatus;\n  pendingDecision?: ApprovalDecision;\n  confirmation?: ApprovalConfirmation;\n  confirmationValue: string;\n  setConfirmationValue: (value: string) => void;\n  confirmationMatches: boolean;\n  disabled: boolean;\n  isSubmitting: boolean;\n  isTerminal: boolean;\n  isActionable: boolean;\n  titleId: string;\n  confirmationId: string;\n};\n\nconst ApprovalCardContext = createContext<ApprovalCardContextValue | null>(null);\n\nfunction useApprovalCard(name: string) {\n  const context = useContext(ApprovalCardContext);\n  if (!context) throw new Error(`${name} must be used within ApprovalCard`);\n  return context;\n}\n\nexport function ApprovalCard(props: ApprovalCardProps) {\n  const {\n    variant = \"compact\",\n    risk = \"medium\",\n    confirmation,\n    status = \"ready\",\n    pendingDecision,\n    disabled = false,\n    children,\n    className,\n    \"aria-labelledby\": ariaLabelledby,\n    ...sectionProps\n  } = props;\n  const titleId = useId();\n  const confirmationId = useId();\n  const [confirmationValue, setConfirmationValue] = useState(\"\");\n  const isSubmitting = status === \"submitting\";\n  const isTerminal = status === \"approved\" || status === \"rejected\";\n  const isActionable = status === \"ready\" || status === \"error\";\n  const confirmationMatches =\n    risk !== \"critical\" ||\n    (Boolean(confirmation?.phrase) && confirmationValue === confirmation?.phrase);\n\n  useEffect(() => {\n    setConfirmationValue((current) => (current === confirmation?.phrase ? current : \"\"));\n  }, [confirmation?.phrase]);\n\n  const context: ApprovalCardContextValue = {\n    risk,\n    variant,\n    status,\n    pendingDecision,\n    confirmation,\n    confirmationValue,\n    setConfirmationValue,\n    confirmationMatches,\n    disabled,\n    isSubmitting,\n    isTerminal,\n    isActionable,\n    titleId,\n    confirmationId,\n  };\n\n  return (\n    <ApprovalCardContext.Provider value={context}>\n      <section\n        data-slot=\"approval-card\"\n        className={cn(\n          approvalCardVariants({ variant }),\n          risk === \"critical\"\n            ? \"border-[color-mix(in_oklab,var(--destructive)_40%,var(--border))]\"\n            : \"border-border\",\n          disabled && \"opacity-55\",\n          className,\n        )}\n        {...sectionProps}\n        aria-busy={isSubmitting}\n        aria-labelledby={ariaLabelledby ?? titleId}\n        data-variant={variant}\n        data-risk={risk}\n        data-status={status}\n      >\n        {children}\n      </section>\n    </ApprovalCardContext.Provider>\n  );\n}\n\nexport type ApprovalCardHeaderProps = Omit<ComponentProps<\"div\">, \"title\"> & {\n  title: ReactNode;\n  description?: ReactNode;\n  riskLabel?: ReactNode;\n  statusLabel?: ReactNode;\n};\n\nexport function ApprovalCardHeader({\n  title,\n  description,\n  riskLabel,\n  statusLabel,\n  className,\n  ...props\n}: ApprovalCardHeaderProps) {\n  const context = useApprovalCard(\"ApprovalCardHeader\");\n  const riskState = riskConfig[context.risk];\n  const RiskIcon = riskState.icon;\n  const resolvedStatusLabel =\n    statusLabel ??\n    {\n      ready: \"Ready for review\",\n      submitting: context.pendingDecision === \"rejected\" ? \"Rejecting…\" : \"Approving…\",\n      approved: \"Approved\",\n      rejected: \"Rejected\",\n      error: \"Decision failed\",\n    }[context.status];\n\n  return (\n    <div\n      data-slot=\"approval-card-header\"\n      className={cn(\"flex items-start gap-3\", className)}\n      {...props}\n    >\n      <span\n        className={cn(\n          \"grid size-8 shrink-0 place-items-center rounded-[10px]\",\n          riskState.iconClassName,\n        )}\n      >\n        <RiskIcon className=\"size-4\" strokeWidth={1.75} aria-hidden=\"true\" />\n      </span>\n      <div className=\"min-w-0 flex-1\">\n        <div className=\"flex flex-wrap items-center justify-between gap-x-3 gap-y-1\">\n          <span\n            className={cn(\n              \"inline-flex items-center rounded-full px-2 py-0.5 text-[11.5px] leading-4 font-medium\",\n              riskState.iconClassName,\n            )}\n          >\n            {riskLabel ?? riskState.label}\n          </span>\n          <span\n            className={cn(\n              \"inline-flex items-center gap-1.5 text-[11.5px] leading-4 text-subtle-foreground\",\n              context.status === \"approved\" && \"text-success-ink\",\n              context.status === \"rejected\" && \"text-muted-foreground\",\n              context.status === \"error\" && \"text-destructive-ink\",\n            )}\n            role=\"status\"\n            aria-live=\"polite\"\n          >\n            {context.isSubmitting ? (\n              <LoaderCircle\n                className=\"size-3 animate-spin motion-reduce:animate-none\"\n                aria-hidden=\"true\"\n              />\n            ) : context.status === \"approved\" ? (\n              <Check\n                key=\"approved\"\n                className=\"size-3 animate-in duration-200 ease-out-quint fade-in-0 zoom-in-50 motion-reduce:animate-none text-success-ink\"\n                aria-hidden=\"true\"\n              />\n            ) : context.status === \"rejected\" ? (\n              <X\n                key=\"rejected\"\n                className=\"size-3 animate-in duration-200 ease-out-quint fade-in-0 zoom-in-50 motion-reduce:animate-none text-destructive-ink\"\n                aria-hidden=\"true\"\n              />\n            ) : context.status === \"error\" ? (\n              <CircleAlert\n                key=\"error\"\n                className=\"size-3 animate-in duration-200 ease-out-quint fade-in-0 zoom-in-50 motion-reduce:animate-none text-destructive-ink\"\n                aria-hidden=\"true\"\n              />\n            ) : null}\n            <span\n              className={cn(\n                context.isSubmitting && \"shimmer-text motion-reduce:text-muted-foreground!\",\n              )}\n            >\n              {resolvedStatusLabel}\n            </span>\n          </span>\n        </div>\n        <h3\n          id={context.titleId}\n          className=\"mt-2 text-[14px] leading-5 font-medium tracking-[-0.005em] text-balance\"\n        >\n          {title}\n        </h3>\n        {description ? (\n          <p className=\"mt-1 max-w-[68ch] text-[13px] leading-[19px] text-pretty text-muted-foreground\">\n            {description}\n          </p>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n\nexport type ApprovalCardDetailsProps = ComponentProps<\"dl\">;\n\nexport function ApprovalCardDetails({ children, className, ...props }: ApprovalCardDetailsProps) {\n  const context = useApprovalCard(\"ApprovalCardDetails\");\n  if (context.variant !== \"detailed\") return null;\n\n  return (\n    <dl\n      data-slot=\"approval-card-details\"\n      className={cn(\n        \"mt-4 grid gap-x-4 gap-y-3 rounded-[10px] bg-muted/55 p-3 sm:grid-cols-2\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </dl>\n  );\n}\n\nexport type ApprovalCardDetailProps = ComponentProps<\"div\"> & { label: ReactNode };\n\nexport function ApprovalCardDetail({\n  label,\n  children,\n  className,\n  ...props\n}: ApprovalCardDetailProps) {\n  return (\n    <div data-slot=\"approval-card-detail\" className={cn(\"min-w-0\", className)} {...props}>\n      <dt className=\"text-[11.5px] leading-4 text-subtle-foreground\">{label}</dt>\n      <dd className=\"mt-1 min-w-0 text-[13px] leading-[18px] text-foreground [&_code]:rounded-md [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-px [&_code]:font-mono [&_code]:text-[11.5px] [&_li+li]:mt-1 [&_ul]:list-inside [&_ul]:list-disc [&_ul]:marker:text-subtle-foreground\">\n        {children}\n      </dd>\n    </div>\n  );\n}\n\nexport type ApprovalCardConfirmationProps = ComponentProps<\"div\">;\n\nexport function ApprovalCardConfirmation({ className, ...props }: ApprovalCardConfirmationProps) {\n  const context = useApprovalCard(\"ApprovalCardConfirmation\");\n  if (context.risk !== \"critical\" || context.isTerminal || !context.confirmation) return null;\n\n  return (\n    <div data-slot=\"approval-card-confirmation\" className={cn(\"mt-4\", className)} {...props}>\n      <Label\n        htmlFor={context.confirmationId}\n        className=\"block text-[12.5px] leading-[18px] font-normal text-muted-foreground select-auto\"\n      >\n        {context.confirmation.label ?? (\n          <>\n            Type{\" \"}\n            <strong className=\"rounded-md bg-muted px-1.5 py-px font-mono text-[11.5px] font-medium text-foreground\">\n              {context.confirmation.phrase}\n            </strong>{\" \"}\n            to confirm.\n          </>\n        )}\n      </Label>\n      <Input\n        id={context.confirmationId}\n        type=\"text\"\n        value={context.confirmationValue}\n        placeholder={context.confirmation.placeholder ?? context.confirmation.phrase}\n        autoComplete=\"off\"\n        spellCheck={false}\n        disabled={context.disabled || context.isSubmitting}\n        onChange={(event) => context.setConfirmationValue(event.target.value)}\n        className=\"mt-2 h-9 w-full rounded-[10px] border border-transparent bg-background px-3 font-mono text-[12.5px] text-foreground caret-foreground shadow-none outline-none transition-[border-color,box-shadow] duration-[120ms] ease-out selection:bg-foreground/18 selection:text-foreground placeholder:text-subtle-foreground focus-visible:border-border-strong focus-visible:ring-2 focus-visible:ring-ring/45 disabled:cursor-not-allowed disabled:opacity-100 motion-reduce:transition-none md:text-[12.5px] dark:bg-background\"\n      />\n    </div>\n  );\n}\n\nexport type ApprovalCardErrorProps = ComponentProps<\"p\">;\n\nexport function ApprovalCardError({ className, children, ...props }: ApprovalCardErrorProps) {\n  const context = useApprovalCard(\"ApprovalCardError\");\n  if (context.status !== \"error\") return null;\n\n  return (\n    <p\n      data-slot=\"approval-card-error\"\n      className={cn(\n        \"mt-4 flex items-start gap-2 rounded-[10px] bg-destructive/10 px-3 py-2.5 text-[13px] leading-[18px] text-foreground\",\n        className,\n      )}\n      {...props}\n      role=\"alert\"\n    >\n      <CircleAlert className=\"mt-0.5 size-3.5 shrink-0 text-destructive-ink\" aria-hidden=\"true\" />\n      {children}\n    </p>\n  );\n}\n\nexport type ApprovalCardActionsProps = ComponentProps<\"div\">;\n\nexport function ApprovalCardActions({ className, children, ...props }: ApprovalCardActionsProps) {\n  const context = useApprovalCard(\"ApprovalCardActions\");\n  if (context.isTerminal) return null;\n\n  return (\n    <div\n      data-slot=\"approval-card-actions\"\n      className={cn(\"mt-4 flex flex-wrap items-center justify-end gap-2\", className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport type ApprovalCardRejectProps = ComponentProps<\"button\">;\n\nexport function ApprovalCardReject({\n  children = \"Reject\",\n  className,\n  disabled,\n  ...props\n}: ApprovalCardRejectProps) {\n  const context = useApprovalCard(\"ApprovalCardReject\");\n\n  return (\n    <Button\n      data-slot=\"approval-card-reject\"\n      variant=\"secondary\"\n      {...props}\n      type=\"button\"\n      className={cn(\n        \"h-8 rounded-full px-3.5 py-0 text-[13px] has-[>svg]:px-3.5 transition-[background-color,scale] duration-[140ms] ease-[cubic-bezier(0.23,1,0.32,1)] hover:bg-[color-mix(in_oklab,var(--secondary)_85%,var(--foreground))] focus-visible:ring-2 focus-visible:ring-ring enabled:active:scale-[0.97] disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transition-none motion-reduce:enabled:active:scale-100\",\n        className,\n      )}\n      disabled={context.disabled || !context.isActionable || disabled}\n    >\n      {context.isSubmitting && context.pendingDecision === \"rejected\" ? \"Rejecting…\" : children}\n    </Button>\n  );\n}\n\nexport type ApprovalCardApproveProps = ComponentProps<\"button\">;\n\nexport function ApprovalCardApprove({\n  children = \"Approve\",\n  className,\n  disabled: disabledProp,\n  ...props\n}: ApprovalCardApproveProps) {\n  const context = useApprovalCard(\"ApprovalCardApprove\");\n  const disabled =\n    context.disabled || !context.isActionable || !context.confirmationMatches || disabledProp;\n\n  return (\n    <Button\n      data-slot=\"approval-card-approve\"\n      variant={context.risk === \"critical\" ? \"destructive\" : \"default\"}\n      {...props}\n      type=\"button\"\n      className={cn(\n        \"h-8 rounded-full px-3.5 py-0 text-[13px] has-[>svg]:px-3.5 transition-[filter,opacity,scale] duration-[140ms] ease-[cubic-bezier(0.23,1,0.32,1)] enabled:hover:brightness-[1.08] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card enabled:active:scale-[0.97] disabled:cursor-not-allowed disabled:opacity-45 motion-reduce:transition-none motion-reduce:enabled:active:scale-100 dark:focus-visible:ring-ring\",\n        context.risk === \"critical\"\n          ? \"bg-destructive text-primary-foreground hover:bg-destructive dark:bg-destructive\"\n          : \"bg-primary text-primary-foreground hover:bg-primary\",\n        className,\n      )}\n      disabled={disabled}\n    >\n      {context.isSubmitting && context.pendingDecision === \"approved\" ? \"Approving…\" : children}\n    </Button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/approval-card.tsx"
    }
  ],
  "categories": [
    "ai",
    "feedback"
  ],
  "type": "registry:ui"
}