{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "astry-ui",
  "type": "registry:ui",
  "title": "Astry UI",
  "description": "Single-file pack of all Astry primitives. Square corners · semantic-token consumers · framer-motion animations · HugeIcons-backed Icon. Install once, get every primitive.",
  "homepage": "https://brand.astry.agency",
  "dependencies": [
    "react",
    "react-dom",
    "next",
    "framer-motion",
    "tailwind-merge",
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "https://brand.astry.agency/r/tokens.json"
  ],
  "files": [
    {
      "path": "components/astry-ui.tsx",
      "type": "registry:ui",
      "target": "components/astry-ui.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport Image from \"next/image\";\nimport { motion, AnimatePresence, LayoutGroup } from \"framer-motion\";\nimport { twMerge } from \"tailwind-merge\";\nimport { HugeiconsIcon, type IconSvgElement } from \"@hugeicons/react\";\nimport { ArrowDown01Icon, ArrowLeft01Icon, ArrowRight01Icon, Tick02Icon } from \"@hugeicons/core-free-icons\";\n\n/* ============================================================================\n   ASTRY · UI PRIMITIVES\n   - Components consume only semantic tokens (no hex)\n   - Icons routed through HugeIcons real package\n   - cx() uses tailwind-merge so user className properly overrides defaults\n   ============================================================================ */\n\nexport function cx(...classes: Array<string | false | null | undefined>) {\n  return twMerge(classes.filter(Boolean).join(\" \"));\n}\n\n/* ----------------------------------------- Icon (HugeIcons wrapper) */\nexport function Icon({\n  icon,\n  size = 18,\n  strokeWidth = 1.8,\n  className,\n  ...rest\n}: {\n  icon: IconSvgElement;\n  size?: number;\n  strokeWidth?: number;\n  className?: string;\n} & React.SVGAttributes<SVGSVGElement>) {\n  return (\n    <HugeiconsIcon\n      icon={icon}\n      size={size}\n      strokeWidth={strokeWidth}\n      className={className}\n      {...rest}\n    />\n  );\n}\n\n/* ----------------------------------------- Eyebrow (slash removed) */\nexport function Eyebrow({\n  children,\n  className,\n  tone = \"brand\",\n}: {\n  children: React.ReactNode;\n  className?: string;\n  withSlash?: boolean; // kept for back-compat, ignored\n  tone?: \"brand\" | \"muted\" | \"subtle\";\n}) {\n  const colorMap = { brand: \"text-brand\", muted: \"text-text-muted\", subtle: \"text-text-subtle\" };\n  return (\n    <p\n      className={cx(\n        \"text-xs font-semibold uppercase tracking-[0.08em] inline-flex items-center gap-2\",\n        colorMap[tone],\n        className\n      )}\n    >\n      {children}\n    </p>\n  );\n}\n\n/* ----------------------------------------- Slash (legacy export, no-op for callers) */\nexport function Slash(_: { className?: string }): null { return null; }\n\n/* ----------------------------------------- AngleCorner — SVG corner from /brand/Angle.svg\n   Native shape is a top-right L (filled top + right edges, open bottom-left).\n   Rotation is done via SVG <g transform> so the CSS transform stays free for hover translate. */\nconst ANGLE_PATH = \"M827 827H683V130H0V0H827V827Z\";\nexport function AngleCorner({\n  position, size = 26, color = \"var(--brand)\", className,\n}: {\n  position: \"tl\" | \"tr\" | \"bl\" | \"br\";\n  size?: number;\n  color?: string;\n  className?: string;\n}) {\n  const rotate = { tr: 0, br: 90, bl: 180, tl: 270 }[position];\n  const place: React.CSSProperties = {\n    position: \"absolute\",\n    width: size,\n    height: size,\n    pointerEvents: \"none\",\n  };\n  if (position === \"tl\") { place.top = 0; place.left = 0; }\n  if (position === \"tr\") { place.top = 0; place.right = 0; }\n  if (position === \"bl\") { place.bottom = 0; place.left = 0; }\n  if (position === \"br\") { place.bottom = 0; place.right = 0; }\n  return (\n    <svg aria-hidden viewBox=\"0 0 827 827\" style={place} className={className}>\n      <g transform={`rotate(${rotate} 413.5 413.5)`}>\n        <path d={ANGLE_PATH} fill={color} />\n      </g>\n    </svg>\n  );\n}\n\n/* ----------------------------------------- BracketFrame — SVG corners on all 4 corners */\nexport function BracketFrame({\n  children,\n  className,\n  size = 26,\n  color = \"var(--brand)\",\n}: {\n  children: React.ReactNode;\n  className?: string;\n  size?: number;\n  color?: string;\n}) {\n  return (\n    <div className={cx(\"relative\", className)}>\n      <AngleCorner position=\"tl\" size={size} color={color} />\n      <AngleCorner position=\"tr\" size={size} color={color} />\n      <AngleCorner position=\"bl\" size={size} color={color} />\n      <AngleCorner position=\"br\" size={size} color={color} />\n      {children}\n    </div>\n  );\n}\n\n/* ----------------------------------------- Button */\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"outline\" | \"ghost\" | \"link\" | \"danger\" | \"inverse\" | \"inverse-ghost\";\nexport type ButtonSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n\nconst buttonBase =\n  \"astry-button inline-flex items-center justify-center gap-2 font-medium rounded-md \" +\n  \"transition-colors duration-fast ease-standard select-none whitespace-nowrap \" +\n  \"disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none \" +\n  \"focus-visible:outline-2 focus-visible:outline-brand focus-visible:outline-offset-2\";\n\nconst buttonSize: Record<ButtonSize, string> = {\n  xs: \"h-7 px-2.5 text-xs gap-1.5\",\n  sm: \"h-9 px-3 text-sm\",\n  md: \"h-11 px-5 text-base\",\n  lg: \"h-12 px-6 text-md\",\n};\n\nconst buttonVariant: Record<ButtonVariant, string> = {\n  primary: \"bg-brand text-text-on-brand hover:bg-brand-hover active:bg-brand-pressed\",\n  secondary: \"bg-charcoal text-text-on-brand hover:bg-charcoal/85 dark:bg-text dark:text-bg\",\n  outline: \"border border-border-strong text-text bg-transparent hover:border-text hover:bg-subtle\",\n  ghost: \"text-text bg-transparent hover:bg-subtle\",\n  link: \"text-brand hover:text-brand-hover underline-offset-4 hover:underline px-0 h-auto\",\n  danger: \"bg-danger text-text-on-brand hover:opacity-90\",\n  // For use on brand-blue surfaces\n  inverse: \"bg-text-on-brand text-brand hover:bg-text-on-brand/90 active:bg-text-on-brand/80\",\n  \"inverse-ghost\": \"text-text-on-brand bg-transparent hover:bg-text-on-brand/10 active:bg-text-on-brand/20\",\n};\n\nexport interface ButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"size\"> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  leftIcon?: IconSvgElement;\n  rightIcon?: IconSvgElement;\n  /** Wrap with 4 brand corner angles — for ghost/outline CTAs on white-on-white surfaces. */\n  withAngles?: boolean;\n}\n\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ variant = \"primary\", size = \"md\", leftIcon, rightIcon, withAngles, className, children, ...rest }, ref) => {\n    const iconSize = size === \"xs\" ? 14 : size === \"sm\" ? 16 : size === \"lg\" ? 20 : 18;\n    const angleSize = size === \"xs\" ? 7 : size === \"sm\" ? 8 : size === \"lg\" ? 11 : 10;\n    const btn = (\n      <button\n        ref={ref}\n        className={cx(buttonBase, buttonSize[size], buttonVariant[variant], className)}\n        {...rest}\n      >\n        {leftIcon && <Icon icon={leftIcon} size={iconSize} />}\n        {children}\n        {rightIcon && <Icon icon={rightIcon} size={iconSize} />}\n      </button>\n    );\n    if (!withAngles) return btn;\n    return (\n      <span className=\"relative inline-flex group/angles\">\n        <AngleCorner position=\"tl\" size={angleSize} className=\"transition-transform duration-base ease-standard group-hover/angles:translate-x-[3px] group-hover/angles:translate-y-[3px]\" />\n        <AngleCorner position=\"tr\" size={angleSize} className=\"transition-transform duration-base ease-standard group-hover/angles:-translate-x-[3px] group-hover/angles:translate-y-[3px]\" />\n        <AngleCorner position=\"bl\" size={angleSize} className=\"transition-transform duration-base ease-standard group-hover/angles:translate-x-[3px] group-hover/angles:-translate-y-[3px]\" />\n        <AngleCorner position=\"br\" size={angleSize} className=\"transition-transform duration-base ease-standard group-hover/angles:-translate-x-[3px] group-hover/angles:-translate-y-[3px]\" />\n        {btn}\n      </span>\n    );\n  }\n);\nButton.displayName = \"Button\";\n\n/* ----------------------------------------- IconButton */\nexport interface IconButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"size\"> {\n  icon: IconSvgElement;\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  \"aria-label\": string;\n}\nexport const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(\n  ({ icon, variant = \"ghost\", size = \"md\", className, ...rest }, ref) => {\n    const dim = size === \"xs\" ? \"h-7 w-7\" : size === \"sm\" ? \"h-9 w-9\" : size === \"lg\" ? \"h-12 w-12\" : \"h-11 w-11\";\n    const iconSize = size === \"xs\" ? 14 : size === \"sm\" ? 16 : size === \"lg\" ? 20 : 18;\n    return (\n      <button\n        ref={ref}\n        className={cx(buttonBase, dim, buttonVariant[variant], \"px-0\", className)}\n        {...rest}\n      >\n        <Icon icon={icon} size={iconSize} />\n      </button>\n    );\n  }\n);\nIconButton.displayName = \"IconButton\";\n\n/* ----------------------------------------- Input */\nexport interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\"> {\n  label?: string;\n  hint?: string;\n  error?: string;\n  success?: boolean;\n  leadingIcon?: IconSvgElement;\n  trailingIcon?: IconSvgElement;\n  size?: \"sm\" | \"md\" | \"lg\";\n}\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ label, hint, error, success, leadingIcon, trailingIcon, id, className, size = \"md\", ...rest }, ref) => {\n    const generatedId = React.useId();\n    const inputId = id || generatedId;\n    const sizeMap = { sm: \"h-9 text-sm\", md: \"h-11 text-base\", lg: \"h-12 text-md\" };\n    return (\n      <div className=\"block w-full\">\n        {label && (\n          <label htmlFor={inputId} className=\"block text-sm font-medium text-text mb-1.5\">\n            {label}\n          </label>\n        )}\n        <div className=\"relative\">\n          {leadingIcon && (\n            <span className=\"absolute left-3 top-1/2 -translate-y-1/2 text-text-subtle\">\n              <Icon icon={leadingIcon} size={16} />\n            </span>\n          )}\n          <input\n            ref={ref}\n            id={inputId}\n            aria-invalid={!!error || undefined}\n            aria-describedby={error ? `${inputId}-error` : hint ? `${inputId}-hint` : undefined}\n            className={cx(\n              \"astry-input w-full rounded-md bg-canvas text-text placeholder:text-text-subtle\",\n              \"border transition-colors duration-fast ease-standard\",\n              error\n                ? \"border-danger focus:ring-2 focus:ring-danger/20\"\n                : success\n                ? \"border-success\"\n                : \"border-border hover:border-border-strong focus:border-brand focus:ring-2 focus:ring-brand/20\",\n              \"focus:outline-none px-4\",\n              sizeMap[size],\n              leadingIcon && \"pl-10\",\n              trailingIcon && \"pr-10\",\n              className\n            )}\n            {...rest}\n          />\n          {trailingIcon && (\n            <span className=\"absolute right-3 top-1/2 -translate-y-1/2 text-text-subtle\">\n              <Icon icon={trailingIcon} size={16} />\n            </span>\n          )}\n        </div>\n        {error ? (\n          <p id={`${inputId}-error`} role=\"alert\" className=\"text-xs text-danger mt-1.5\">\n            {error}\n          </p>\n        ) : hint ? (\n          <p id={`${inputId}-hint`} className=\"text-xs text-text-subtle mt-1.5\">{hint}</p>\n        ) : null}\n      </div>\n    );\n  }\n);\nInput.displayName = \"Input\";\n\n/* ----------------------------------------- Textarea */\nexport interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {\n  label?: string;\n  hint?: string;\n  error?: string;\n}\nexport const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ label, hint, error, id, className, ...rest }, ref) => {\n    const generatedId = React.useId();\n    const inputId = id || generatedId;\n    return (\n      <div className=\"block w-full\">\n        {label && (\n          <label htmlFor={inputId} className=\"block text-sm font-medium text-text mb-1.5\">\n            {label}\n          </label>\n        )}\n        <textarea\n          ref={ref}\n          id={inputId}\n          aria-invalid={!!error || undefined}\n          className={cx(\n            \"astry-input w-full rounded-md bg-canvas text-text placeholder:text-text-subtle\",\n            \"border transition-colors duration-fast ease-standard\",\n            error\n              ? \"border-danger\"\n              : \"border-border hover:border-border-strong focus:border-brand focus:ring-2 focus:ring-brand/20\",\n            \"focus:outline-none px-4 py-3 text-base resize-none\",\n            className\n          )}\n          {...rest}\n        />\n        {error ? (\n          <p role=\"alert\" className=\"text-xs text-danger mt-1.5\">{error}</p>\n        ) : hint ? (\n          <p className=\"text-xs text-text-subtle mt-1.5\">{hint}</p>\n        ) : null}\n      </div>\n    );\n  }\n);\nTextarea.displayName = \"Textarea\";\n\n/* ----------------------------------------- Select (custom dropdown — single source of truth) */\nexport type SelectOption = { value: string; label: string; description?: string; disabled?: boolean };\nexport interface SelectProps {\n  label?: string;\n  hint?: string;\n  error?: string;\n  options: SelectOption[];\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  placeholder?: string;\n  className?: string;\n  size?: \"sm\" | \"md\" | \"lg\";\n  disabled?: boolean;\n  id?: string;\n  \"aria-label\"?: string;\n}\nexport function Select({\n  label, hint, error, options, value, defaultValue, onChange,\n  placeholder = \"Select…\", className, size = \"md\", disabled, id,\n  \"aria-label\": ariaLabel,\n}: SelectProps) {\n  const generatedId = React.useId();\n  const inputId = id || generatedId;\n  const [internal, setInternal] = React.useState(defaultValue ?? \"\");\n  const current = value ?? internal;\n  const selected = options.find((o) => o.value === current);\n  const [open, setOpen] = React.useState(false);\n  const ref = React.useRef<HTMLDivElement>(null);\n\n  React.useEffect(() => {\n    if (!open) return;\n    const handler = (e: MouseEvent) => {\n      if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);\n    };\n    const esc = (e: KeyboardEvent) => { if (e.key === \"Escape\") setOpen(false); };\n    document.addEventListener(\"mousedown\", handler);\n    document.addEventListener(\"keydown\", esc);\n    return () => {\n      document.removeEventListener(\"mousedown\", handler);\n      document.removeEventListener(\"keydown\", esc);\n    };\n  }, [open]);\n\n  const choose = (v: string) => {\n    if (value === undefined) setInternal(v);\n    onChange?.(v);\n    setOpen(false);\n  };\n\n  const sizeMap = { sm: \"h-9 text-sm\", md: \"h-11 text-base\", lg: \"h-12 text-md\" };\n\n  return (\n    <div className=\"block w-full\">\n      {label && (\n        <label htmlFor={inputId} className=\"block text-sm font-medium text-text mb-1.5\">\n          {label}\n        </label>\n      )}\n      <div className=\"relative\" ref={ref}>\n        <button\n          id={inputId}\n          type=\"button\"\n          disabled={disabled}\n          aria-haspopup=\"listbox\"\n          aria-expanded={open}\n          aria-label={ariaLabel}\n          aria-invalid={!!error || undefined}\n          onClick={() => setOpen((o) => !o)}\n          className={cx(\n            \"astry-input w-full bg-canvas px-4 pr-10 inline-flex items-center justify-between text-left\",\n            \"border transition-colors duration-fast ease-standard\",\n            error\n              ? \"border-danger\"\n              : \"border-border hover:border-border-strong focus:border-brand focus:ring-2 focus:ring-brand/20\",\n            \"focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed\",\n            sizeMap[size],\n            className\n          )}\n        >\n          <span className={cx(selected ? \"text-text\" : \"text-text-subtle\", \"truncate\")}>\n            {selected?.label ?? placeholder}\n          </span>\n          <Icon\n            icon={ArrowDown01Icon}\n            size={16}\n            className={cx(\"shrink-0 text-text-subtle transition-transform duration-fast\", open && \"rotate-180\")}\n          />\n        </button>\n        <AnimatePresence>\n          {open && (\n            <motion.div\n              initial={{ opacity: 0, y: -4 }}\n              animate={{ opacity: 1, y: 0 }}\n              exit={{ opacity: 0, y: -4 }}\n              transition={{ duration: 0.12, ease: [0.4, 0, 0.2, 1] }}\n              className=\"absolute z-50 top-[calc(100%+4px)] left-0 right-0 bg-canvas border border-border-strong shadow-md max-h-64 overflow-y-auto\"\n              role=\"listbox\"\n            >\n              {options.map((o) => {\n                const isSelected = o.value === current;\n                return (\n                  <button\n                    key={o.value}\n                    type=\"button\"\n                    role=\"option\"\n                    aria-selected={isSelected}\n                    disabled={o.disabled}\n                    onClick={() => choose(o.value)}\n                    className={cx(\n                      \"w-full text-left px-4 py-2.5 text-sm transition-colors duration-fast inline-flex items-center justify-between gap-3\",\n                      isSelected\n                        ? \"bg-brand-soft text-brand font-medium\"\n                        : \"text-text hover:bg-subtle\",\n                      o.disabled && \"opacity-50 cursor-not-allowed\"\n                    )}\n                  >\n                    <span className=\"flex flex-col min-w-0\">\n                      <span className=\"truncate\">{o.label}</span>\n                      {o.description && <span className=\"text-[11px] text-text-subtle truncate\">{o.description}</span>}\n                    </span>\n                    {isSelected && <Icon icon={Tick02Icon} size={14} strokeWidth={2.5} />}\n                  </button>\n                );\n              })}\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n      {error ? (\n        <p role=\"alert\" className=\"text-xs text-danger mt-1.5\">{error}</p>\n      ) : hint ? (\n        <p className=\"text-xs text-text-subtle mt-1.5\">{hint}</p>\n      ) : null}\n    </div>\n  );\n}\n\n/* ----------------------------------------- Card */\nexport function Card({\n  children,\n  className,\n  hover = false,\n  as: Comp = \"div\",\n  ...rest\n}: {\n  children: React.ReactNode;\n  className?: string;\n  hover?: boolean;\n  as?: any;\n  [key: string]: any;\n}) {\n  return (\n    <Comp\n      className={cx(\n        \"astry-card bg-surface border border-border\",\n        hover && \"transition-colors duration-base ease-standard hover:border-text\",\n        className\n      )}\n      {...rest}\n    >\n      {children}\n    </Comp>\n  );\n}\nexport const CardHeader = ({ children, className }: any) => <div className={cx(\"px-6 pt-6 pb-3\", className)}>{children}</div>;\nexport const CardBody = ({ children, className }: any) => <div className={cx(\"px-6 pb-6\", className)}>{children}</div>;\nexport const CardFooter = ({ children, className }: any) => <div className={cx(\"px-6 py-4 border-t border-border\", className)}>{children}</div>;\nexport const CardTitle = ({ children, className }: any) => <h3 className={cx(\"text-lg font-semibold text-text\", className)}>{children}</h3>;\nexport const CardSubtitle = ({ children, className }: any) => <p className={cx(\"text-sm text-text-muted mt-1\", className)}>{children}</p>;\n\n/* ----------------------------------------- Badge */\ntype BadgeVariant = \"neutral\" | \"brand\" | \"success\" | \"warning\" | \"danger\" | \"outline\";\nconst badgeMap: Record<BadgeVariant, { bg: string; text: string; dot: string }> = {\n  neutral: { bg: \"bg-subtle\", text: \"text-text\", dot: \"bg-text-muted\" },\n  brand: { bg: \"bg-brand-soft\", text: \"text-brand\", dot: \"bg-brand\" },\n  success: { bg: \"bg-success-soft\", text: \"text-success\", dot: \"bg-success\" },\n  warning: { bg: \"bg-warning-soft\", text: \"text-warning\", dot: \"bg-warning\" },\n  danger: { bg: \"bg-danger-soft\", text: \"text-danger\", dot: \"bg-danger\" },\n  outline: { bg: \"bg-transparent border border-border-strong\", text: \"text-text\", dot: \"bg-text-muted\" },\n};\nexport function Badge({\n  children,\n  variant = \"neutral\",\n  dot = false,\n  size = \"md\",\n  className,\n}: {\n  children: React.ReactNode;\n  variant?: BadgeVariant;\n  dot?: boolean;\n  size?: \"sm\" | \"md\";\n  className?: string;\n}) {\n  const v = badgeMap[variant];\n  return (\n    <span\n      className={cx(\n        \"astry-badge inline-flex items-center gap-1.5 font-medium\",\n        size === \"sm\" ? \"px-2 py-0.5 text-[11px]\" : \"px-2.5 py-1 text-xs\",\n        v.bg, v.text, className\n      )}\n    >\n      {dot && <span aria-hidden className={cx(\"w-1.5 h-1.5 rounded-pill\", v.dot)} />}\n      {children}\n    </span>\n  );\n}\n\n/* ----------------------------------------- Tag (with optional removable) */\nexport function Tag({\n  children,\n  onRemove,\n  removeIcon,\n  className,\n}: {\n  children: React.ReactNode;\n  onRemove?: () => void;\n  removeIcon?: IconSvgElement;\n  className?: string;\n}) {\n  return (\n    <span className={cx(\n      \"inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-xs font-medium\",\n      \"bg-subtle text-text border border-border\",\n      className\n    )}>\n      {children}\n      {onRemove && removeIcon && (\n        <button\n          type=\"button\"\n          onClick={onRemove}\n          className=\"text-text-subtle hover:text-text transition-colors\"\n          aria-label=\"Remove\"\n        >\n          <Icon icon={removeIcon} size={12} strokeWidth={2} />\n        </button>\n      )}\n    </span>\n  );\n}\n\n/* ----------------------------------------- Alert */\nexport function Alert({\n  variant = \"info\",\n  title,\n  children,\n  icon,\n  onClose,\n  closeIcon,\n  className,\n}: {\n  variant?: \"info\" | \"success\" | \"warning\" | \"danger\";\n  title?: string;\n  children?: React.ReactNode;\n  icon?: IconSvgElement;\n  onClose?: () => void;\n  closeIcon?: IconSvgElement;\n  className?: string;\n}) {\n  const styleMap = {\n    info: \"bg-info-soft border-info/25 text-info\",\n    success: \"bg-success-soft border-success/25 text-success\",\n    warning: \"bg-warning-soft border-warning/25 text-warning\",\n    danger: \"bg-danger-soft border-danger/25 text-danger\",\n  };\n  return (\n    <div role={variant === \"danger\" ? \"alert\" : \"status\"} className={cx(\"border rounded-lg p-4 flex gap-3\", styleMap[variant], className)}>\n      {icon && <span className=\"shrink-0 mt-0.5\"><Icon icon={icon} size={20} /></span>}\n      <div className=\"flex-1\">\n        {title && <p className=\"font-semibold mb-0.5\">{title}</p>}\n        {children && <div className=\"text-sm text-text-muted\">{children}</div>}\n      </div>\n      {onClose && closeIcon && (\n        <button onClick={onClose} className=\"shrink-0 text-text-subtle hover:text-text transition-colors\" aria-label=\"Dismiss\">\n          <Icon icon={closeIcon} size={16} />\n        </button>\n      )}\n    </div>\n  );\n}\n\n/* ----------------------------------------- Checkbox */\nexport interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"type\" | \"size\"> {\n  label?: React.ReactNode;\n}\nexport const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(\n  ({ label, id, className, checked, ...rest }, ref) => {\n    const generatedId = React.useId();\n    const inputId = id || generatedId;\n    return (\n      <label htmlFor={inputId} className=\"inline-flex items-start gap-2.5 cursor-pointer select-none\">\n        <span className=\"relative inline-flex w-5 h-5 shrink-0 items-center justify-center mt-0.5\">\n          <input ref={ref} id={inputId} type=\"checkbox\" checked={checked} className=\"peer sr-only\" {...rest} />\n          <span\n            aria-hidden\n            className={cx(\n              \"w-5 h-5 rounded-sm border transition-colors duration-fast ease-standard\",\n              \"peer-checked:bg-brand peer-checked:border-brand\",\n              \"border-border-strong bg-canvas\",\n              \"peer-disabled:opacity-50 peer-focus-visible:ring-2 peer-focus-visible:ring-brand/30\",\n              className\n            )}\n          />\n          <svg\n            aria-hidden viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"3\"\n            strokeLinecap=\"round\" strokeLinejoin=\"round\"\n            className=\"absolute w-3.5 h-3.5 text-text-on-brand opacity-0 peer-checked:opacity-100 pointer-events-none\"\n          >\n            <polyline points=\"20 6 9 17 4 12\" />\n          </svg>\n        </span>\n        {label && <span className=\"text-sm text-text leading-snug\">{label}</span>}\n      </label>\n    );\n  }\n);\nCheckbox.displayName = \"Checkbox\";\n\n/* ----------------------------------------- Radio */\nexport interface RadioProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"type\" | \"size\"> {\n  label?: React.ReactNode;\n}\nexport const Radio = React.forwardRef<HTMLInputElement, RadioProps>(\n  ({ label, id, className, ...rest }, ref) => {\n    const generatedId = React.useId();\n    const inputId = id || generatedId;\n    return (\n      <label htmlFor={inputId} className=\"inline-flex items-start gap-2.5 cursor-pointer select-none\">\n        <span className=\"relative inline-flex w-5 h-5 shrink-0 items-center justify-center mt-0.5\">\n          <input ref={ref} id={inputId} type=\"radio\" className=\"peer sr-only\" {...rest} />\n          <span\n            aria-hidden\n            className={cx(\n              \"w-5 h-5 rounded-pill border transition-colors duration-fast ease-standard\",\n              \"peer-checked:border-brand peer-checked:border-[6px]\",\n              \"border-border-strong bg-canvas\",\n              \"peer-disabled:opacity-50 peer-focus-visible:ring-2 peer-focus-visible:ring-brand/30\",\n              className\n            )}\n          />\n        </span>\n        {label && <span className=\"text-sm text-text leading-snug\">{label}</span>}\n      </label>\n    );\n  }\n);\nRadio.displayName = \"Radio\";\n\n/* ----------------------------------------- Switch */\nexport function Switch({\n  checked, onChange, label, disabled, id,\n}: {\n  checked: boolean; onChange: (next: boolean) => void; label?: string; disabled?: boolean; id?: string;\n}) {\n  const generatedSwitchId = React.useId();\n  const switchId = id || generatedSwitchId;\n  return (\n    <label htmlFor={switchId} className={cx(\"inline-flex items-center gap-3 cursor-pointer select-none\", disabled && \"opacity-50 cursor-not-allowed\")}>\n      <button\n        id={switchId} type=\"button\" role=\"switch\" aria-checked={checked} aria-label={label}\n        disabled={disabled} onClick={() => !disabled && onChange(!checked)}\n        className={cx(\n          \"relative inline-flex h-6 w-10 shrink-0 items-center rounded-pill\",\n          \"transition-colors duration-base ease-standard\",\n          \"focus-visible:outline-2 focus-visible:outline-brand focus-visible:outline-offset-2\",\n          checked ? \"bg-brand\" : \"bg-border-strong\"\n        )}\n      >\n        <motion.span\n          aria-hidden\n          animate={{ x: checked ? 18 : 2 }}\n          transition={{ type: \"spring\", stiffness: 700, damping: 32 }}\n          className=\"absolute h-5 w-5 rounded-pill bg-canvas shadow-sm\"\n        />\n      </button>\n      {label && <span className=\"text-sm text-text\">{label}</span>}\n    </label>\n  );\n}\n\n/* ----------------------------------------- Tabs */\nexport function Tabs({\n  tabs, value, onChange, className, fullWidth = false,\n}: {\n  tabs: Array<{ id: string; label: string; icon?: IconSvgElement }>;\n  value: string;\n  onChange: (id: string) => void;\n  className?: string;\n  fullWidth?: boolean;\n}) {\n  const groupId = React.useId();\n  return (\n    <LayoutGroup id={groupId}>\n      <div role=\"tablist\" className={cx(\"relative inline-flex gap-1 p-1 bg-subtle rounded-md border border-border\", fullWidth && \"w-full\", className)}>\n        {tabs.map((tab) => {\n          const active = tab.id === value;\n          return (\n            <button\n              key={tab.id}\n              role=\"tab\"\n              aria-selected={active}\n              onClick={() => onChange(tab.id)}\n              className={cx(\n                \"relative h-9 px-4 text-sm font-medium rounded-md inline-flex items-center gap-2\",\n                \"transition-colors duration-fast ease-standard\",\n                \"focus-visible:outline-2 focus-visible:outline-brand focus-visible:outline-offset-2\",\n                active ? \"text-text\" : \"text-text-muted hover:text-text\",\n                fullWidth && \"flex-1\"\n              )}\n            >\n              {active && (\n                <motion.span\n                  layoutId=\"astry-tab-pill\"\n                  className=\"absolute inset-0 bg-canvas rounded-md shadow-xs\"\n                  transition={{ type: \"spring\", stiffness: 500, damping: 38 }}\n                />\n              )}\n              <span className=\"relative z-10 inline-flex items-center gap-2\">\n                {tab.icon && <Icon icon={tab.icon} size={15} />}\n                {tab.label}\n              </span>\n            </button>\n          );\n        })}\n      </div>\n    </LayoutGroup>\n  );\n}\n\n/* ----------------------------------------- Avatar (square — sharp brand) */\nexport function Avatar({\n  src, alt, fallback, size = 40, className, status,\n}: {\n  src?: string; alt: string; fallback?: string; size?: number; className?: string;\n  status?: \"online\" | \"offline\" | \"busy\";\n}) {\n  const statusColor = { online: \"bg-success\", offline: \"bg-text-subtle\", busy: \"bg-danger\" };\n  return (\n    <span className={cx(\"relative inline-flex items-center justify-center bg-subtle text-text font-medium overflow-hidden shrink-0 border border-border\", className)}\n          style={{ width: size, height: size, fontSize: Math.round(size * 0.4) }}>\n      {src ? (\n        <Image src={src} alt={alt} width={size} height={size} className=\"w-full h-full object-cover\" />\n      ) : (\n        <span aria-hidden>{fallback ?? alt.slice(0, 1).toUpperCase()}</span>\n      )}\n      {status && (\n        <span aria-label={status} className={cx(\"absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-pill border-2 border-canvas\", statusColor[status])} />\n      )}\n    </span>\n  );\n}\n\nexport function AvatarGroup({\n  avatars, max = 4, size = 32,\n}: {\n  avatars: Array<{ src?: string; alt: string; fallback?: string }>;\n  max?: number; size?: number;\n}) {\n  const visible = avatars.slice(0, max);\n  const remaining = avatars.length - visible.length;\n  return (\n    <div className=\"inline-flex\">\n      {visible.map((a, i) => (\n        <Avatar key={i} {...a} size={size} className={cx(i > 0 && \"-ml-2\", \"ring-2 ring-canvas\")} />\n      ))}\n      {remaining > 0 && (\n        <span\n          className=\"inline-flex items-center justify-center bg-subtle text-text-muted font-medium border-2 border-canvas -ml-2\"\n          style={{ width: size, height: size, fontSize: Math.round(size * 0.35) }}\n        >\n          +{remaining}\n        </span>\n      )}\n    </div>\n  );\n}\n\n/* ----------------------------------------- Skeleton */\nexport function Skeleton({ className }: { className?: string }) {\n  return <span aria-hidden className={cx(\"block bg-subtle rounded-sm animate-pulse\", className)} />;\n}\n\n/* ----------------------------------------- Kbd */\nexport function Kbd({ children }: { children: React.ReactNode }) {\n  return (\n    <kbd className=\"inline-flex items-center justify-center min-w-[1.5rem] h-6 px-1.5 text-xs font-mono font-medium text-text-muted bg-canvas border border-border-strong rounded-xs shadow-xs\">\n      {children}\n    </kbd>\n  );\n}\n\n/* ----------------------------------------- Progress */\nexport function Progress({ value, label, className }: { value: number; label?: string; className?: string }) {\n  return (\n    <div className={cx(\"w-full\", className)}>\n      {label && (\n        <div className=\"flex justify-between mb-1.5\">\n          <span className=\"text-xs text-text-muted\">{label}</span>\n          <span className=\"text-xs font-medium text-text\">{Math.round(value)}%</span>\n        </div>\n      )}\n      <div className=\"h-2 w-full bg-subtle overflow-hidden\">\n        <motion.div\n          className=\"h-full bg-brand\"\n          initial={{ width: 0 }}\n          animate={{ width: `${Math.min(100, Math.max(0, value))}%` }}\n          transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}\n        />\n      </div>\n    </div>\n  );\n}\n\n/* ----------------------------------------- Slider */\nexport function Slider({\n  value, onChange, min = 0, max = 100, step = 1, label, className,\n}: {\n  value: number; onChange: (v: number) => void; min?: number; max?: number; step?: number; label?: string; className?: string;\n}) {\n  return (\n    <div className={cx(\"w-full\", className)}>\n      {label && (\n        <div className=\"flex justify-between mb-1.5\">\n          <span className=\"text-sm font-medium text-text\">{label}</span>\n          <span className=\"text-sm font-mono text-text-muted\">{value}</span>\n        </div>\n      )}\n      <input\n        type=\"range\" min={min} max={max} step={step} value={value} onChange={(e) => onChange(Number(e.target.value))}\n        className=\"w-full appearance-none h-1.5 bg-border-strong accent-brand cursor-pointer\n          [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:h-5\n          [&::-webkit-slider-thumb]:rounded-pill [&::-webkit-slider-thumb]:bg-brand [&::-webkit-slider-thumb]:border-2\n          [&::-webkit-slider-thumb]:border-canvas [&::-webkit-slider-thumb]:shadow-sm\n          [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:active:cursor-grabbing\"\n      />\n    </div>\n  );\n}\n\n/* ----------------------------------------- Breadcrumb */\nexport function Breadcrumb({\n  items, separator = \"/\",\n}: {\n  items: Array<{ label: string; href?: string }>; separator?: React.ReactNode;\n}) {\n  return (\n    <nav aria-label=\"Breadcrumb\" className=\"flex items-center gap-2 text-sm\">\n      {items.map((it, i) => (\n        <React.Fragment key={i}>\n          {it.href ? (\n            <a href={it.href} className=\"text-text-muted hover:text-text transition-colors\">{it.label}</a>\n          ) : (\n            <span className=\"text-text font-medium\">{it.label}</span>\n          )}\n          {i < items.length - 1 && <span className=\"text-brand\">{separator}</span>}\n        </React.Fragment>\n      ))}\n    </nav>\n  );\n}\n\n/* ----------------------------------------- Pagination */\nexport function Pagination({\n  current, total, onChange,\n}: {\n  current: number; total: number; onChange: (p: number) => void;\n}) {\n  const pages: Array<number | \"...\"> = [];\n  for (let i = 1; i <= total; i++) {\n    if (i === 1 || i === total || Math.abs(i - current) <= 1) pages.push(i);\n    else if (pages[pages.length - 1] !== \"...\") pages.push(\"...\");\n  }\n  return (\n    <div className=\"inline-flex items-center gap-1\">\n      <IconButton icon={ArrowLeft01Icon} variant=\"outline\" size=\"sm\" aria-label=\"Previous\"\n        disabled={current <= 1} onClick={() => onChange(Math.max(1, current - 1))} />\n      {pages.map((p, i) =>\n        p === \"...\" ? (\n          <span key={i} className=\"px-2 text-text-subtle text-sm\">…</span>\n        ) : (\n          <button\n            key={i}\n            onClick={() => onChange(p)}\n            className={cx(\n              \"h-9 min-w-[36px] px-2 text-sm font-medium transition-colors duration-fast\",\n              p === current ? \"bg-brand text-text-on-brand\" : \"text-text hover:bg-subtle\"\n            )}\n          >\n            {p}\n          </button>\n        )\n      )}\n      <IconButton icon={ArrowRight01Icon} variant=\"outline\" size=\"sm\" aria-label=\"Next\"\n        disabled={current >= total} onClick={() => onChange(Math.min(total, current + 1))} />\n    </div>\n  );\n}\n\n/* ----------------------------------------- Stepper (horizontal) */\nexport function Stepper({\n  steps, current,\n}: {\n  steps: Array<{ label: string; description?: string }>; current: number;\n}) {\n  return (\n    <ol className=\"flex items-start gap-3 w-full\">\n      {steps.map((step, i) => {\n        const done = i < current;\n        const active = i === current;\n        return (\n          <li key={i} className=\"flex-1 flex items-start gap-3 min-w-0\">\n            <span\n              aria-hidden\n              className={cx(\n                \"flex items-center justify-center w-7 h-7 rounded-pill text-xs font-semibold shrink-0\",\n                done ? \"bg-brand text-text-on-brand\" : active ? \"border-2 border-brand text-brand\" : \"border border-border-strong text-text-subtle\"\n              )}\n            >\n              {done ? \"✓\" : i + 1}\n            </span>\n            <div className=\"flex-1 min-w-0\">\n              <p className={cx(\"text-sm font-medium truncate\", active ? \"text-text\" : done ? \"text-text\" : \"text-text-muted\")}>{step.label}</p>\n              {step.description && <p className=\"text-xs text-text-subtle truncate\">{step.description}</p>}\n            </div>\n            {i < steps.length - 1 && (\n              <span aria-hidden className={cx(\"h-px flex-1 mt-3.5\", done ? \"bg-brand\" : \"bg-border\")} />\n            )}\n          </li>\n        );\n      })}\n    </ol>\n  );\n}\n\n/* ----------------------------------------- Tooltip (CSS-driven, no portal) */\nexport function Tooltip({ content, children }: { content: React.ReactNode; children: React.ReactNode }) {\n  return (\n    <span className=\"relative inline-flex group\">\n      {children}\n      <span\n        role=\"tooltip\"\n        className=\"absolute bottom-[calc(100%+6px)] left-1/2 -translate-x-1/2 z-50 pointer-events-none whitespace-nowrap\n          px-2 py-1 text-xs font-medium text-text-on-brand bg-charcoal rounded-sm\n          opacity-0 group-hover:opacity-100 transition-opacity duration-fast\"\n      >\n        {content}\n      </span>\n    </span>\n  );\n}\n\n/* ----------------------------------------- AstryLogo — icon + wordmark, single source of truth */\nexport function AstryLogo({\n  size = \"md\",\n  tone = \"default\",\n  className,\n  href,\n}: {\n  size?: \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n  tone?: \"default\" | \"knockout\";\n  className?: string;\n  href?: string;\n}) {\n  const map = {\n    xs: { icon: 18, text: \"text-sm\", gap: \"gap-1.5\" },\n    sm: { icon: 22, text: \"text-md\", gap: \"gap-2\" },\n    md: { icon: 28, text: \"text-lg\", gap: \"gap-2\" },\n    lg: { icon: 36, text: \"text-xl\", gap: \"gap-2.5\" },\n    xl: { icon: 44, text: \"text-2xl\", gap: \"gap-3\" },\n  } as const;\n  const s = map[size];\n  const textColor = tone === \"knockout\" ? \"text-text-on-brand\" : \"text-text\";\n  const Wrap: any = href ? \"a\" : \"span\";\n  const wrapProps: any = href ? { href } : {};\n  return (\n    <Wrap\n      className={cx(\"inline-flex items-center\", s.gap, className)}\n      {...wrapProps}\n    >\n      <svg\n        viewBox=\"0 0 84 67\"\n        height={s.icon}\n        width={Math.round((s.icon * 84) / 67)}\n        fill=\"currentColor\"\n        aria-hidden\n        className={cx(\"shrink-0\", textColor)}\n      >\n        <path d=\"M19.5019 67H0L37.5331 0H57.5253L19.5019 67Z\" />\n        <path d=\"M84 67H33.8288L58.7782 24.5082L84 67Z\" />\n      </svg>\n      <span className={cx(\"font-semibold tracking-tight\", s.text, textColor)}>astry</span>\n    </Wrap>\n  );\n}\n\n/* ----------------------------------------- Mascot */\nconst MASCOTS = [\n  // Mood (6)\n  \"happy\", \"sad\", \"love\", \"enjoy\", \"tired\", \"excited\",\n  // Reaction (5)\n  \"agree\", \"thumbsup\", \"surprised\", \"shrug\", \"cheering\",\n  // UX state (5)\n  \"question\", \"sleepy\", \"waving\", \"waiting\", \"focused\",\n  // Pose (5)\n  \"neutral\", \"three-quarter-left\", \"three-quarter-right\", \"side-profile\", \"standing\",\n  // Motion (6)\n  \"moving\", \"running\", \"sprinting\", \"braking\", \"jumping\", \"runny\",\n  // Gesture (7)\n  \"pointing\", \"signaling\", \"signal-flag\", \"spotting\", \"observing\", \"carrying\", \"at-the-system\",\n  // Role · operational verbs (19)\n  \"auditing\", \"mapping\", \"analyzing\", \"implementing\",\n  \"connecting\", \"integrating\", \"reviewing\", \"shipping\",\n  \"scanning\", \"debugging\", \"securing\", \"alerting\",\n  \"measuring\", \"automating\", \"collaborating\", \"deploying\",\n  \"working\", \"searching\", \"thinking\",\n  // Milestone (2)\n  \"party\", \"cool\",\n  // Alert (2)\n  \"worried\", \"confused\",\n] as const;\nexport type MascotEmotion = typeof MASCOTS[number];\nexport function AstryMascot({\n  emotion = \"happy\", size = 96, fill: shouldFill = false, className,\n}: { emotion?: MascotEmotion; size?: number; fill?: boolean; className?: string }) {\n  // `fill` makes the mascot stretch to its parent (parent must have explicit\n  // width/height — typically a square wrapper). Use this for hero heroes where\n  // the surrounding panel determines the size.\n  const wrapperStyle: React.CSSProperties = shouldFill\n    ? { width: \"100%\", height: \"100%\" }\n    : { width: size, height: size };\n  return (\n    <span\n      className={cx(\"relative inline-block shrink-0\", className)}\n      style={wrapperStyle}\n    >\n      <Image\n        src={`/brand/mascot/${emotion}.svg`}\n        alt={`Astry mascot — ${emotion}`}\n        fill\n        sizes={shouldFill ? \"(min-width: 1024px) 30vw, 80vw\" : `${size}px`}\n        className=\"object-contain\"\n      />\n    </span>\n  );\n}\nAstryMascot.emotions = MASCOTS;\n\n/* ----------------------------------------- Re-exports */\nexport { motion, AnimatePresence };\n"
    }
  ],
  "meta": {
    "primitives": [
      "cx",
      "Icon",
      "Eyebrow",
      "AngleCorner",
      "BracketFrame",
      "Button",
      "IconButton",
      "Input",
      "Textarea",
      "Select",
      "Checkbox",
      "Radio",
      "Switch",
      "Card",
      "Badge",
      "Tag",
      "Alert",
      "Avatar",
      "AvatarGroup",
      "Skeleton",
      "Kbd",
      "Tabs",
      "Slider",
      "Stepper",
      "Pagination",
      "Breadcrumb",
      "Progress",
      "Tooltip",
      "AstryLogo",
      "AstryMascot"
    ],
    "details": [
      {
        "name": "cx",
        "kind": "util",
        "description": "tailwind-merge wrapper used by every primitive."
      },
      {
        "name": "Icon",
        "kind": "component",
        "description": "HugeIcons wrapper. Default strokeWidth 1.8.",
        "props": [
          "icon",
          "size",
          "strokeWidth"
        ]
      },
      {
        "name": "Eyebrow",
        "kind": "component",
        "description": "Section label · uppercase · brand-coloured by default.",
        "props": [
          "tone"
        ],
        "tones": [
          "brand",
          "muted",
          "subtle"
        ]
      },
      {
        "name": "AngleCorner",
        "kind": "component",
        "description": "Single brand corner SVG · use for selective treatments.",
        "props": [
          "position",
          "size",
          "color"
        ],
        "positions": [
          "tl",
          "tr",
          "bl",
          "br"
        ]
      },
      {
        "name": "BracketFrame",
        "kind": "component",
        "description": "Wraps content with 4 brand corner angles.",
        "props": [
          "size",
          "color"
        ]
      },
      {
        "name": "Button",
        "kind": "component",
        "description": "Primary CTA primitive. `withAngles` reserved for special CTAs.",
        "props": [
          "variant",
          "size",
          "leftIcon",
          "rightIcon",
          "withAngles"
        ],
        "variants": [
          "primary",
          "secondary",
          "outline",
          "ghost",
          "link",
          "danger",
          "inverse",
          "inverse-ghost"
        ],
        "sizes": [
          "xs",
          "sm",
          "md",
          "lg"
        ]
      },
      {
        "name": "IconButton",
        "kind": "component",
        "description": "Square icon-only button. Requires aria-label.",
        "props": [
          "icon",
          "variant",
          "size",
          "aria-label"
        ]
      },
      {
        "name": "Input",
        "kind": "component",
        "description": "Text input with optional leading/trailing icons.",
        "props": [
          "label",
          "hint",
          "error",
          "success",
          "leadingIcon",
          "trailingIcon",
          "size"
        ],
        "sizes": [
          "sm",
          "md",
          "lg"
        ]
      },
      {
        "name": "Textarea",
        "kind": "component",
        "description": "Multi-line text input.",
        "props": [
          "label",
          "hint",
          "error",
          "rows"
        ]
      },
      {
        "name": "Select",
        "kind": "component",
        "description": "Custom dropdown — replaces native <select>. API: options array.",
        "props": [
          "label",
          "options",
          "value",
          "onChange",
          "placeholder",
          "size"
        ]
      },
      {
        "name": "Checkbox",
        "kind": "component",
        "description": "Square checkbox. Brand fill when checked.",
        "props": [
          "label",
          "checked"
        ]
      },
      {
        "name": "Radio",
        "kind": "component",
        "description": "Pill radio. 6px brand border when active.",
        "props": [
          "label",
          "name",
          "value"
        ]
      },
      {
        "name": "Switch",
        "kind": "component",
        "description": "Animated switch. Pill knob is the only rounded element.",
        "props": [
          "checked",
          "onChange",
          "label",
          "disabled"
        ]
      },
      {
        "name": "Card",
        "kind": "component",
        "description": "Square surface · no shadow by default · hover darkens border.",
        "props": [
          "hover",
          "as"
        ],
        "subcomponents": [
          "CardHeader",
          "CardBody",
          "CardFooter",
          "CardTitle",
          "CardSubtitle"
        ]
      },
      {
        "name": "Badge",
        "kind": "component",
        "description": "Status badge · always pair colour with a dot.",
        "props": [
          "variant",
          "dot",
          "size"
        ],
        "variants": [
          "neutral",
          "brand",
          "success",
          "warning",
          "danger",
          "outline"
        ]
      },
      {
        "name": "Tag",
        "kind": "component",
        "description": "Removable pill-tag for filters and chips.",
        "props": [
          "onRemove",
          "removeIcon"
        ]
      },
      {
        "name": "Alert",
        "kind": "component",
        "description": "Inline message · paired role attribute by variant.",
        "props": [
          "variant",
          "title",
          "icon",
          "onClose",
          "closeIcon"
        ],
        "variants": [
          "info",
          "success",
          "warning",
          "danger"
        ]
      },
      {
        "name": "Avatar",
        "kind": "component",
        "description": "Square avatar · pill status indicator on bottom-right.",
        "props": [
          "src",
          "alt",
          "size",
          "status"
        ],
        "statuses": [
          "online",
          "offline",
          "busy"
        ]
      },
      {
        "name": "AvatarGroup",
        "kind": "component",
        "description": "Stacked avatars with `+N` overflow.",
        "props": [
          "avatars",
          "max",
          "size"
        ]
      },
      {
        "name": "Skeleton",
        "kind": "component",
        "description": "Loading placeholder — animated pulse.",
        "props": []
      },
      {
        "name": "Kbd",
        "kind": "component",
        "description": "Keyboard key chip for shortcuts.",
        "props": []
      },
      {
        "name": "Tabs",
        "kind": "component",
        "description": "Animated pill tabs (framer layoutId).",
        "props": [
          "tabs",
          "value",
          "onChange",
          "fullWidth"
        ]
      },
      {
        "name": "Slider",
        "kind": "component",
        "description": "Range slider · pill thumb · brand accent.",
        "props": [
          "value",
          "onChange",
          "min",
          "max",
          "step",
          "label"
        ]
      },
      {
        "name": "Stepper",
        "kind": "component",
        "description": "Horizontal progress stepper for flows.",
        "props": [
          "steps",
          "current"
        ]
      },
      {
        "name": "Pagination",
        "kind": "component",
        "description": "Numbered pagination · arrows baked in.",
        "props": [
          "current",
          "total",
          "onChange"
        ]
      },
      {
        "name": "Breadcrumb",
        "kind": "component",
        "description": "Crumb trail. Brand-coloured separator.",
        "props": [
          "items",
          "separator"
        ]
      },
      {
        "name": "Progress",
        "kind": "component",
        "description": "Linear progress bar · brand fill.",
        "props": [
          "value",
          "label"
        ]
      },
      {
        "name": "Tooltip",
        "kind": "component",
        "description": "CSS-only hover tooltip · charcoal fill.",
        "props": [
          "content"
        ]
      },
      {
        "name": "AstryLogo",
        "kind": "component",
        "description": "Canonical brand mark · icon + wordmark.",
        "props": [
          "size",
          "tone",
          "href"
        ],
        "tones": [
          "default",
          "knockout"
        ],
        "sizes": [
          "xs",
          "sm",
          "md",
          "lg",
          "xl"
        ]
      },
      {
        "name": "AstryMascot",
        "kind": "component",
        "description": "Mascot SVG. emotion prop is a typed union (see /r/index.json mascots).",
        "props": [
          "emotion",
          "size"
        ]
      }
    ],
    "importPath": "@/components/astry-ui",
    "rules": [
      "Tokens > hex. Never write a hex literal in a component.",
      "Sharp corners. Don't add `rounded-*` classes — radius tokens already resolve to 0.",
      "No card shadows by default. Hover darkens the border.",
      "On brand-blue surfaces: variant=\"inverse\" or \"inverse-ghost\". Don't override Tailwind manually.",
      "Logo is <AstryLogo />. Mascot is <AstryMascot emotion=\"...\" />. Never inline brand assets."
    ]
  }
}