{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"card","type":"registry:component","title":"Card","description":"Plain, shadcn-compatible Card building block (CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter) with default, outline, elevated and interactive variants — the interactive one lifts on hover and dips on press and becomes a whole-card link or button via asChild — plus an optional height-morphing CardExpandTrigger/CardExpandContent disclosure.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/card.tsx","type":"registry:component","target":"@components/motion/card.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/motion/card\n\nimport { ChevronDown } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ComponentPropsWithoutRef,\n  cloneElement,\n  createContext,\n  forwardRef,\n  isValidElement,\n  type ReactElement,\n  useCallback,\n  useContext,\n  useId,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_PRESS } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\n// Card is the plain building block: a surface plus shadcn-compatible slots\n// (CardHeader / CardTitle / CardDescription / CardAction / CardContent /\n// CardFooter). `TiltCard` adds cursor-tracked 3D tilt, `StatCard` is a KPI\n// readout and `BentoCard` a marketing tile; reach for those for their effect,\n// and for this one when you just need a well-behaved container.\n\nexport type CardVariant = \"default\" | \"outline\" | \"elevated\" | \"interactive\";\n\nconst variantClass: Record<CardVariant, string> = {\n  default: \"border border-border bg-card shadow-sm\",\n  outline: \"border border-border bg-transparent\",\n  elevated: \"border border-border bg-card shadow-md\",\n  interactive: cn(\n    \"cursor-pointer border border-border bg-card shadow-sm outline-none transition-[border-color,box-shadow] duration-200\",\n    \"hover:border-border-strong hover:shadow-md\",\n    \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n  ),\n};\n\n/* ------------------------------------------------------------------------ */\n/* Expand state                                                              */\n/* ------------------------------------------------------------------------ */\n\ninterface CardContextValue {\n  expanded: boolean;\n  setExpanded: (next: boolean) => void;\n  contentId: string;\n}\n\nconst CardContext = createContext<CardContextValue | null>(null);\n\nfunction useCardContext(part: string) {\n  const ctx = useContext(CardContext);\n  if (!ctx) throw new Error(`${part} must be used inside <Card>.`);\n  return ctx;\n}\n\n/* ------------------------------------------------------------------------ */\n/* Card                                                                      */\n/* ------------------------------------------------------------------------ */\n\ntype ClonableChild = ReactElement<{ className?: string }>;\n\nexport interface CardProps extends ComponentPropsWithoutRef<\"div\"> {\n  variant?: CardVariant;\n  /**\n   * Merge the card surface onto the single child element instead of\n   * rendering a `<div>` — e.g. `<Card variant=\"interactive\" asChild><a href=\"…\">…</a></Card>`\n   * or a router `<Link>` / `<button>`. Keep other links and buttons out of an\n   * interactive card's children so interactive elements never nest.\n   */\n  asChild?: boolean;\n  /** Controlled open state for `CardExpandTrigger` / `CardExpandContent`. */\n  expanded?: boolean;\n  defaultExpanded?: boolean;\n  onExpandedChange?: (expanded: boolean) => void;\n}\n\nconst surfaceBase = \"group/card relative flex flex-col gap-5 rounded-xl py-5 text-card-foreground\";\n\n/**\n * Card surface. `variant=\"interactive\"` lifts on hover (pointer devices only)\n * and dips on press with `SPRING_PRESS`; pair it with `asChild` so the whole\n * card is one link or button.\n */\nexport const Card = forwardRef<HTMLDivElement, CardProps>(function Card(\n  {\n    variant = \"default\",\n    asChild = false,\n    expanded,\n    defaultExpanded = false,\n    onExpandedChange,\n    className,\n    children,\n    ...rest\n  },\n  ref,\n) {\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  const contentId = useId();\n  const [internal, setInternal] = useState(defaultExpanded);\n  const isExpanded = expanded ?? internal;\n  const setExpanded = useCallback(\n    (next: boolean) => {\n      if (expanded === undefined) setInternal(next);\n      onExpandedChange?.(next);\n    },\n    [expanded, onExpandedChange],\n  );\n\n  const surfaceClass = cn(surfaceBase, variantClass[variant], className);\n  const dataExpanded = isExpanded ? \"true\" : undefined;\n\n  let surface: ReactElement;\n  if (asChild && isValidElement(children)) {\n    const child = children as ClonableChild;\n    surface = cloneElement(child, {\n      ...rest,\n      \"data-slot\": \"card\",\n      \"data-expanded\": dataExpanded,\n      className: cn(surfaceClass, variant === \"interactive\" && \"w-full\", child.props.className),\n    } as Partial<{ className: string }>);\n  } else {\n    surface = (\n      <div\n        ref={variant === \"interactive\" ? undefined : ref}\n        data-slot=\"card\"\n        data-expanded={dataExpanded}\n        className={cn(surfaceClass, variant === \"interactive\" && \"w-full\")}\n        {...rest}\n      >\n        {children}\n      </div>\n    );\n  }\n\n  const content =\n    variant === \"interactive\" ? (\n      // Presentational wrapper carries the transform so the surface (often a\n      // link or router component) needs no motion props of its own.\n      <motion.div\n        ref={ref}\n        className=\"flex rounded-xl\"\n        whileHover={reduce || !canHover ? undefined : { y: -3 }}\n        whileTap={reduce ? undefined : { scale: 0.985 }}\n        transition={SPRING_PRESS}\n      >\n        {surface}\n      </motion.div>\n    ) : (\n      surface\n    );\n\n  return (\n    <CardContext.Provider value={{ expanded: isExpanded, setExpanded, contentId }}>\n      {content}\n    </CardContext.Provider>\n  );\n});\n\n/* ------------------------------------------------------------------------ */\n/* Slots                                                                     */\n/* ------------------------------------------------------------------------ */\n\nexport function CardHeader({ className, ...props }: ComponentPropsWithoutRef<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-header\"\n      className={cn(\n        \"grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-5\",\n        \"has-data-[slot=card-action]:grid-cols-[1fr_auto]\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport function CardTitle({ className, ...props }: ComponentPropsWithoutRef<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-title\"\n      className={cn(\"font-semibold leading-snug tracking-tight text-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function CardDescription({ className, ...props }: ComponentPropsWithoutRef<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-description\"\n      className={cn(\"text-sm text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\n/** Top-right slot inside `CardHeader` (a menu, badge or dismiss button). */\nexport function CardAction({ className, ...props }: ComponentPropsWithoutRef<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-action\"\n      className={cn(\"col-start-2 row-span-2 row-start-1 self-start justify-self-end\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function CardContent({ className, ...props }: ComponentPropsWithoutRef<\"div\">) {\n  return <div data-slot=\"card-content\" className={cn(\"px-5\", className)} {...props} />;\n}\n\nexport function CardFooter({ className, ...props }: ComponentPropsWithoutRef<\"div\">) {\n  return (\n    <div\n      data-slot=\"card-footer\"\n      className={cn(\"flex items-center gap-2 px-5\", className)}\n      {...props}\n    />\n  );\n}\n\n/* ------------------------------------------------------------------------ */\n/* Expandable                                                                */\n/* ------------------------------------------------------------------------ */\n\nexport interface CardExpandTriggerProps\n  extends Omit<ComponentPropsWithoutRef<\"button\">, \"type\"> {\n  /** Hide the rotating chevron. */\n  hideIcon?: boolean;\n}\n\n/** Toggles the card's `CardExpandContent`; a real button with `aria-expanded`. */\nexport function CardExpandTrigger({\n  hideIcon = false,\n  className,\n  children,\n  onClick,\n  ...props\n}: CardExpandTriggerProps) {\n  const ctx = useCardContext(\"CardExpandTrigger\");\n  const reduce = useReducedMotion();\n  return (\n    <button\n      type=\"button\"\n      data-slot=\"card-expand-trigger\"\n      aria-expanded={ctx.expanded}\n      aria-controls={ctx.expanded ? ctx.contentId : undefined}\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) ctx.setExpanded(!ctx.expanded);\n      }}\n      className={cn(\n        \"inline-flex items-center gap-1.5 rounded-md text-sm font-medium text-muted-foreground outline-none transition-colors hover:text-foreground\",\n        \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      {hideIcon ? null : (\n        <motion.span\n          aria-hidden\n          className=\"inline-flex\"\n          animate={{ rotate: ctx.expanded ? 180 : 0 }}\n          transition={reduce ? { duration: 0 } : SPRING_PRESS}\n        >\n          <ChevronDown className=\"size-4\" />\n        </motion.span>\n      )}\n    </button>\n  );\n}\n\n/** Collapsible region of the card, revealed by `CardExpandTrigger`. */\nexport function CardExpandContent({\n  className,\n  children,\n  ...props\n}: Omit<\n  ComponentPropsWithoutRef<\"div\">,\n  \"onAnimationStart\" | \"onDrag\" | \"onDragStart\" | \"onDragEnd\" | \"onDragOver\"\n>) {\n  const ctx = useCardContext(\"CardExpandContent\");\n  const reduce = useReducedMotion();\n  return (\n    <AnimatePresence initial={false}>\n      {ctx.expanded ? (\n        // Height is animated on purpose: disclosing content is this part's\n        // layout job. Reduced motion skips the height morph and only fades.\n        <motion.div\n          key=\"content\"\n          id={ctx.contentId}\n          data-slot=\"card-expand-content\"\n          initial={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}\n          animate={reduce ? { opacity: 1 } : { height: \"auto\", opacity: 1 }}\n          exit={\n            reduce\n              ? { opacity: 0, transition: { duration: 0.12 } }\n              : { height: 0, opacity: 0, transition: { duration: 0.18, ease: EASE_OUT } }\n          }\n          transition={{ duration: 0.26, ease: EASE_OUT }}\n          // -mt-5 cancels the card's flex gap so the region grows from zero\n          // without a jump; the inner pt-5 restores that gap once open.\n          className=\"-mt-5 overflow-hidden\"\n        >\n          <div className={cn(\"px-5 pt-5\", className)} {...props}>\n            {children}\n          </div>\n        </motion.div>\n      ) : null}\n    </AnimatePresence>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/hooks/use-hover-capable.ts","type":"registry:hook","target":"@lib/hooks/use-hover-capable.ts","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true only on devices that have a true hover (mouse / trackpad).\n * Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere\n * — gate hover-only effects (scale lifts, magnetic pulls) behind this.\n */\nexport function useHoverCapable() {\n  const [canHover, setCanHover] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) return;\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    const update = () => setCanHover(mq.matches);\n    update();\n    mq.addEventListener?.(\"change\", update);\n    return () => mq.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canHover;\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}