{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "trust-panel",
  "title": "Trust Panel",
  "description": "Customer logos, ratings, certifications, and security evidence in one section.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/trust-panel.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { ShieldCheck, Star } from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  type ReactNode,\n  type RefObject,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const TRUST_PANEL_VARIANTS = [\"card\", \"plain\", \"compact\"] as const;\nexport type TrustPanelVariant = (typeof TRUST_PANEL_VARIANTS)[number];\nexport type TrustPanelProps = ComponentProps<\"section\"> & { variant?: TrustPanelVariant };\ntype TrustContext = { id: string; variant: TrustPanelVariant; revealed: boolean };\nconst Context = createContext<TrustContext | null>(null);\nfunction useTrust(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within TrustPanel`);\n  return context;\n}\n\n/** Flips to true once, when 30% of the element is on screen. */\nfunction useRevealOnce(ref: RefObject<Element | null>, enabled = true) {\n  const [revealed, setRevealed] = useState(false);\n  useEffect(() => {\n    const element = ref.current;\n    if (!enabled || !element) return;\n    if (typeof IntersectionObserver === \"undefined\") {\n      setRevealed(true);\n      return;\n    }\n    const observer = new IntersectionObserver(\n      (entries) => {\n        if (entries.some((entry) => entry.isIntersecting)) {\n          setRevealed(true);\n          observer.disconnect();\n        }\n      },\n      { threshold: 0.3 },\n    );\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, [ref, enabled]);\n  return revealed;\n}\n\nconst trustPanelVariants = cva(\n  \"@container/trust-panel box-border grid min-w-0 text-[13px]/[18px] text-card-foreground\",\n  {\n    variants: {\n      variant: {\n        card: \"gap-[18px] rounded-[14px] border bg-card p-5\",\n        plain: \"gap-4\",\n        compact: \"gap-3 rounded-xl border bg-card p-3\",\n      },\n    },\n  },\n);\n\nexport function TrustPanel({ variant = \"card\", className, children, ...props }: TrustPanelProps) {\n  const id = useId();\n  const ref = useRef<HTMLElement>(null);\n  const revealed = useRevealOnce(ref);\n  return (\n    <Context.Provider value={{ id, variant, revealed }}>\n      <section\n        ref={ref}\n        aria-labelledby={`${id}-title`}\n        data-slot=\"trust-panel\"\n        className={cn(trustPanelVariants({ variant }), className)}\n        {...props}\n        data-variant={variant}\n        data-revealed={revealed || undefined}\n      >\n        {children}\n      </section>\n    </Context.Provider>\n  );\n}\n\n/** Put the proof number in a `<b>` to lift it to the foreground: `<b>2,400</b> teams …`. */\nexport function TrustPanelTitle({ className, ...props }: ComponentProps<\"h2\">) {\n  const context = useTrust(\"TrustPanelTitle\");\n  return (\n    <h2\n      data-slot=\"trust-panel-title\"\n      className={cn(\n        \"m-0 font-medium tracking-[-0.01em] text-balance text-muted-foreground [&_b]:font-semibold [&_b]:text-foreground [&_b]:tabular-nums\",\n        context.variant === \"compact\" ? \"text-xs/[18px]\" : \"text-[15px]/5\",\n        className,\n      )}\n      {...props}\n      id={`${context.id}-title`}\n    />\n  );\n}\n\nexport function TrustPanelLogos({ className, ...props }: ComponentProps<\"ul\">) {\n  const context = useTrust(\"TrustPanelLogos\");\n  return (\n    <ul\n      data-slot=\"trust-panel-logos\"\n      className={cn(\n        \"m-0 min-w-0 list-none p-0\",\n        context.variant === \"card\" &&\n          \"grid grid-cols-[repeat(auto-fit,minmax(104px,1fr))] border-y py-3 @max-[44rem]/trust-panel:grid-cols-3 @max-[26rem]/trust-panel:grid-cols-2\",\n        context.variant === \"plain\" && \"flex flex-wrap gap-x-6\",\n        context.variant === \"compact\" && \"flex flex-wrap gap-1.5\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst trustPanelLogoVariants = cva(\n  [\n    \"relative flex items-center whitespace-nowrap text-muted-foreground transition-colors duration-120 ease-out has-[a:hover]:text-foreground\",\n    \"[&_a]:rounded-md [&_a]:text-inherit [&_a]:no-underline [&_a]:focus-visible:outline-2 [&_a]:focus-visible:outline-offset-2 [&_a]:focus-visible:outline-ring\",\n    \"[&_svg]:flex-none [&_svg]:opacity-90 [&_svg]:transition-opacity [&_svg]:duration-120 [&_svg]:ease-out has-[a:hover]:[&_svg]:opacity-100\",\n    \"data-[enter=true]:animate-in data-[enter=true]:fade-in-0 data-[enter=true]:slide-in-from-bottom-1 data-[enter=true]:duration-240 data-[enter=true]:ease-out-quint data-[enter=true]:fill-mode-both motion-reduce:animate-none\",\n    \"nth-2:[animation-delay:40ms] nth-3:[animation-delay:80ms] nth-4:[animation-delay:120ms] nth-5:[animation-delay:160ms] nth-[n+6]:[animation-delay:200ms]\",\n    \"motion-safe:data-[enter=false]:opacity-0\",\n  ],\n  {\n    variants: {\n      variant: {\n        card: \"h-11 justify-center gap-[7px] text-[14.5px]/none font-semibold tracking-[-0.02em] [&_svg]:size-[15px]\",\n        plain:\n          \"h-8 justify-start gap-[7px] text-[14.5px]/none font-semibold tracking-[-0.02em] [&_svg]:size-[15px]\",\n        compact: \"h-7 justify-center gap-1.5 rounded-full bg-muted px-2.5 text-xs [&_svg]:size-3.5\",\n      },\n    },\n  },\n);\n\nexport function TrustPanelLogo({\n  name,\n  children,\n  className,\n  onErrorCapture,\n  ...props\n}: ComponentProps<\"li\"> & { name: string; children?: ReactNode }) {\n  const context = useTrust(\"TrustPanelLogo\");\n  const [failed, setFailed] = useState(false);\n  return (\n    <li\n      data-slot=\"trust-panel-logo\"\n      data-enter={context.revealed}\n      data-fallback={failed || undefined}\n      className={cn(trustPanelLogoVariants({ variant: context.variant }), className)}\n      onErrorCapture={(event) => {\n        onErrorCapture?.(event);\n        if (event.target instanceof HTMLImageElement) setFailed(true);\n      }}\n      {...props}\n    >\n      {failed ? (\n        name\n      ) : (\n        <>\n          <span aria-hidden=\"true\" className=\"contents\">\n            {children}\n          </span>\n          <span className=\"sr-only\">{name}</span>\n        </>\n      )}\n    </li>\n  );\n}\n\nconst STAR_SIZE = 14;\nconst STAR_GAP = 2;\n\nfunction Stars({ max, className }: { max: number; className?: string }) {\n  return (\n    <span className={cn(\"inline-flex flex-none gap-0.5\", className)}>\n      {Array.from({ length: max }, (_, index) => (\n        <Star\n          // biome-ignore lint/suspicious/noArrayIndexKey: stars are positional and static.\n          key={index}\n          size={STAR_SIZE}\n          strokeWidth={0}\n          fill=\"currentColor\"\n          className=\"flex-none\"\n        />\n      ))}\n    </span>\n  );\n}\n\nexport function TrustPanelRating({\n  value,\n  max = 5,\n  children,\n  className,\n  ...props\n}: ComponentProps<\"p\"> & { value: number; max?: number }) {\n  const context = useContext(Context);\n  const ref = useRef<HTMLParagraphElement>(null);\n  const ownReveal = useRevealOnce(ref, !context);\n  const revealed = context ? context.revealed : ownReveal;\n  const compact = context?.variant === \"compact\";\n  const clamped = Math.min(Math.max(value, 0), max);\n  const trackWidth = max * STAR_SIZE + (max - 1) * STAR_GAP;\n  const fillWidth = Math.round((clamped / max) * trackWidth * 10) / 10;\n  return (\n    <p\n      ref={ref}\n      data-slot=\"trust-panel-rating\"\n      className={cn(\n        \"m-0 tabular-nums\",\n        compact\n          ? \"flex flex-wrap items-center gap-x-2 gap-y-0.5\"\n          : \"grid content-start justify-items-start gap-0.5\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"inline-flex items-center gap-2\">\n        <span\n          className={cn(\n            \"font-semibold text-foreground\",\n            compact ? \"text-[13px]/[18px]\" : \"text-xl/6 tracking-[-0.02em]\",\n          )}\n        >\n          {clamped.toFixed(1)}\n          <span className=\"sr-only\"> out of {max}</span>\n        </span>\n        <span aria-hidden=\"true\" className=\"relative inline-flex\">\n          <Stars max={max} className=\"text-foreground/18\" />\n          <span\n            data-slot=\"trust-panel-rating-fill\"\n            className=\"absolute inset-y-0 left-0 overflow-hidden\"\n            style={{ width: fillWidth }}\n          >\n            <span\n              className={cn(\n                \"flex h-full overflow-hidden\",\n                revealed\n                  ? \"animate-in slide-in-from-left-full duration-480 ease-out-quint fill-mode-both [animation-delay:120ms] motion-reduce:animate-none\"\n                  : \"motion-safe:-translate-x-full\",\n              )}\n            >\n              <span\n                className={cn(\n                  \"flex h-full\",\n                  revealed\n                    ? \"animate-in slide-in-from-right-full duration-480 ease-out-quint fill-mode-both [animation-delay:120ms] motion-reduce:animate-none\"\n                    : \"motion-safe:translate-x-full\",\n                )}\n              >\n                <Stars max={max} className=\"text-foreground\" />\n              </span>\n            </span>\n          </span>\n        </span>\n      </span>\n      {children ? <span className=\"text-[11.5px]/4 text-subtle-foreground\">{children}</span> : null}\n    </p>\n  );\n}\n\nexport function TrustPanelBadges({ className, ...props }: ComponentProps<\"ul\">) {\n  return (\n    <ul\n      data-slot=\"trust-panel-badges\"\n      className={cn(\"m-0 flex list-none flex-wrap gap-1.5 p-0\", className)}\n      {...props}\n    />\n  );\n}\n\n/** Put the claim in a `<b>` to lift it to the foreground: `<b>SOC 2 Type II</b> report on request`. */\nexport function TrustPanelBadge({\n  icon = <ShieldCheck size={13} className=\"size-[13px]\" strokeWidth={1.75} aria-hidden=\"true\" />,\n  children,\n  className,\n  ...props\n}: ComponentProps<\"li\"> & { icon?: ReactNode }) {\n  return (\n    <Badge\n      asChild\n      variant=\"secondary\"\n      className={cn(\n        \"min-h-[26px] justify-start gap-1.5 rounded-full border-0 bg-muted py-0 pr-2.5 pl-2 text-xs/4 font-normal whitespace-normal text-muted-foreground [&_b]:font-medium [&_b]:text-foreground [&>span>svg]:size-[13px]\",\n        \"has-[a]:transition-[background-color,scale] has-[a]:duration-120 has-[a]:ease-out has-[a:hover]:bg-[color-mix(in_oklab,var(--muted)_85%,var(--foreground))] has-[a:active]:scale-[0.97] has-[a:active]:duration-140 has-[a:active]:ease-out-quint motion-reduce:has-[a:active]:scale-100\",\n        \"[&_a]:text-inherit [&_a]:no-underline [&_a]:outline-none [&_a]:after:absolute [&_a]:after:inset-0 [&_a]:after:rounded-full has-[a:focus-visible]:outline-2 has-[a:focus-visible]:outline-offset-2 has-[a:focus-visible]:outline-ring relative\",\n        className,\n      )}\n    >\n      <li data-slot=\"trust-panel-badge\" {...props}>\n        <span aria-hidden=\"true\" className=\"inline-flex flex-none text-success\">\n          {icon}\n        </span>\n        {children}\n      </li>\n    </Badge>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/trust-panel.tsx"
    }
  ],
  "categories": [
    "marketing",
    "social-proof"
  ],
  "type": "registry:ui"
}