diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,29 @@
 # Changelog
 
+## 0.12.0.0 (2024-02-27)
+
+ *  Render tabs in code blocks by expanding them to spaces.  The amount of
+    spaces a tab character aligns to is customizable using `tabStop`, e.g.
+    `tabStop: 8`.  The default is 4.
+
+ *  Rename eval.wrap to eval.container (#167)
+
+    `wrap` is used at the top-level of settings for wrapping at a certain
+    column, and inside `eval` to determine the type in which the result
+    is "wrapped". Using the same name for both is confusing, so this adds
+    `eval.container` as the new name for `eval.wrap`. `eval.wrap` will continue
+    to be supported for the forseeable future, but its use will be discouraged.
+
+    This also changes the values (again keeping the original ones for
+    backwards-compat), so the complete changes to a configuration would be:
+
+     -   `wrap: code` becomes `container: code`
+     -   `wrap: raw` becomes `container: none`
+     -   `wrap: rawInline` becomes `container: inline`
+
+ *  Add a `type: matrix` transition effect, loosely inspired by the 1999 science
+    fiction movie.
+
 ## 0.11.0.0 (2024-02-14)
 
  *  Support wrapping at a specific column (#164)
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@
 - [Centering](#centering) and [re-wrapping](#line-wrapping) text to terminal
   width with proper indentation.
 - [Theming](#theming) support including 24-bit RGB.
-- Hihgly portable as it only requires an ANSI terminal as opposed to
+- Highly portable as it only requires an ANSI terminal as opposed to
   something like `ncurses`.
 
 ![screenshot](extra/demo.gif?raw=true)
@@ -42,6 +42,7 @@
 -   [Input format](#input-format)
 -   [Configuration](#configuration)
     -   [Line wrapping](#line-wrapping)
+    -   [Tab stop](#tab-stop)
     -   [Margins](#margins)
     -   [Auto advancing](#auto-advancing)
     -   [Advanced slide splitting](#advanced-slide-splitting)
@@ -266,7 +267,8 @@
     Hello world.
     ```
 
-3.  Within a slide, using a comment starting with `<!--config:`.  These
+3.  In version 0.10 and later slides can be individually configured.
+    Within a slide, using a comment starting with `<!--config:`.  These
     settings can override configuration for that specific slide only.
     There should not be any whitespace between `<!--` and `config:`.
 
@@ -302,8 +304,14 @@
 
 Line wrapping can be enabled by setting `wrap: true` in the configuration.  This
 will re-wrap all lines to fit the terminal width better.  You can also ask patat
-to wrap at a specific column using e.g. `wrap: 60`.
+to wrap at a specific column using `wrap: number`, e.g. `wrap: 60`.
 
+### Tab stop
+
+In version 0.12 and later, the amount of spaces a `\t` character in a code block
+aligns to can be customized by setting `tabStop: number` in the configuration.
+The default is `4`.
+
 ### Margins
 
 Margins can be enabled by setting a `margins` entry in the configuration:
@@ -336,9 +344,13 @@
 
 #### Centering
 
-To vertically center content, use `top: auto`. To horizontally center content,
-use both `left: auto` and `right: auto`.  For example:
+Version 0.11 and later can center content.
 
+ -  To vertically center content, use `top: auto`.
+ -  To horizontally center content, use both `left: auto` and `right: auto`.
+
+For example:
+
 ```markdown
 ---
 title: Centered presentation
@@ -611,7 +623,7 @@
 
 #### Native Images support
 
-`patat-0.8.0.0` and newer include images support for some terminal emulators.
+Version 0.8 and later include images support for some terminal emulators.
 
 ```markdown
 ---
@@ -656,7 +668,7 @@
 [code evaluation](#evaluating-code).
 
 In order to do that, for example, we could configure `kitten` code snippets
-to evaluate using [Kitty]'s command `icat`.  This uses the `rawInline` code
+to evaluate using [Kitty]'s command `icat`.  This uses the `none` container
 setting to ensure that the resulting output is not wrapped in a code block,
 and the `fragment` and `replace` settings immediately replace the snippet.
 
@@ -667,7 +679,7 @@
           command: sed 's/^/kitten /' | bash
           replace: true
           fragment: false
-          wrap: rawInline
+          container: none
     ...
 
     See, for example:
@@ -712,7 +724,7 @@
           command: irb --noecho --noverbose
           fragment: true  # Optional
           replace: false  # Optional
-          wrap: code  # Optional
+          container: code  # Optional
     ...
 
     Here is an example of a code block that is evaluated:
@@ -728,18 +740,20 @@
 code of presentations downloaded from the internet before running them if they
 contain `eval` settings.
 
-Aside from the command, there are two more options:
+Aside from the command, there are three more options:
 
  -  `fragment`: Introduce a pause (see [fragments](#fragmented-slides)) in
     between showing the original code block and the output.  Defaults to `true`.
  -  `replace`: Remove the original code block and replace it with the output
     rather than appending the output in a new code block.  Defaults to `false`.
- -  `wrap`: By default, the output is wrapped in a code block again with the
-    original syntax highlighting.  You can customize this behaviour by setting
-    `wrap` to:
+ -  `container`: By default, the output is wrapped in a code block again with
+    the original syntax highlighting.  You can customize this behaviour by
+    setting `container` to:
      *  `code`: the default setting.
-     *  `raw`: no formatting applied.
-     *  `rawInline`: no formatting applied and no trailing newline.
+     *  `none`: no formatting applied.
+     *  `inline`: no formatting applied and no trailing newline.
+ -  `wrap`: this is a deprecated name for `container`, used in version 0.11 and
+    earlier.
 
 Setting `fragment: false` and `replace: true` offers a way to "filter" code
 blocks, which can be used to render ASCII graphics.
@@ -766,7 +780,7 @@
 
 ### Speaker Notes
 
-`patat` supports comments which can be used as speaker notes.
+Version 0.9 and later support comments which can be used as speaker notes.
 
 ```markdown
 ---
@@ -817,10 +831,10 @@
 
 ### Transitions
 
-`patat` supports transitions in between slides.  A relatively fast terminal
-emulator (e.g. [Alacritty], [Kitty], [iTerm2])
-is suggested when enabling this, to avoid too much flickering -- some
-flickering is unavoidable since we redraw the entire screen on each frame.
+Version 0.10 and later support transitions in between slides.  A relatively
+fast terminal emulator (e.g. [Alacritty], [Kitty], [iTerm2]) is suggested when
+enabling this, to avoid too much flickering -- some flickering is unavoidable
+since we redraw the entire screen on each frame.
 
 ```yaml
 patat:
@@ -851,6 +865,7 @@
 
  -  `slideLeft`: slides the new slide in from right to left.
  -  `dissolve`: changes characters over time.
+ -  `matrix`: loosely inspired by the 1999 science fiction movie.
 
 All transitions currently take these arguments:
 
diff --git a/lib/Patat/Eval.hs b/lib/Patat/Eval.hs
--- a/lib/Patat/Eval.hs
+++ b/lib/Patat/Eval.hs
@@ -72,10 +72,10 @@
                     evalCommand <> ": exit code " <> T.pack (show i) <> "\n" <>
                     erStderr
         let fmt = "eval"
-            blocks = case evalWrap of
-                EvalWrapCode      -> [Pandoc.CodeBlock attr out]
-                EvalWrapRaw       -> [Pandoc.RawBlock fmt out]
-                EvalWrapRawInline -> [Pandoc.Plain [Pandoc.RawInline fmt out]]
+            blocks = case evalContainer of
+                EvalContainerCode   -> [Pandoc.CodeBlock attr out]
+                EvalContainerNone   -> [Pandoc.RawBlock fmt out]
+                EvalContainerInline -> [Pandoc.Plain [Pandoc.RawInline fmt out]]
         pure $ case (evalFragment, evalReplace) of
             (False, True) -> [Append blocks]
             (False, False) -> [Append (orig : blocks)]
diff --git a/lib/Patat/Presentation/Display.hs b/lib/Patat/Presentation/Display.hs
--- a/lib/Patat/Presentation/Display.hs
+++ b/lib/Patat/Presentation/Display.hs
@@ -60,8 +60,9 @@
     settings  = activeSettings pres
     ds        = DisplaySettings
         { dsSize          = canvasSize
-        , dsWrap          = fromMaybe NoWrap $ psWrap settings
         , dsMargins       = margins settings
+        , dsWrap          = fromMaybe NoWrap $ psWrap settings
+        , dsTabStop       = maybe 4 A.unFlexibleNum $ psTabStop settings
         , dsTheme         = fromMaybe Theme.defaultTheme (psTheme settings)
         , dsSyntaxMap     = pSyntaxMap
         }
diff --git a/lib/Patat/Presentation/Display/CodeBlock.hs b/lib/Patat/Presentation/Display/CodeBlock.hs
--- a/lib/Patat/Presentation/Display/CodeBlock.hs
+++ b/lib/Patat/Presentation/Display/CodeBlock.hs
@@ -1,5 +1,6 @@
 --------------------------------------------------------------------------------
 -- | Displaying code blocks, optionally with syntax highlighting.
+{-# LANGUAGE BangPatterns      #-}
 {-# LANGUAGE OverloadedStrings #-}
 module Patat.Presentation.Display.CodeBlock
     ( prettyCodeBlock
@@ -7,6 +8,7 @@
 
 
 --------------------------------------------------------------------------------
+import           Data.Char.WCWidth                   (wcwidth)
 import           Data.Maybe                          (mapMaybe)
 import qualified Data.Text                           as T
 import           Patat.Presentation.Display.Internal
@@ -50,12 +52,30 @@
 
 
 --------------------------------------------------------------------------------
+-- | Expands tabs in code lines.
+expandTabs :: Int -> Skylighting.SourceLine -> Skylighting.SourceLine
+expandTabs tabStop = goTokens 0
+  where
+    goTokens _    []                        = []
+    goTokens col0 ((tokType, txt) : tokens) = goString col0 "" (T.unpack txt) $
+        \col1 str -> (tokType, T.pack str) : goTokens col1 tokens
+
+    goString :: Int -> String -> String -> (Int -> String -> k) -> k
+    goString !col acc str k = case str of
+        []       -> k col (reverse acc)
+        '\t' : t -> goString (col + spaces) (replicate spaces ' ' ++ acc) t k
+        c    : t -> goString (col + wcwidth c) (c : acc) t k
+      where
+        spaces = tabStop - col `mod` tabStop
+
+
+--------------------------------------------------------------------------------
 prettyCodeBlock :: DisplaySettings -> [T.Text] -> T.Text -> PP.Doc
 prettyCodeBlock ds classes rawCodeBlock =
     PP.vcat (map blockified sourceLines) <> PP.hardline
   where
     sourceLines :: [Skylighting.SourceLine]
-    sourceLines =
+    sourceLines = map (expandTabs (dsTabStop ds)) $
         [[]] ++ highlight (dsSyntaxMap ds) classes rawCodeBlock ++ [[]]
 
     prettySourceLine :: Skylighting.SourceLine -> PP.Doc
@@ -65,7 +85,7 @@
     prettyToken (tokenType, str) = themed
         ds
         (\theme -> syntaxHighlight theme tokenType)
-        (PP.string $ T.unpack str)
+        (PP.text str)
 
     sourceLineLength :: Skylighting.SourceLine -> Int
     sourceLineLength line = sum [T.length str | (_, str) <- line]
diff --git a/lib/Patat/Presentation/Display/Internal.hs b/lib/Patat/Presentation/Display/Internal.hs
--- a/lib/Patat/Presentation/Display/Internal.hs
+++ b/lib/Patat/Presentation/Display/Internal.hs
@@ -18,6 +18,7 @@
 data DisplaySettings = DisplaySettings
     { dsSize      :: !Size
     , dsWrap      :: !Wrap
+    , dsTabStop   :: !Int
     , dsMargins   :: !Margins
     , dsTheme     :: !Theme.Theme
     , dsSyntaxMap :: !Skylighting.SyntaxMap
diff --git a/lib/Patat/Presentation/Settings.hs b/lib/Patat/Presentation/Settings.hs
--- a/lib/Patat/Presentation/Settings.hs
+++ b/lib/Patat/Presentation/Settings.hs
@@ -16,7 +16,7 @@
     , ImageSettings (..)
 
     , EvalSettingsMap
-    , EvalSettingsWrap (..)
+    , EvalSettingsContainer (..)
     , EvalSettings (..)
 
     , SpeakerNotesSettings (..)
@@ -49,6 +49,7 @@
     , psColumns           :: !(Maybe (A.FlexibleNum Int))
     , psMargins           :: !(Maybe MarginSettings)
     , psWrap              :: !(Maybe Wrap)
+    , psTabStop           :: !(Maybe (A.FlexibleNum Int))
     , psTheme             :: !(Maybe Theme.Theme)
     , psIncrementalLists  :: !(Maybe Bool)
     , psAutoAdvanceDelay  :: !(Maybe (A.FlexibleNum Int))
@@ -71,6 +72,7 @@
         , psColumns           = on mplus psColumns           l r
         , psMargins           = on (<>)  psMargins           l r
         , psWrap              = on mplus psWrap              l r
+        , psTabStop           = on mplus psTabStop           l r
         , psTheme             = on (<>)  psTheme             l r
         , psIncrementalLists  = on mplus psIncrementalLists  l r
         , psAutoAdvanceDelay  = on mplus psAutoAdvanceDelay  l r
@@ -90,9 +92,9 @@
 instance Monoid PresentationSettings where
     mappend = (<>)
     mempty  = PresentationSettings
-                    Nothing Nothing Nothing Nothing Nothing Nothing Nothing
-                    Nothing Nothing Nothing Nothing Nothing Nothing Nothing
-                    Nothing Nothing
+                Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
+                Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
+                Nothing
 
 
 --------------------------------------------------------------------------------
@@ -221,38 +223,52 @@
 
 
 --------------------------------------------------------------------------------
-data EvalSettingsWrap
-    = EvalWrapCode
-    | EvalWrapRaw
-    | EvalWrapRawInline
+data EvalSettingsContainer
+    = EvalContainerCode
+    | EvalContainerNone
+    | EvalContainerInline
     deriving (Show)
 
 
 --------------------------------------------------------------------------------
-instance A.FromJSON EvalSettingsWrap where
-    parseJSON = A.withText "FromJSON EvalSettingsWrap" $ \txt -> case txt of
-        "code"      -> pure EvalWrapCode
-        "raw"       -> pure EvalWrapRaw
-        "rawInline" -> pure EvalWrapRawInline
-        _           -> fail $ "unknown wrap: " <> show txt
+instance A.FromJSON EvalSettingsContainer where
+    parseJSON = A.withText "FromJSON EvalSettingsContainer" $ \t -> case t of
+        "code"      -> pure EvalContainerCode
+        "none"      -> pure EvalContainerNone
+        "inline"    -> pure EvalContainerInline
+        -- Deprecated names
+        "raw"       -> pure EvalContainerNone
+        "rawInline" -> pure EvalContainerInline
+        _           -> fail $ "unknown container: " <> show t
 
 
 --------------------------------------------------------------------------------
 data EvalSettings = EvalSettings
-    { evalCommand  :: !T.Text
-    , evalReplace  :: !Bool
-    , evalFragment :: !Bool
-    , evalWrap     :: !EvalSettingsWrap
+    { evalCommand   :: !T.Text
+    , evalReplace   :: !Bool
+    , evalFragment  :: !Bool
+    , evalContainer :: !EvalSettingsContainer
     } deriving (Show)
 
 
 --------------------------------------------------------------------------------
 instance A.FromJSON EvalSettings where
     parseJSON = A.withObject "FromJSON EvalSettings" $ \o -> EvalSettings
-        <$> o A..: "command"
+        <$> o A..:  "command"
         <*> o A..:? "replace"  A..!= False
         <*> o A..:? "fragment" A..!= True
-        <*> o A..:? "wrap"     A..!= EvalWrapCode
+        <*> deprecated "wrap" "container" EvalContainerCode o
+      where
+        deprecated old new def obj = do
+            mo <- obj A..:? old
+            mn <- obj A..:? new
+            case (mo, mn) of
+                (Just _, Just _)   -> fail $
+                    show old ++ " (deprecated) and " ++ show new ++ " " ++
+                    "are both specified, please remove " ++ show old
+                (Just o, Nothing)  -> pure o
+                (Nothing, Just n)  -> pure n
+                (Nothing, Nothing) -> pure def
 
 
 --------------------------------------------------------------------------------
diff --git a/lib/Patat/Transition.hs b/lib/Patat/Transition.hs
--- a/lib/Patat/Transition.hs
+++ b/lib/Patat/Transition.hs
@@ -29,6 +29,7 @@
 import           Patat.Presentation.Settings (TransitionSettings (..))
 import qualified Patat.Transition.Dissolve   as Dissolve
 import           Patat.Transition.Internal
+import qualified Patat.Transition.Matrix     as Matrix
 import qualified Patat.Transition.SlideLeft  as SlideLeft
 import           System.Random               (uniformR)
 
@@ -54,6 +55,7 @@
 transitions :: NonEmpty (Text, Transition)
 transitions =
     ("dissolve",  Transition Dissolve.transition) :|
+    ("matrix",    Transition Matrix.transition) :
     ("slideLeft", Transition SlideLeft.transition) : []
 
 
diff --git a/lib/Patat/Transition/Matrix.hs b/lib/Patat/Transition/Matrix.hs
new file mode 100644
--- /dev/null
+++ b/lib/Patat/Transition/Matrix.hs
@@ -0,0 +1,128 @@
+--------------------------------------------------------------------------------
+{-# LANGUAGE TemplateHaskell #-}
+module Patat.Transition.Matrix
+    ( transition
+    ) where
+
+
+--------------------------------------------------------------------------------
+import           Control.Monad             (forM_, guard, when)
+import qualified Data.Aeson.Extended       as A
+import qualified Data.Aeson.TH.Extended    as A
+import           Data.Bifunctor            (first)
+import qualified Data.Vector               as V
+import qualified Data.Vector.Mutable       as VM
+import           Patat.PrettyPrint.Matrix
+import           Patat.Size                (Size (..))
+import           Patat.Transition.Internal
+import           System.Random.Stateful
+
+
+--------------------------------------------------------------------------------
+data Config = Config
+    { cDuration  :: Maybe (A.FlexibleNum Double)
+    , cFrameRate :: Maybe (A.FlexibleNum Int)
+    }
+
+
+--------------------------------------------------------------------------------
+data Particle = Particle
+    { pX        :: Double
+    , pInitialY :: Double
+    , pFinalY   :: Double
+    , pSpeed    :: Double
+    , pCell     :: Cell
+    }
+
+
+--------------------------------------------------------------------------------
+particleY :: Particle -> Double -> Double
+particleY p t = pInitialY p * (1 - t') + pFinalY p * t'
+  where
+    t' = min 1 (pSpeed p * t)
+
+
+--------------------------------------------------------------------------------
+-- | Maximum speed of a particle, expressed as a factor of the minimum speed of
+-- a particle.
+particleMaxSpeed :: Double
+particleMaxSpeed = 2
+
+
+--------------------------------------------------------------------------------
+-- | Number of ghosts a particle leaves behind.  Currently hardcoded but could
+-- be moved to config.
+particleGhosts :: Int
+particleGhosts = 3
+
+
+--------------------------------------------------------------------------------
+transition :: Config -> TransitionGen
+transition config (Size rows cols) initial final rgen =
+    first frame <$>
+    evenlySpacedFrames
+        (A.unFlexibleNum <$> cDuration  config)
+        (A.unFlexibleNum <$> cFrameRate config)
+  where
+    speeds :: V.Vector Double
+    speeds = runStateGen_ rgen $ \g ->
+        V.replicateM (rows * cols) (uniformRM (1, particleMaxSpeed) g)
+
+    up :: V.Vector Bool
+    up = runStateGen_ rgen $ \g ->
+        V.replicateM (rows * cols) (uniformM g)
+
+    ghosts :: Double -> [Double]
+    ghosts baseSpeed =
+        [ baseSpeed * (1 + fromIntegral i / fromIntegral particleGhosts)
+        | i <- [0 .. particleGhosts]
+        ]
+
+    initialParticles :: [Particle]
+    initialParticles = do
+        (x, y, cell) <- posCells initial
+        let idx = y * cols + x
+        speed <- ghosts $ speeds V.! idx
+        pure Particle
+            { pX        = fromIntegral x
+            , pInitialY = fromIntegral y
+            , pFinalY   = if up V.! idx then 0 else fromIntegral rows
+            , pSpeed    = speed
+            , pCell     = cell
+            }
+
+    finalParticles :: [Particle]
+    finalParticles = do
+        (x, y, cell) <- posCells final
+        let idx = y * cols + x
+        speed <- ghosts $ speeds V.! idx
+        pure Particle
+            { pX        = fromIntegral x
+            , pInitialY = if up V.! idx then -1 else fromIntegral rows
+            , pFinalY   = fromIntegral y
+            , pSpeed    = speed
+            , pCell     = cell
+            }
+
+    posCells :: Matrix -> [(Int, Int, Cell)]
+    posCells mat = do
+        y <- [0 .. rows - 1]
+        x <- [0 .. cols - 1]
+        let cell = mat V.! (y * cols + x)
+        guard . not $ cell == emptyCell
+        pure (x, y, cell)
+
+    frame :: Double -> Matrix
+    frame t = V.create $ do
+        mat <- VM.replicate (rows * cols) emptyCell
+        forM_ (initialParticles ++ finalParticles) $ \particle ->
+            let y = round $ particleY particle t
+                x = round $ pX particle
+                idx = y * cols + x in
+            when (x >= 0 && x < cols && y >= 0 && y < rows) $
+                VM.write mat idx $ pCell particle
+        pure mat
+
+
+--------------------------------------------------------------------------------
+$(A.deriveFromJSON A.dropPrefixOptions ''Config)
diff --git a/patat.cabal b/patat.cabal
--- a/patat.cabal
+++ b/patat.cabal
@@ -1,5 +1,5 @@
 Name:          patat
-Version:       0.11.0.0
+Version:       0.12.0.0
 Synopsis:      Terminal-based presentations using Pandoc
 Description:   Terminal-based presentations using Pandoc.
 License:       GPL-2
@@ -97,6 +97,7 @@
     Patat.Transition
     Patat.Transition.Internal
     Patat.Transition.Dissolve
+    Patat.Transition.Matrix
     Patat.Transition.SlideLeft
 
   Other-modules:
